From e0d9c1d20ecb198285182e2314c104ccd84b319a Mon Sep 17 00:00:00 2001 From: DanielS Date: Thu, 20 Aug 2026 17:16:25 +0200 Subject: [PATCH 1/3] fix(wizard): fix UI/UX and usability glitches - constrain viewport height on wizard routes in HeaderWrapper - remove nested scrollbar in step-software - add active module glows and tooltips for locked licenses - show device name badge in step 3 card header when editing - update main action button text in summary sidebar when editing - implement double-click inline delete confirmation in step-summary - animate customer accordion and add tab-index key bindings --- shop/components/HeaderWrapper.tsx | 6 +- shop/components/customer-accordion-list.tsx | 64 ++++++++++++++------- shop/components/order-wizard.tsx | 1 + shop/components/wizard/step-software.tsx | 20 +++++-- shop/components/wizard/step-summary.tsx | 22 +++++-- shop/components/wizard/summary-sidebar.tsx | 6 +- 6 files changed, 84 insertions(+), 35 deletions(-) diff --git a/shop/components/HeaderWrapper.tsx b/shop/components/HeaderWrapper.tsx index b7010ff..fa08852 100644 --- a/shop/components/HeaderWrapper.tsx +++ b/shop/components/HeaderWrapper.tsx @@ -17,8 +17,10 @@ export function HeaderWrapper({ navbar, children }: HeaderWrapperProps) { return <>{children}; } + const isWizard = pathname === "/order" || pathname === "/wizard"; + return ( -
+
{/* Demo Banner */}
+
{children}
diff --git a/shop/components/customer-accordion-list.tsx b/shop/components/customer-accordion-list.tsx index 72373b3..98f8235 100644 --- a/shop/components/customer-accordion-list.tsx +++ b/shop/components/customer-accordion-list.tsx @@ -1,6 +1,7 @@ 'use client' import React, { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' import Link from 'next/link' import type { EndCustomerWithDevices, FlattenedDevice } from '@/lib/types' import { @@ -250,7 +251,16 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps) {/* Akkordeon-Header */}
toggleCustomer(customer.id)} - className="p-5 flex items-center justify-between cursor-pointer hover:bg-white/[0.02] transition-colors select-none gap-4 flex-wrap" + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + toggleCustomer(customer.id) + } + }} + tabIndex={0} + role="button" + aria-expanded={isOpen} + className="p-5 flex items-center justify-between cursor-pointer hover:bg-white/[0.02] transition-colors select-none gap-4 flex-wrap focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset" >
@@ -324,28 +334,38 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
{/* Ausgeklappter Bereich mit Kassen */} - {isOpen && ( -
- {devices.length === 0 ? ( -
- Keine Kassen oder Bestellungen für diesen Kunden vorhanden. + + {isOpen && ( + +
+ {devices.length === 0 ? ( +
+ Keine Kassen oder Bestellungen für diesen Kunden vorhanden. +
+ ) : ( +
+

+ Zugeordnete Kassen ({devices.length}) +

+ {devices.map((device, idx) => ( + + ))} +
+ )}
- ) : ( -
-

- Zugeordnete Kassen ({devices.length}) -

- {devices.map((device, idx) => ( - - ))} -
- )} -
- )} + + )} + ) })} diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index faaa20d..367f68b 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -1085,6 +1085,7 @@ export function OrderWizard({ billingBadgeClass={billingBadgeClass} existingModuleIds={existingModuleIds} activeCategoryId={activeCategoryId} + editingDeviceName={editingIdx !== null ? (basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`) : null} />
diff --git a/shop/components/wizard/step-software.tsx b/shop/components/wizard/step-software.tsx index ea63193..08febab 100644 --- a/shop/components/wizard/step-software.tsx +++ b/shop/components/wizard/step-software.tsx @@ -29,6 +29,7 @@ interface StepSoftwareProps { /** Modul-IDs, die bereits lizenziert sind (Upgrade-Modus) */ existingModuleIds?: string[] activeCategoryId: string | null + editingDeviceName?: string | null } export function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { @@ -52,6 +53,7 @@ export function StepSoftware({ billingBadgeClass, existingModuleIds = [], activeCategoryId, + editingDeviceName = null, }: StepSoftwareProps) { const currentCategory = visibleCategories.find(c => c.id === activeCategoryId) ?? visibleCategories[0] ?? null @@ -69,15 +71,22 @@ export function StepSoftware({ return ( - - - Optionen wählen + +
+ + Optionen wählen +
+ {editingDeviceName && ( + + ✏️ Bearbeite Kasse: "{editingDeviceName}" + + )}
Passen Sie die Konfiguration für {currentCategory.name} an.
- + diff --git a/shop/components/wizard/step-summary.tsx b/shop/components/wizard/step-summary.tsx index 11a40ee..ca27f77 100644 --- a/shop/components/wizard/step-summary.tsx +++ b/shop/components/wizard/step-summary.tsx @@ -70,6 +70,8 @@ export function StepSummary({ onAddNewBasketItem, onDeleteBasketItem, }: StepSummaryProps) { + const [confirmDeleteIdx, setConfirmDeleteIdx] = React.useState(null) + return (
{/* LINKER BEREICH: Scrollbares Bedienfeld */} @@ -211,16 +213,26 @@ export function StepSummary({ {onDeleteBasketItem && ( )}
diff --git a/shop/components/wizard/summary-sidebar.tsx b/shop/components/wizard/summary-sidebar.tsx index 420346a..4ddae47 100644 --- a/shop/components/wizard/summary-sidebar.tsx +++ b/shop/components/wizard/summary-sidebar.tsx @@ -283,7 +283,11 @@ export function SummarySidebar({ onClick={nextStep} disabled={isNextStepDisabled && basketItems.length === 0} > - Weiter zu Schritt 4 + {editingIdx !== null ? ( + <>Änderungen für "{basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`}" übernehmen + ) : ( + <>Weiter zu Schritt 4 + )} - -
@@ -372,7 +457,7 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
- {/* Geräte & Produktauswahl (Snapshot Aufteilung) */} -
-
- - Konfigurierte Kassen & Module ({(selectedOrder.order_data?.items || []).length}) -
- -
- {(selectedOrder.order_data?.items || []).map((item: any, idx: number) => ( -
-
-
- - {item.device_name || `Kasse #${idx + 1}`} - - {item.product_name} + {/* Geräte & Produktauswahl (Snapshot Aufteilung) */} +
+
+
+ + Konfigurierte Kassen & Module ({displayItems.length})
- {item.price && ( - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.price)} - + {selectedOrder.status === 'in_review' && !isEditingSnapshot && ( + )}
- {/* Ausgewählte Module & Lizenzen */} - {item.selected_modules?.length > 0 && ( -
-
- - Zusatzmodule ({item.selected_modules.length}): -
-
- {item.selected_modules.map((mod: any, mIdx: number) => ( -
- - {mod.module_name || mod.name} - - {mod.price && ( - - +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price)} - - )} -
- ))} + {isEditingSnapshot && ( +
+ Sie befinden sich im Preiskorrektur-Modus. +
+ +
)} + +
+ {displayItems.map((item: any, idx: number) => ( +
+
+
+ + {item.device_name || `Kasse #${idx + 1}`} + + {item.product_name} +
+ + {isEditingSnapshot ? ( +
+ + updateItemBasePrice(idx, parseFloat(e.target.value) || 0)} + className="w-20 h-7 bg-slate-950 border-white/10 text-white text-xs px-2" + /> +
+ ) : ( + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.base_price || 0)} + + )} +
+ + {/* Ausgewählte Module & Lizenzen */} + {item.selected_modules?.length > 0 && ( +
+
+ + Zusatzmodule ({item.selected_modules.length}): +
+
+ {item.selected_modules.map((mod: any, mIdx: number) => ( +
+ + {mod.module_name || mod.name} + + + {isEditingSnapshot ? ( +
+ updateModulePriceOrQty(idx, mIdx, 'price', parseFloat(e.target.value) || 0)} + className="w-14 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1" + /> + x + updateModulePriceOrQty(idx, mIdx, 'quantity', parseInt(e.target.value) || 1)} + className="w-10 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1" + /> +
+ ) : ( + + +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * (mod.quantity || 1))} + + )} +
+ ))} +
+
+ )} +
+ ))} +
- ))} -
-
{/* Workflow Freigabe & Aktionen */}
@@ -551,34 +711,34 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
- - + + - - + + )} diff --git a/shop/components/invoice-pdf.tsx b/shop/components/invoice-pdf.tsx index c1e6a5a..2d5bddd 100644 --- a/shop/components/invoice-pdf.tsx +++ b/shop/components/invoice-pdf.tsx @@ -105,6 +105,8 @@ export const InvoicePDF = ({ const items = orderSnapshot?.items ?? []; const taxRate = orderSnapshot?.tax_rate ?? 19; const isSubscription = order?.type === 'subscription' || orderSnapshot?.billing_cycle === 'monthly'; + const formattedOrderNumber = (order.order_number || order.id || '').replace(/^BE-/, 'AE-'); + const isUpgrade = !!orderSnapshot?.last_license_date; let oneTimeNet = 0; let monthlyNet = 0; @@ -162,7 +164,7 @@ export const InvoicePDF = ({ - Anfragebestätigung: {order.order_number || order.id} + {isUpgrade ? 'Erweiterungsangebot' : 'Anfragebestätigung'}: #{formattedOrderNumber} @@ -177,7 +179,7 @@ export const InvoicePDF = ({ Bestelldetails - Bestellnummer: {order.order_number || order.id} + Bestellnummer: #{formattedOrderNumber} Zahlungsart: {isSubscription ? 'SEPA-Lastschrift' : 'Rechnung'} Datum: {new Date(order.created_at || Date.now()).toLocaleDateString('de-DE')} @@ -197,7 +199,7 @@ export const InvoicePDF = ({ - {deviceName === 'Zusatzleistung' ? 'Backoffice' : `Kasse: ${deviceName}`} + {deviceName === 'Zusatzleistung' ? 'Backoffice' : `${isUpgrade ? 'Erweiterung für Gerät' : 'Kasse'}: ${deviceName}`} {devItems.map((item: any, idx: number) => ( diff --git a/shop/lib/actions/email-templates.ts b/shop/lib/actions/email-templates.ts index cab0262..ad03779 100644 --- a/shop/lib/actions/email-templates.ts +++ b/shop/lib/actions/email-templates.ts @@ -99,31 +99,92 @@ export function getOrderEmailTemplate( return { text, html } } -export function getStatusEmailTemplate(orderNumber: string, oldLabel: string, newLabel: string) { - const text = `Hallo,\n\nder Status deiner Anfrage ${orderNumber} hat sich geändert.\n\nStatus: ${newLabel} (vorher: ${oldLabel})\n\nViele Grüße,\nDein CASPOS Team` +export function getStatusEmailTemplate( + orderNumber: string, + oldLabel: string, + newLabel: string, + statusKey?: string, + rejectionReason?: string +) { + const formattedOrderNumber = orderNumber.replace(/^BE-/, 'AE-') + + let title = 'Statusänderung' + let intro = `der Status deiner Anfrage #${formattedOrderNumber} wurde aktualisiert.` + let statusColor = '#f59e0b' // Amber default + let showAttachmentBadge = false + + if (statusKey === 'approved' || statusKey === 'active' || statusKey === 'completed') { + title = 'Freigabebestätigung' + intro = `Ihre Anfrage #${formattedOrderNumber} wurde erfolgreich freigegeben und ist nun aktiv. Die unterzeichnete Anfragebestätigung finden Sie im Anhang dieses Schreibens.` + statusColor = '#10b981' // Emerald + showAttachmentBadge = true + } else if (statusKey === 'rejected') { + title = 'Anfrage abgelehnt' + intro = `Ihre Anfrage #${formattedOrderNumber} wurde vom Support geprüft und abgelehnt.` + statusColor = '#ef4444' // Red + } else if (statusKey === 'pending_approval' || statusKey === 'in_review' || statusKey === 'pending') { + title = 'Eingangsbestätigung' + intro = `Ihre Anfrage #${formattedOrderNumber} ist eingegangen und wird derzeit von unserem Support geprüft.` + statusColor = '#f59e0b' + } + + const text = `Hallo,\n\n${title}\n\n${intro.replace(/<[^>]*>/g, '')}\n\nStatus: ${newLabel} (vorher: ${oldLabel})\n${rejectionReason ? `Grund: ${rejectionReason}\n` : ''}\nViele Grüße,\nDein CASPOS Team` + + const reasonSection = rejectionReason + ? ` +
+

Begründung:

+

"${rejectionReason}"

+
+ ` + : '' + + const footerActions = showAttachmentBadge + ? ` +

+ + 📎 PDF im Anhang verfügbar + +

+ ` + : '' const html = ` -
-

Statusänderung

-

Hallo,

-

der Status deiner Anfrage ${orderNumber} wurde aktualisiert.

-
- +
+
+

CASPOS

+

Die Kasse

+
+ +
+

${title}

+

Hallo,

+

${intro}

+ +
- - + + - - + + - - + +
Nummer:${orderNumber}Anfragenummer:#${formattedOrderNumber}
Vorher:${oldLabel}Vorher:${oldLabel}
Aktuell:${newLabel}Aktuell:${newLabel}
+ + ${reasonSection} +
+ + ${footerActions} + +
+

CASPOS Computerabrechnungssysteme GmbH · Alte Bundesstraße 16 · 76846 Hauenstein

+

Dies ist eine automatisch generierte Systembenachrichtigung.

-

Viele Grüße,
Dein CASPOS Team

` diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index 42d7433..c9e6466 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -468,7 +468,7 @@ export async function updateOrderStatus( const oldLabel = statusLabelMap[oldStatus] || oldStatus const newLabel = statusLabelMap[newStatus] || newStatus - const statusEmail = getStatusEmailTemplate(orderNumber, oldLabel, newLabel) + const statusEmail = getStatusEmailTemplate(orderNumber, oldLabel, newLabel, newStatus) const mailOptions: any = { to: user.email, @@ -642,7 +642,7 @@ export async function rejectOrder( const { data: { user: orderUser }, error: userError } = await admin.auth.admin.getUserById(order.user_id) if (!userError && orderUser && orderUser.email) { const orderNumber = order.order_number || order.id.slice(0, 8) - const statusEmail = getStatusEmailTemplate(orderNumber, 'Wartet auf Freigabe', `Abgelehnt (Grund: ${reason})`) + const statusEmail = getStatusEmailTemplate(orderNumber, 'Wartet auf Freigabe', 'Abgelehnt', 'rejected', reason) await sendMail({ to: orderUser.email, From 073dc99860eeb01744a9fe3f3d2bcd193f0fefc7 Mon Sep 17 00:00:00 2001 From: DanielS Date: Thu, 20 Aug 2026 17:44:30 +0200 Subject: [PATCH 3/3] chore(auth): add 2FA database logging - capture and log insert errors during 2FA code generation - print verification queries and lookup parameters to console --- shop/lib/actions/auth.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/shop/lib/actions/auth.ts b/shop/lib/actions/auth.ts index 49df9e4..a523ab0 100644 --- a/shop/lib/actions/auth.ts +++ b/shop/lib/actions/auth.ts @@ -185,9 +185,16 @@ async function send2FACodeInternal(userId: string, deviceHash: string, email: st .eq('device_hash', deviceHash) // Neuen Code speichern - await adminClient + const { data: insData, error: insError } = await adminClient .from('device_verification_codes') .insert({ user_id: userId, code, device_hash: deviceHash, expires_at: expiresAt }) + .select() + + if (insError) { + console.error("2FA CODE INSERT ERROR:", insError) + } else { + console.log("2FA CODE INSERTED:", code, insData) + } // Mail senden await sendMail({ @@ -222,6 +229,7 @@ export async function verifyDevice2FA(userId: string, code: string, deviceHash: const cleanCode = code.trim() // Code prüfen (unter Berücksichtigung von Leerzeichen & device_hash) + console.log("VERIFYING 2FA: userId =", userId, "code =", cleanCode) const { data: codeEntries, error: codeError } = await adminClient .from('device_verification_codes') .select('*') @@ -231,6 +239,7 @@ export async function verifyDevice2FA(userId: string, code: string, deviceHash: .limit(1) const codeEntry = codeEntries && codeEntries.length > 0 ? codeEntries[0] : null + console.log("VERIFY 2FA DB RESULT: entry =", codeEntry, "error =", codeError) if (codeError || !codeEntry) { // Doppelklick- & Race-Condition Schutz: Prüfen ob das Gerät eben bereits verifiziert wurde