diff --git a/shop/app/admin/orders/page.tsx b/shop/app/admin/orders/page.tsx index f4c02c8..b921661 100644 --- a/shop/app/admin/orders/page.tsx +++ b/shop/app/admin/orders/page.tsx @@ -43,7 +43,7 @@ async function OrdersData() { - ID + Bestellnr. Datum Kunde Betrag @@ -61,8 +61,8 @@ async function OrdersData() { ) : ( orders?.map((order) => ( - - {order.id.slice(0, 8)}... + + {order.order_number ?? order.id.slice(0, 8) + '...'} {new Date(order.created_at).toLocaleDateString('de-DE')} diff --git a/shop/app/admin/page.tsx b/shop/app/admin/page.tsx index 71c3e93..21c9380 100644 --- a/shop/app/admin/page.tsx +++ b/shop/app/admin/page.tsx @@ -1,11 +1,36 @@ +import { createClient } from '@/lib/supabase/server' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Package, ShoppingCart, Users, TrendingUp } from 'lucide-react' +import { AdminRecentOrders } from '@/components/admin/recent-orders' + +// ─── KPI-Daten aus DB ──────────────────────────────────────────────────────── +async function getKpis() { + const supabase = await createClient() + + const [ + { data: orders }, + { count: userCount }, + { count: productCount }, + ] = await Promise.all([ + supabase.from('orders').select('total_price, status'), + supabase.from('profiles').select('*', { count: 'exact', head: true }), + supabase.from('products').select('*', { count: 'exact', head: true }), + ]) + + const totalRevenue = (orders ?? []).reduce((sum, o) => sum + Number(o.total_price), 0) + const orderCount = (orders ?? []).length + + return { totalRevenue, orderCount, userCount: userCount ?? 0, productCount: productCount ?? 0 } +} + +export default async function AdminDashboard() { + const { totalRevenue, orderCount, userCount, productCount } = await getKpis() -export default function AdminDashboard() { return (

Dashboard

- + + {/* KPI Cards – echte DB-Daten */}
@@ -13,75 +38,49 @@ export default function AdminDashboard() { -
€12,234.56
-

+20.1% zum Vormonat

+
+ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalRevenue)} +
+

Aus allen Bestellungen

+ Bestellungen -
+573
-

+12% zur Vorwoche

+
{orderCount}
+

Gesamt

+ Produkte -
12
-

+2 neue Module

+
{productCount}
+

Im Katalog

+ Kunden -
2,450
-

+180 heute

-
-
-
- -
- - - Übersicht - - -
- [Umsatz-Chart wird geladen...] -
-
-
- - - Letzte Bestellungen - - -
- {[1, 2, 3].map(i => ( -
-
- JD -
-
-

John Doe

-

john.doe@example.com

-
-
+€450.00
-
- ))} -
+
{userCount}
+

Registrierte User

+ + {/* Letzte Bestellungen – Client Component mit 30s-Polling */} +
) } diff --git a/shop/app/api/admin/recent-orders/route.ts b/shop/app/api/admin/recent-orders/route.ts new file mode 100644 index 0000000..b4ddc30 --- /dev/null +++ b/shop/app/api/admin/recent-orders/route.ts @@ -0,0 +1,30 @@ +import { createClient } from '@/lib/supabase/server' +import { NextResponse } from 'next/server' + +/** + * GET /api/admin/recent-orders + * Gibt die letzten 10 Bestellungen zurück. + * Wird vom Admin-Dashboard alle 30 Sekunden gepollt. + * Zugriff nur für authentifizierte User (Supabase Session). + */ +export async function GET() { + const supabase = await createClient() + + // Session prüfen + const { data: { user }, error: authError } = await supabase.auth.getUser() + if (authError || !user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { data: orders, error } = await supabase + .from('orders') + .select('id, order_number, created_at, total_price, status, customer_data') + .order('created_at', { ascending: false }) + .limit(10) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json({ orders }) +} diff --git a/shop/app/my-orders/page.tsx b/shop/app/my-orders/page.tsx new file mode 100644 index 0000000..17a54c3 --- /dev/null +++ b/shop/app/my-orders/page.tsx @@ -0,0 +1,158 @@ +import { createClient } from '@/lib/supabase/server' +import { redirect } from 'next/navigation' +import Link from 'next/link' +import { Download, ExternalLink, ShoppingBag, ArrowLeft, Package } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import type { Order } from '@/lib/types' + +const statusLabel: Record = { + pending: 'Eingegangen', + active: 'In Bearbeitung', + cancelled: 'Storniert', +} + +const statusClass: Record = { + pending: 'bg-amber-500/20 text-amber-400 border-amber-500/30', + active: 'bg-green-500/20 text-green-400 border-green-500/30', + cancelled: 'bg-red-500/20 text-red-400 border-red-500/30', +} + +export default async function MyOrdersPage() { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + if (!user) redirect('/auth/login') + + const { data: orders, error } = await supabase + .from('orders') + .select('*') + .eq('user_id', user.id) + .order('created_at', { ascending: false }) + + return ( +
+
+ {/* Header */} +
+ + + +
+

+ + Meine Bestellungen +

+

+ Alle Ihre Bestellungen auf einen Blick – inkl. aktuellem Status. +

+
+
+ + {/* Fehler */} + {error && ( +
+ Fehler beim Laden der Bestellungen: {error.message} +
+ )} + + {/* Keine Bestellungen */} + {!error && (!orders || orders.length === 0) && ( + + + +

Sie haben noch keine Bestellungen aufgegeben.

+ + + +
+
+ )} + + {/* Bestellliste */} +
+ {(orders ?? []).map((order) => { + const o = order as Order + const items = o.order_data?.items ?? [] + return ( + + + {/* Kopfzeile */} +
+
+

Bestellnummer

+

#{o.order_number}

+
+
+

Datum

+

+ {new Date(o.created_at).toLocaleDateString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + })} +

+
+
+

Status

+ + {statusLabel[o.status] ?? o.status} + +
+
+

Gesamt

+

+ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(o.total_price)} +

+
+
+ + {/* Produkte kurz */} + {items.length > 0 && ( +
+ {items.map((item, i) => ( + + {item.product_name} + {item.selected_modules.length > 0 && ( + +{item.selected_modules.length} Module + )} + + ))} +
+ )} + + {/* Aktionen */} +
+ + + + {o.pdf_url && ( + <> + + + + )} +
+
+
+ ) + })} +
+
+
+ ) +} diff --git a/shop/app/order/success/page.tsx b/shop/app/order/success/page.tsx new file mode 100644 index 0000000..a75e126 --- /dev/null +++ b/shop/app/order/success/page.tsx @@ -0,0 +1,157 @@ +import { createClient } from '@/lib/supabase/server' +import { redirect } from 'next/navigation' +import Link from 'next/link' +import { CheckCircle2, Download, ExternalLink, ShoppingBag, ArrowRight } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Badge } from '@/components/ui/badge' +import type { Order } from '@/lib/types' + +export default async function OrderSuccessPage({ + searchParams, +}: { + searchParams: Promise<{ id?: string }> +}) { + const { id } = await searchParams + if (!id) redirect('/') + + const supabase = await createClient() + + // Authentifizierung prüfen + const { data: { user } } = await supabase.auth.getUser() + if (!user) redirect('/auth/login') + + // Bestellung laden und prüfen ob sie dem eingeloggten User gehört + const { data: order, error } = await supabase + .from('orders') + .select('*') + .eq('id', id) + .eq('user_id', user.id) + .single() + + if (error || !order) redirect('/') + + const o = order as Order + const items = o.order_data?.items ?? [] + + const statusLabel: Record = { + pending: 'Eingegangen', + active: 'In Bearbeitung', + cancelled: 'Storniert', + } + + return ( +
+
+ {/* Erfolgsmeldung */} +
+
+ +
+

+ Bestellung eingegangen! +

+

+ Ihre Bestellung wurde erfolgreich gespeichert. +

+
+ #{o.order_number} +
+
+ + {/* Bestell-Details */} + + + Bestellübersicht + + {statusLabel[o.status] ?? o.status} + + + + {/* Produkte */} +
+ {items.map((item, i) => ( +
+
+ {item.product_name} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.base_price)} + + {item.billing_interval === 'one_time' ? 'einmalig' : '/ Monat'} + + +
+ {item.selected_modules.map((mod, j) => ( +
+ + {mod.module_name} + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price)} +
+ ))} +
+ ))} +
+ + + + {/* Gesamtbetrag */} +
+ Gesamtbetrag + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(o.total_price)} + +
+ + + + {/* Kundendaten */} +
+

