From 58fcf4a7225e0b6bee840cf1399ffb53b383499d Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Sun, 19 Jul 2026 19:33:08 +0200 Subject: [PATCH] feat: add dashboard page with stats, quick actions, recent vehicles, update navigation and login redirect --- frontend/app/(auth)/login/page.tsx | 2 +- frontend/app/[locale]/dashboard/page.tsx | 5 + frontend/app/[locale]/page.tsx | 5 + frontend/components/Dashboard.tsx | 249 +++++++++++++++++++++++ frontend/components/Navigation.tsx | 9 + frontend/messages/de.json | 18 ++ frontend/messages/en.json | 18 ++ 7 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 frontend/app/[locale]/dashboard/page.tsx create mode 100644 frontend/app/[locale]/page.tsx create mode 100644 frontend/components/Dashboard.tsx diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx index a177528..fa5b3f7 100644 --- a/frontend/app/(auth)/login/page.tsx +++ b/frontend/app/(auth)/login/page.tsx @@ -41,7 +41,7 @@ function LoginForm() { const tokens = await apiLogin(email, password); setTokens(tokens); showToast(t('login.success'), 'success'); - router.push('/de/fahrzeuge'); + router.push('/de/dashboard'); } catch (err) { const message = (err as any)?.error?.message || t('login.error.invalidCredentials'); showToast(message, 'error'); diff --git a/frontend/app/[locale]/dashboard/page.tsx b/frontend/app/[locale]/dashboard/page.tsx new file mode 100644 index 0000000..bb93abc --- /dev/null +++ b/frontend/app/[locale]/dashboard/page.tsx @@ -0,0 +1,5 @@ +import { Dashboard } from '@/components/Dashboard'; + +export default function DashboardPage() { + return ; +} diff --git a/frontend/app/[locale]/page.tsx b/frontend/app/[locale]/page.tsx new file mode 100644 index 0000000..209dd96 --- /dev/null +++ b/frontend/app/[locale]/page.tsx @@ -0,0 +1,5 @@ +import { Dashboard } from '@/components/Dashboard'; + +export default function LocaleHomePage() { + return ; +} diff --git a/frontend/components/Dashboard.tsx b/frontend/components/Dashboard.tsx new file mode 100644 index 0000000..53f4404 --- /dev/null +++ b/frontend/components/Dashboard.tsx @@ -0,0 +1,249 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { Card } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Table } from '@/components/ui/Table'; +import { useI18n } from '@/lib/i18n'; +import { listVehicles, type VehicleResponse } from '@/lib/vehicles'; + +interface DashboardStats { + total: number; + available: number; + reserved: number; + sold: number; + totalValue: number; +} + +interface StatCardProps { + icon: React.ReactNode; + label: string; + value: string; + testId: string; +} + +function StatCard({ icon, label, value, testId }: StatCardProps) { + return ( +
+
+ {icon} + {label} +
+ {value} +
+ ); +} + +const CarIcon = ( + + + + +); + +const CheckCircleIcon = ( + + + +); + +const ClockIcon = ( + + + +); + +const TagIcon = ( + + + +); + +const EuroIcon = ( + + + +); + +export function Dashboard() { + const router = useRouter(); + const { t } = useI18n(); + const [recentVehicles, setRecentVehicles] = useState([]); + const [stats, setStats] = useState({ + total: 0, + available: 0, + reserved: 0, + sold: 0, + totalValue: 0, + }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchDashboardData = useCallback(async () => { + setLoading(true); + setError(null); + try { + const [recentResponse, allResponse] = await Promise.all([ + listVehicles({ page: 1, page_size: 5, sort: '-created_at' }), + listVehicles({ page: 1, page_size: 1000 }), + ]); + + setRecentVehicles(recentResponse.items); + + const allVehicles = allResponse.items; + const computedStats: DashboardStats = { + total: allResponse.total, + available: allVehicles.filter(v => v.availability === 'available').length, + reserved: allVehicles.filter(v => v.availability === 'reserved').length, + sold: allVehicles.filter(v => v.availability === 'sold').length, + totalValue: allVehicles.reduce((sum, v) => sum + v.price, 0), + }; + setStats(computedStats); + } catch (err: unknown) { + const apiErr = err as { error?: { message?: string } }; + setError(apiErr?.error?.message || t('dashboard.error')); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + fetchDashboardData(); + }, [fetchDashboardData]); + + const currencyFormatter = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }); + + const vehicleColumns = [ + { + key: 'make', + label: t('dashboard.make'), + render: (row: VehicleResponse) => ( + + ), + }, + { key: 'model', label: t('dashboard.model') }, + { key: 'fin', label: t('dashboard.fin') }, + { + key: 'price', + label: t('dashboard.price'), + render: (row: VehicleResponse) => currencyFormatter.format(row.price), + }, + { key: 'availability', label: t('dashboard.availability') }, + ]; + + if (loading) { + return ( +
+ {t('common.loading')} +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + return ( +
+

+ {t('dashboard.title')} +

+ +
+ + + + +
+ +
+
+ {EuroIcon} + {t('dashboard.totalValue')} +
+ + {currencyFormatter.format(stats.totalValue)} + +
+ +
+

{t('dashboard.quickActions')}

+
+ + + +
+
+ +
+

{t('dashboard.recentVehicles')}

+ + {recentVehicles.length > 0 ? ( + row.id} /> + ) : ( +

+ {t('dashboard.noVehicles')} +

+ )} + + + + ); +} diff --git a/frontend/components/Navigation.tsx b/frontend/components/Navigation.tsx index 5ecd351..4c410b7 100644 --- a/frontend/components/Navigation.tsx +++ b/frontend/components/Navigation.tsx @@ -13,6 +13,15 @@ interface NavItem { } const navItems: NavItem[] = [ + { + href: '/de/dashboard', + labelKey: 'nav.dashboard', + icon: ( + + + + ), + }, { href: '/de/fahrzeuge', labelKey: 'nav.vehicles', diff --git a/frontend/messages/de.json b/frontend/messages/de.json index fea554e..2186a9d 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -19,6 +19,24 @@ "nav.retouch": "Bildretusche", "nav.menu": "Menü", "nav.appName": "ERP Nutzfahrzeuge", + "dashboard.title": "Dashboard", + "dashboard.totalVehicles": "Fahrzeuge gesamt", + "dashboard.available": "Verfügbar", + "dashboard.reserved": "Reserviert", + "dashboard.sold": "Verkauft", + "dashboard.totalValue": "Gesamtwert", + "dashboard.quickActions": "Schnellaktionen", + "dashboard.newVehicle": "Neues Fahrzeug", + "dashboard.newContact": "Neuer Kontakt", + "dashboard.newSale": "Neuer Verkauf", + "dashboard.recentVehicles": "Letzte Fahrzeuge", + "dashboard.noVehicles": "Keine Fahrzeuge vorhanden", + "dashboard.error": "Fehler beim Laden der Daten", + "dashboard.make": "Marke", + "dashboard.model": "Modell", + "dashboard.fin": "FIN", + "dashboard.price": "Preis", + "dashboard.availability": "Verfügbarkeit", "common.save": "Speichern", "common.cancel": "Abbrechen", "common.delete": "Löschen", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index c90343b..980b89f 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -19,6 +19,24 @@ "nav.retouch": "Image Retouch", "nav.menu": "Menu", "nav.appName": "ERP Commercial Vehicles", + "dashboard.title": "Dashboard", + "dashboard.totalVehicles": "Total Vehicles", + "dashboard.available": "Available", + "dashboard.reserved": "Reserved", + "dashboard.sold": "Sold", + "dashboard.totalValue": "Total Value", + "dashboard.quickActions": "Quick Actions", + "dashboard.newVehicle": "New Vehicle", + "dashboard.newContact": "New Contact", + "dashboard.newSale": "New Sale", + "dashboard.recentVehicles": "Recent Vehicles", + "dashboard.noVehicles": "No vehicles available", + "dashboard.error": "Error loading data", + "dashboard.make": "Make", + "dashboard.model": "Model", + "dashboard.fin": "VIN", + "dashboard.price": "Price", + "dashboard.availability": "Availability", "common.save": "Save", "common.cancel": "Cancel", "common.delete": "Delete",