diff --git a/shop/components/admin/orders-table.tsx b/shop/components/admin/orders-table.tsx index 4702e5c..a9379e5 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) { )} - {/* 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,