{o.customer_data?.company_name}

+

{o.customer_data?.first_name} {o.customer_data?.last_name}

+

{o.customer_data?.address}, {o.customer_data?.zip_code} {o.customer_data?.city}

+
+ + {/* PDF-Download */} + {o.pdf_url && ( + + )} +
+
+ + {/* Aktions-Buttons */} +
+ + + + + + +
+
+
+ ) +} diff --git a/shop/app/page.tsx b/shop/app/page.tsx index 9ed974a..fc3288b 100644 --- a/shop/app/page.tsx +++ b/shop/app/page.tsx @@ -22,6 +22,9 @@ export default function Home() { Bestellen + + Meine Bestellungen + Admin diff --git a/shop/components/admin/recent-orders.tsx b/shop/components/admin/recent-orders.tsx new file mode 100644 index 0000000..290aa06 --- /dev/null +++ b/shop/components/admin/recent-orders.tsx @@ -0,0 +1,140 @@ +'use client' + +import { useEffect, useState, useRef } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import Link from 'next/link' + +type RecentOrder = { + id: string + order_number: string + created_at: string + total_price: number + status: string + customer_data?: { + company_name?: string + first_name?: string + last_name?: string + } +} + +const statusClass: Record = { + pending: 'bg-amber-500/20 text-amber-400 border-amber-500/30', + active: 'bg-green-500/20 text-green-400 border-green-500/30', + cancelled: 'bg-red-500/20 text-red-400 border-red-500/30', +} + +const statusLabel: Record = { + pending: 'Eingegangen', + active: 'In Bearbeitung', + cancelled: 'Storniert', +} + +const POLL_INTERVAL_MS = 30_000 + +export function AdminRecentOrders() { + const [orders, setOrders] = useState([]) + const [newOrderIds, setNewOrderIds] = useState>(new Set()) + const [lastChecked, setLastChecked] = useState(new Date()) + const knownIds = useRef>(new Set()) + + const fetchOrders = async () => { + try { + const res = await fetch('/api/admin/recent-orders', { cache: 'no-store' }) + if (!res.ok) return + const { orders: fresh } = await res.json() + + // Neue Bestellungen erkennen (Highlight) + const freshIds = new Set(fresh.map((o: RecentOrder) => o.id)) + const newOnes = new Set() + freshIds.forEach(id => { + if (!knownIds.current.has(id)) newOnes.add(id) + }) + knownIds.current = freshIds + + setOrders(fresh) + setNewOrderIds(newOnes) + setLastChecked(new Date()) + + // Highlight nach 5 Sekunden entfernen + if (newOnes.size > 0) { + setTimeout(() => setNewOrderIds(new Set()), 5000) + } + } catch (e) { + console.error('Polling error:', e) + } + } + + useEffect(() => { + fetchOrders() + const interval = setInterval(fetchOrders, POLL_INTERVAL_MS) + return () => clearInterval(interval) + }, []) + + return ( +
+ + + Übersicht + + +
+ [Umsatz-Chart – folgt in nächstem Update] +
+
+
+ + + + Neue Bestellungen + + Aktualisiert: {lastChecked.toLocaleTimeString('de-DE')} + + + + {orders.length === 0 ? ( +

Noch keine Bestellungen

+ ) : ( +
+ {orders.map(order => { + const isNew = newOrderIds.has(order.id) + const customerName = + order.customer_data?.company_name || + `${order.customer_data?.first_name ?? ''} ${order.customer_data?.last_name ?? ''}`.trim() || + 'Unbekannt' + + return ( + +
+
+ {customerName.slice(0, 2).toUpperCase()} +
+
+

{customerName}

+

#{order.order_number}

+
+
+

+ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)} +

+ + {statusLabel[order.status] ?? order.status} + +
+
+ + ) + })} +
+ )} +
+
+
+ ) +} diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 5e38173..808fac6 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useMemo } from 'react' +import { useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'framer-motion' import { Product, ProductModule, Profile } from '@/lib/types' import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' @@ -10,7 +11,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { Check, ChevronRight, ChevronLeft, ShoppingCart, User, ShieldCheck, Cloud, Utensils, HardDrive, Package, AlertCircle } from 'lucide-react' +import { Check, ChevronRight, ChevronLeft, ShoppingCart, User, ShieldCheck, Cloud, Utensils, HardDrive, Package, AlertCircle, Loader2 } from 'lucide-react' import { submitOrder } from '@/lib/actions/orders' import { Category } from '@/lib/types' import { Badge } from '@/components/ui/badge' @@ -76,7 +77,9 @@ export function OrderWizard({ categories: Category[] initialProfile: Profile | null }) { + const router = useRouter() const [step, setStep] = useState(1) + const [isSubmitting, setIsSubmitting] = useState(false) const [customerData, setCustomerData] = useState>( initialProfile || { company_name: '', @@ -145,18 +148,20 @@ export function OrderWizard({ const prevStep = () => setStep(s => s - 1) const handleSubmit = async () => { + if (isSubmitting) return // Doppelklick-Guard + setIsSubmitting(true) try { - await submitOrder({ + const order = await submitOrder({ selections, products, categories, customerProfile: customerData, }) - alert('Bestellung erfolgreich aufgegeben!') - window.location.href = '/' + router.push(`/order/success?id=${order.id}`) } catch (error) { console.error(error) - alert('Fehler bei der Bestellung.') + alert('Fehler bei der Bestellung. Bitte versuchen Sie es erneut.') + setIsSubmitting(false) // Nur bei Fehler wieder entsperren } } @@ -552,8 +557,13 @@ export function OrderWizard({