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/admin/orders-table.tsx b/shop/components/admin/orders-table.tsx index f294763..f9590ab 100644 --- a/shop/components/admin/orders-table.tsx +++ b/shop/components/admin/orders-table.tsx @@ -40,7 +40,7 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; -import { updateOrderStatus, rejectOrder } from "@/lib/actions/orders"; +import { updateOrderStatus, rejectOrder, updateOrderPayload } from "@/lib/actions/orders"; interface OrdersTableProps { initialOrders: any[]; @@ -83,6 +83,102 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { const [rejectionReason, setRejectionReason] = useState(""); const [isSubmittingReject, setIsSubmittingReject] = useState(false); + // Preiskorrektur States + const [isEditingSnapshot, setIsEditingSnapshot] = useState(false); + const [editedSnapshot, setEditedSnapshot] = useState(null); + const [isSavingSnapshot, setIsSavingSnapshot] = useState(false); + + // Initialize editedSnapshot when selectedOrder changes + useEffect(() => { + if (selectedOrder) { + setEditedSnapshot(JSON.parse(JSON.stringify(selectedOrder.order_data || {}))); + setIsEditingSnapshot(false); + } else { + setEditedSnapshot(null); + setIsEditingSnapshot(false); + } + }, [selectedOrder]); + + const updateItemBasePrice = (itemIdx: number, newPrice: number) => { + if (!editedSnapshot) return; + const updatedItems = [...editedSnapshot.items]; + const item = { ...updatedItems[itemIdx] }; + item.base_price = newPrice; + + const moduleTotal = (item.selected_modules || []).reduce((acc: number, m: any) => acc + (m.total_price ?? (m.price * (m.quantity || 1))), 0); + item.item_total = newPrice + moduleTotal; + + updatedItems[itemIdx] = item; + recalculateSnapshot(updatedItems); + }; + + const updateModulePriceOrQty = (itemIdx: number, modIdx: number, field: 'price' | 'quantity', value: number) => { + if (!editedSnapshot) return; + const updatedItems = [...editedSnapshot.items]; + const item = { ...updatedItems[itemIdx] }; + const updatedModules = [...(item.selected_modules || [])]; + const mod = { ...updatedModules[modIdx] }; + + if (field === 'price') { + mod.price = value; + } else { + mod.quantity = value; + } + mod.total_price = mod.price * (mod.quantity || 1); + updatedModules[modIdx] = mod; + + item.selected_modules = updatedModules; + const moduleTotal = updatedModules.reduce((acc: number, m: any) => acc + (m.total_price ?? (m.price * (m.quantity || 1))), 0); + item.item_total = (item.base_price || 0) + moduleTotal; + + updatedItems[itemIdx] = item; + recalculateSnapshot(updatedItems); + }; + + const recalculateSnapshot = (updatedItems: any[]) => { + if (!editedSnapshot) return; + const taxRate = editedSnapshot.tax_rate ?? 19; + const subtotal = updatedItems.reduce((acc: number, item: any) => acc + (item.item_total || 0), 0); + const taxAmount = Math.round(subtotal * (taxRate / 100) * 100) / 100; + const total = Math.round((subtotal + taxAmount) * 100) / 100; + + setEditedSnapshot({ + ...editedSnapshot, + items: updatedItems, + subtotal, + tax_amount: taxAmount, + total + }); + }; + + const handleSaveSnapshot = async () => { + if (!editedSnapshot || !selectedOrder) return; + setIsSavingSnapshot(true); + try { + const updated = await updateOrderPayload(selectedOrder.id, { + orderSnapshot: editedSnapshot + }); + setOrders(prev => prev.map(o => o.id === selectedOrder.id ? { ...o, order_data: editedSnapshot, total_price: editedSnapshot.total } : o)); + setSelectedOrder({ ...selectedOrder, order_data: editedSnapshot, total_price: editedSnapshot.total }); + setIsEditingSnapshot(false); + } catch (error: any) { + console.error(error); + alert("Fehler beim Speichern der Änderungen: " + error.message); + } finally { + setIsSavingSnapshot(false); + } + }; + + // Badge-Counter berechnen + const countAll = orders.length; + const countOffen = orders.filter(o => ["pending", "pending_approval", "in_review"].includes(o.status)).length; + const countApproved = orders.filter(o => o.status === "approved").length; + const countActive = orders.filter(o => ["active", "completed"].includes(o.status)).length; + const countRejected = orders.filter(o => ["rejected", "cancelled"].includes(o.status)).length; + + const displaySnapshot = editedSnapshot || selectedOrder?.order_data || {}; + const displayItems = displaySnapshot.items || []; + useEffect(() => { setOrders(initialOrders); }, [initialOrders]); @@ -142,7 +238,12 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { company.includes(search.toLowerCase()) || contact.includes(search.toLowerCase()); - const matchesStatus = statusFilter === "all" || order.status === statusFilter; + let matchesStatus = false; + if (statusFilter === "all") matchesStatus = true; + else if (statusFilter === "offen") matchesStatus = ["pending", "pending_approval", "in_review"].includes(order.status); + else if (statusFilter === "approved") matchesStatus = order.status === "approved"; + else if (statusFilter === "active") matchesStatus = ["active", "completed"].includes(order.status); + else if (statusFilter === "rejected") matchesStatus = ["rejected", "cancelled"].includes(order.status); return matchesSearch && matchesStatus; }) @@ -208,57 +309,41 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { variant={statusFilter === "all" ? "default" : "outline"} size="sm" onClick={() => setStatusFilter("all")} - className="text-xs" + className="text-xs gap-1.5" > - Alle + Alle {countAll} - -
@@ -372,7 +457,7 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { + )} + {isEditingSnapshot && ( +
+ Sie befinden sich im Preiskorrektur-Modus. +
+ + +
+
+ )} +
- {(selectedOrder.order_data?.items || []).map((item: any, idx: number) => { + {displayItems.map((item: any, idx: number) => { const devName = item.device_name || `Kasse #${idx + 1}` const licNum = item.license_number const modules = item.selected_modules || [] @@ -492,10 +621,23 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { {/* Inhalt der Kasse: Hauptprodukt + Preis */}
{item.product_name} - {(item.price || item.base_price) && ( - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.price || item.base_price)} - + {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 font-mono" + /> +
+ ) : ( + (item.price || item.base_price) && ( + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.price || item.base_price)} + + ) )}
@@ -508,14 +650,34 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
{modules.map((mod: any, mIdx: number) => ( -
+
- + {mod.module_name || mod.name} {mod.quantity > 1 ? `(${mod.quantity}x)` : ''} + + {mod.module_name || mod.name} {!isEditingSnapshot && mod.quantity > 1 ? `(${mod.quantity}x)` : ''} - {mod.price && ( - - +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price)} - + {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 font-mono" + /> + 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 font-mono" + /> +
+ ) : ( + mod.price && ( + + +{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * (mod.quantity || 1))} + + ) )}
))} @@ -571,34 +733,34 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
- - + + -
- - - -
-
+
+ + + +
+
)} 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/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/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 8e43404..ff5f1d8 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -1097,6 +1097,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 0c7be32..e91ea75 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 29b7acc..f9dd17f 100644 --- a/shop/components/wizard/summary-sidebar.tsx +++ b/shop/components/wizard/summary-sidebar.tsx @@ -294,7 +294,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 + )}