diff --git a/shop/app/api/admin/recent-orders/route.ts b/shop/app/api/admin/recent-orders/route.ts index b4ddc30..3a37ab8 100644 --- a/shop/app/api/admin/recent-orders/route.ts +++ b/shop/app/api/admin/recent-orders/route.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server' /** * GET /api/admin/recent-orders - * Gibt die letzten 10 Bestellungen zurück. + * Gibt die letzten 10 Bestellungen und Umsatzdaten der letzten 6 Monate zurück. * Wird vom Admin-Dashboard alle 30 Sekunden gepollt. * Zugriff nur für authentifizierte User (Supabase Session). */ @@ -16,6 +16,7 @@ export async function GET() { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + // 1. Letzte 10 Bestellungen laden const { data: orders, error } = await supabase .from('orders') .select('id, order_number, created_at, total_price, status, customer_data') @@ -26,5 +27,47 @@ export async function GET() { return NextResponse.json({ error: error.message }, { status: 500 }) } - return NextResponse.json({ orders }) + // 2. Umsatzdaten der letzten 6 Monate für das Chart ermitteln + const sixMonthsAgo = new Date() + sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5) + sixMonthsAgo.setDate(1) + sixMonthsAgo.setHours(0, 0, 0, 0) + + const { data: chartOrders, error: chartError } = await supabase + .from('orders') + .select('total_price, created_at') + .neq('status', 'cancelled') + .gte('created_at', sixMonthsAgo.toISOString()) + + let chartData: { name: string; value: number }[] = [] + if (!chartError && chartOrders) { + const monthlyData: Record = {} + const now = new Date() + + // Die letzten 6 Monate initialisieren + for (let i = 5; i >= 0; i--) { + const d = new Date(now.getFullYear(), now.getMonth() - i, 1) + const key = d.toLocaleDateString('de-DE', { month: 'short', year: '2-digit' }) + monthlyData[key] = 0 + } + + // Werte aufsummieren + chartOrders.forEach(o => { + const date = new Date(o.created_at) + const key = date.toLocaleDateString('de-DE', { month: 'short', year: '2-digit' }) + if (key in monthlyData) { + monthlyData[key] += Number(o.total_price) + } + }) + + // In Array umwandeln + chartData = Object.entries(monthlyData).map(([name, value]) => ({ + name, + value: Math.round(value * 100) / 100 + })) + } else if (chartError) { + console.error('Chart data fetch error:', chartError) + } + + return NextResponse.json({ orders, chartData }) } diff --git a/shop/components/admin/recent-orders.tsx b/shop/components/admin/recent-orders.tsx index 5fb7be5..74ddabe 100644 --- a/shop/components/admin/recent-orders.tsx +++ b/shop/components/admin/recent-orders.tsx @@ -4,6 +4,7 @@ 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' +import { RevenueChart } from './revenue-chart' type RecentOrder = { id: string @@ -34,6 +35,7 @@ const POLL_INTERVAL_MS = 30_000 export function AdminRecentOrders() { const [orders, setOrders] = useState([]) + const [chartData, setChartData] = useState<{ name: string; value: number }[]>([]) const [newOrderIds, setNewOrderIds] = useState>(new Set()) const [lastChecked, setLastChecked] = useState(new Date()) const knownIds = useRef>(new Set()) @@ -43,7 +45,7 @@ export function AdminRecentOrders() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; const res = await fetch(`${baseUrl}/api/admin/recent-orders`, { cache: 'no-store' }); if (!res.ok) return - const { orders: fresh } = await res.json() + const { orders: fresh, chartData: freshChart } = await res.json() // Neue Bestellungen erkennen (Highlight) const freshIds = new Set(fresh.map((o: RecentOrder) => o.id)) @@ -54,6 +56,7 @@ export function AdminRecentOrders() { knownIds.current = freshIds setOrders(fresh) + setChartData(freshChart || []) setNewOrderIds(newOnes) setLastChecked(new Date()) @@ -76,12 +79,10 @@ export function AdminRecentOrders() {
- Übersicht + Übersicht (letzte 6 Monate) -
- [Umsatz-Chart – folgt in nächstem Update] -
+
diff --git a/shop/components/admin/revenue-chart.tsx b/shop/components/admin/revenue-chart.tsx new file mode 100644 index 0000000..a7f182c --- /dev/null +++ b/shop/components/admin/revenue-chart.tsx @@ -0,0 +1,168 @@ +'use client' + +import { useState } from 'react' + +type ChartDataPoint = { + name: string + value: number +} + +export function RevenueChart({ data }: { data: ChartDataPoint[] }) { + const [hoveredIdx, setHoveredIdx] = useState(null) + + if (!data || data.length === 0) { + return ( +
+ Keine Umsatzdaten vorhanden. +
+ ) + } + + const width = 500 + const height = 200 + const paddingLeft = 55 + const paddingRight = 15 + const paddingTop = 25 + const paddingBottom = 30 + + const chartWidth = width - paddingLeft - paddingRight + const chartHeight = height - paddingTop - paddingBottom + + const maxVal = Math.max(...data.map(d => d.value), 100) + const maxValue = Math.ceil(maxVal * 1.15) // 15% headroom + + const points = data.map((d, i) => { + const x = paddingLeft + (i / (data.length - 1)) * chartWidth + const y = paddingTop + chartHeight - (d.value / maxValue) * chartHeight + return { x, y, name: d.name, value: d.value } + }) + + const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ') + const areaPath = points.length > 0 + ? `${linePath} L ${points[points.length - 1].x} ${paddingTop + chartHeight} L ${points[0].x} ${paddingTop + chartHeight} Z` + : '' + + // 4 ticks for grid + const yTicks = [0, 0.33, 0.66, 1].map(ratio => Math.round(maxValue * ratio)) + + return ( +
+ + + + + + + + + {/* Grid Lines */} + {yTicks.map((val, idx) => { + const y = paddingTop + chartHeight - (val / maxValue) * chartHeight + return ( + + + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(val)} + + + ) + })} + + {/* X Axis Labels */} + {points.map((p, i) => ( + + {p.name} + + ))} + + {/* Area under the line */} + {areaPath && ( + + )} + + {/* Line */} + {linePath && ( + + )} + + {/* Interactive Dots & Hover areas */} + {points.map((p, i) => ( + + {/* Thick hover circle area */} + setHoveredIdx(i)} + onMouseLeave={() => setHoveredIdx(null)} + /> + {/* Visual Point dot */} + + {hoveredIdx === i && ( + + )} + + ))} + + + {/* HTML Tooltip overlay */} + {hoveredIdx !== null && ( +
+

{points[hoveredIdx].name}

+

+ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(points[hoveredIdx].value)} +

+
+ )} +
+ ) +} diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index bef39d4..ad9c382 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -484,7 +484,10 @@ export function OrderWizard({