Files
webshop/shop/components/admin/recent-orders.tsx
DanielS bffaca1d63
All checks were successful
Staging Build / build (push) Successful in 2m49s
feat(orders): add rejected status to order schema and admin table views
2026-07-08 10:13:21 +02:00

138 lines
5.2 KiB
TypeScript

'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'
import { RevenueChart } from './revenue-chart'
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<string, string> = {
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',
rejected: 'bg-rose-500/20 text-rose-400 border-rose-500/30',
}
const statusLabel: Record<string, string> = {
pending: 'Eingegangen',
active: 'In Bearbeitung',
cancelled: 'Storniert',
rejected: 'Abgelehnt',
}
const POLL_INTERVAL_MS = 30_000
export function AdminRecentOrders() {
const [orders, setOrders] = useState<RecentOrder[]>([])
const [chartData, setChartData] = useState<{ name: string; value: number }[]>([])
const [newOrderIds, setNewOrderIds] = useState<Set<string>>(new Set())
const [lastChecked, setLastChecked] = useState<Date>(new Date())
const knownIds = useRef<Set<string>>(new Set())
const fetchOrders = async () => {
try {
const res = await fetch('/api/admin/recent-orders', { cache: 'no-store' });
if (!res.ok) return
const { orders: fresh, chartData: freshChart } = await res.json()
// Neue Bestellungen erkennen (Highlight)
const freshIds = new Set<string>(fresh.map((o: RecentOrder) => o.id))
const newOnes = new Set<string>()
freshIds.forEach(id => {
if (!knownIds.current.has(id)) newOnes.add(id)
})
knownIds.current = freshIds
setOrders(fresh)
setChartData(freshChart || [])
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 (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7 bg-[#020617]/85 p-4 rounded-xl">
<div className="rounded-xl border border-white/5 bg-[#020617]/85 overflow-hidden backdrop-blur-xl col-span-4">
<CardHeader>
<CardTitle className="text-white">Übersicht (letzte 6 Monate)</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<RevenueChart data={chartData} />
</CardContent>
</div>
<div className="flex flex-col gap-4 bg-[#020617]/85 p-4 rounded-xl border border-white/5 col-span-3">
<div className="flex flex-row items-center justify-between pb-3">
<h3 className="font-semibold text-lg text-white">Neue Bestellungen</h3>
<span className="text-[10px] text-slate-500">
Aktualisiert: {lastChecked.toLocaleTimeString('de-DE')}
</span>
</div>
<div className="space-y-3 max-h-96 overflow-y-auto">
{orders.length === 0 ? (
<p className="text-slate-500 text-sm italic text-center py-8">Noch keine Bestellungen</p>
) : (
orders.slice(0, 3).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 (
<Link key={order.id} href="/admin/orders" className="block">
<div
className={`flex items-center gap-3 p-3 rounded-lg transition-all duration-500 ${isNew
? 'bg-primary/10 border border-primary/30 shadow-[0_0_12px_rgba(59,130,246,0.2)]'
: 'bg-[#020617]/20 hover:bg-[#020617]/30 border border-white/5'
}`}
>
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs font-bold shrink-0">
{customerName.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">{customerName}</p>
<p className="text-xs text-slate-500 font-mono">#{order.order_number}</p>
</div>
<div className="text-right shrink-0 space-y-1">
<p className="font-medium text-white text-sm">
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
</p>
<Badge className={`text-[10px] ${statusClass[order.status] ?? ''}`}>{statusLabel[order.status] ?? order.status}</Badge>
</div>
</div>
</Link>
)
})
)}
</div>
</div>
</div>
)
}