diff --git a/shop/app/admin/orders/page.tsx b/shop/app/admin/orders/page.tsx index e54c755..c132b5f 100644 --- a/shop/app/admin/orders/page.tsx +++ b/shop/app/admin/orders/page.tsx @@ -7,8 +7,8 @@ export default async function AdminOrdersPage() {
-

Bestellungen

-

Verwalten Sie alle Kundenbestellungen und greifen Sie auf generierte Annfragesbestätigungen zu.

+

Anfragen & Bestellungen

+

Verwalten Sie alle Anfragen, prüfen Sie Details und greifen Sie auf generierte Anfragebestätigungen zu.

diff --git a/shop/components/admin/orders-table.tsx b/shop/components/admin/orders-table.tsx index 4ce3b8b..4702e5c 100644 --- a/shop/components/admin/orders-table.tsx +++ b/shop/components/admin/orders-table.tsx @@ -5,15 +5,42 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Download, ExternalLink, Search, Loader2 } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { + Download, + ExternalLink, + Search, + Loader2, + Eye, + ArrowUpDown, + Building2, + User, + Mail, + MapPin, + Package, + FileText, + Calendar, + Layers, + Sparkles, + XCircle, + CheckCircle, + Edit3 +} from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { updateOrderStatus } from "@/lib/actions/orders"; -import { resolveSupabaseUrl } from "@/lib/utils"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { updateOrderStatus, rejectOrder } from "@/lib/actions/orders"; interface OrdersTableProps { initialOrders: any[]; @@ -21,7 +48,10 @@ interface OrdersTableProps { const statusLabel: Record = { pending: 'Eingegangen', - active: 'In Bearbeitung', + pending_approval: 'Wartet auf Freigabe', + in_review: 'In Prüfung', + approved: 'Freigegeben', + active: 'Aktiviert', completed: 'Abgeschlossen', cancelled: 'Storniert', rejected: 'Abgelehnt', @@ -29,27 +59,51 @@ const statusLabel: Record = { const statusClass: Record = { pending: 'bg-amber-500/20 text-amber-400 border border-amber-500/30', + pending_approval: 'bg-amber-600/20 text-amber-300 border border-amber-500/40', + in_review: 'bg-purple-500/20 text-purple-400 border border-purple-500/30', + approved: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30', active: 'bg-blue-500/20 text-blue-400 border border-blue-500/30', completed: 'bg-green-500/20 text-green-400 border border-green-500/30', - cancelled: 'bg-red-500/20 text-red-400 border border-red-500/30', - rejected: 'bg-rose-500/20 text-rose-400 border border-rose-500/30', + cancelled: 'bg-slate-500/20 text-slate-400 border border-slate-500/30', + rejected: 'bg-red-500/20 text-red-400 border border-red-500/30', }; +type SortField = 'date_desc' | 'date_asc' | 'price_desc' | 'price_asc' | 'order_num'; + export function OrdersTable({ initialOrders }: OrdersTableProps) { const [orders, setOrders] = useState(initialOrders); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + const [sortBy, setSortBy] = useState("date_desc"); const [updatingId, setUpdatingId] = useState(null); + const [selectedOrder, setSelectedOrder] = useState(null); + + // Rejection Dialog State + const [rejectingOrder, setRejectingOrder] = useState(null); + const [rejectionReason, setRejectionReason] = useState(""); + const [isSubmittingReject, setIsSubmittingReject] = useState(false); useEffect(() => { setOrders(initialOrders); }, [initialOrders]); - const handleStatusChange = async (orderId: string, newStatus: 'pending' | 'active' | 'completed' | 'cancelled' | 'rejected') => { + const handleStatusChange = async (orderId: string, newStatus: 'pending' | 'pending_approval' | 'in_review' | 'approved' | 'active' | 'completed' | 'cancelled' | 'rejected') => { + if (newStatus === 'rejected') { + const target = orders.find(o => o.id === orderId); + if (target) { + setRejectingOrder(target); + setRejectionReason(""); + return; + } + } + setUpdatingId(orderId); try { await updateOrderStatus(orderId, newStatus); setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o)); + if (selectedOrder?.id === orderId) { + setSelectedOrder((prev: any) => prev ? { ...prev, status: newStatus } : null); + } } catch (error) { console.error(error); alert("Fehler beim Ändern des Status."); @@ -58,36 +112,98 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { } }; - const filteredOrders = orders.filter((order) => { - const orderNum = (order.order_number || "").toLowerCase(); - const company = (order.customer_data?.company_name || "").toLowerCase(); - const contact = `${order.customer_data?.first_name || ""} ${order.customer_data?.last_name || ""}`.toLowerCase(); - - const matchesSearch = - orderNum.includes(search.toLowerCase()) || - company.includes(search.toLowerCase()) || - contact.includes(search.toLowerCase()); + const handleConfirmReject = async () => { + if (!rejectingOrder) return; + setIsSubmittingReject(true); + try { + await rejectOrder(rejectingOrder.id, rejectionReason || "Vom Administrator abgelehnt."); + setOrders(prev => prev.map(o => o.id === rejectingOrder.id ? { ...o, status: 'rejected' } : o)); + if (selectedOrder?.id === rejectingOrder.id) { + setSelectedOrder((prev: any) => prev ? { ...prev, status: 'rejected' } : null); + } + setRejectingOrder(null); + setRejectionReason(""); + } catch (error) { + console.error(error); + alert("Fehler beim Ablehnen der Anfrage."); + } finally { + setIsSubmittingReject(false); + } + }; - const matchesStatus = statusFilter === "all" || order.status === statusFilter; + const filteredAndSortedOrders = orders + .filter((order) => { + const orderNum = (order.order_number || "").toLowerCase(); + const company = (order.customer_data?.company_name || "").toLowerCase(); + const contact = `${order.customer_data?.first_name || ""} ${order.customer_data?.last_name || ""}`.toLowerCase(); + + const matchesSearch = + orderNum.includes(search.toLowerCase()) || + company.includes(search.toLowerCase()) || + contact.includes(search.toLowerCase()); - return matchesSearch && matchesStatus; - }); + const matchesStatus = statusFilter === "all" || order.status === statusFilter; + + return matchesSearch && matchesStatus; + }) + .sort((a, b) => { + if (sortBy === 'date_desc') return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + if (sortBy === 'date_asc') return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); + if (sortBy === 'price_desc') return (b.total_price || 0) - (a.total_price || 0); + if (sortBy === 'price_asc') return (a.total_price || 0) - (b.total_price || 0); + if (sortBy === 'order_num') return (a.order_number || a.id).localeCompare(b.order_number || b.id); + return 0; + }); return (
- {/* Filterleiste */} -
-
- - setSearch(e.target.value)} - className="pl-10 bg-slate-950/40 border-white/10 text-white placeholder:text-slate-500" - /> + {/* Filter- & Sortierleiste */} +
+
+
+ + setSearch(e.target.value)} + className="pl-10 bg-slate-950/40 border-white/10 text-white placeholder:text-slate-500" + /> +
+ + + + + + + setSortBy('date_desc')} className="text-xs cursor-pointer"> + Datum (Neueste zuerst) + + setSortBy('date_asc')} className="text-xs cursor-pointer"> + Datum (Älteste zuerst) + + setSortBy('price_desc')} className="text-xs cursor-pointer"> + Betrag (Höchster zuerst) + + setSortBy('price_asc')} className="text-xs cursor-pointer"> + Betrag (Niedrigster zuerst) + + setSortBy('order_num')} className="text-xs cursor-pointer"> + Anfragenr. + + +
-
+
+ + + - - @@ -144,31 +268,31 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) { - Bestellnr. - Datum - Kunde - Auswahl - Betrag - Status - Aktionen + Anfragenr. + Datum + Kunde / Firma + Auswahl / Produkte + Gesamtbetrag + Status + Aktionen - {filteredOrders.length === 0 ? ( + {filteredAndSortedOrders.length === 0 ? ( - Keine Bestellungen gefunden. + Keine Anfragen gefunden. ) : ( - filteredOrders.map((order) => { + filteredAndSortedOrders.map((order) => { const items = order.order_data?.items || []; return ( - - + + #{order.order_number || order.id.slice(0, 8)} - + {new Date(order.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} @@ -177,38 +301,43 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
{order.customer_data?.company_name || 'Privatkunde'}
-
+
+ {order.customer_data?.first_name} {order.customer_data?.last_name}
- +
- {items.map((item: any, idx: number) => ( + {items.slice(0, 2).map((item: any, idx: number) => (
- - {item.device_name || 'Kasse'}: + + {item.device_name || 'Kasse'}: {item.product_name} - {item.product_name} {item.selected_modules?.length > 0 && ( - + + {item.selected_modules.map((m: any) => m.module_name || m.name).join(', ')} )}
))} + {items.length > 2 && ( + + + {items.length - 2} weitere Kassen + + )}
- - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price || 0)} - + {Object.entries(statusLabel).map(([key, label]) => ( @@ -257,6 +389,248 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
+ + {/* Detail-Modal für vollständige Anfragenübersicht & Freigabe-Workflow */} + {selectedOrder && ( + !open && setSelectedOrder(null)}> + + +
+
+
+ + + Anfragenr. #{selectedOrder.order_number || selectedOrder.id.slice(0, 8)} + + + {statusLabel[selectedOrder.status] ?? selectedOrder.status} + +
+ + + Erstellt am {new Date(selectedOrder.created_at).toLocaleString('de-DE')} + +
+
+
Gesamtsumme
+
+ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(selectedOrder.total_price || 0)} +
+
+
+
+ + {/* Kunden & Stammdaten */} +
+
+
+ + Kunden- & Firmendaten +
+
+ {selectedOrder.customer_data?.company_name || 'Keine Firma angegeben'} +
+
+ + {selectedOrder.customer_data?.first_name} {selectedOrder.customer_data?.last_name} +
+ {selectedOrder.customer_data?.email && ( +
+ + {selectedOrder.customer_data.email} +
+ )} +
+ +
+
+ + Adresse & Standort +
+
+
{selectedOrder.customer_data?.street || 'Keine Straße'}
+
+ {selectedOrder.customer_data?.zip} {selectedOrder.customer_data?.city} +
+
+
+
+ + {/* 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} +
+ {item.price && ( + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.price)} + + )} +
+ + {/* 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)} + + )} +
+ ))} +
+
+ )} +
+ ))} +
+
+ + {/* Workflow Freigabe & Aktionen */} +
+
+ + Freigabe-Workflow & Verwalter-Aktionen +
+
+ + + + +
+
+ + + + + + +
+
+ )} + + {/* Dialog für Ablehnungsbegründung */} + {rejectingOrder && ( + !open && setRejectingOrder(null)}> + + + + + Anfrage Ablehnen + + + Bitte geben Sie einen Grund für die Ablehnung der Anfrage #{rejectingOrder.order_number || rejectingOrder.id.slice(0, 8)} an. + + + +
+ + setRejectionReason(e.target.value)} + className="bg-slate-900 border-white/10 text-white text-xs" + /> +
+ + + + + +
+
+ )}
); }