import { useCallback, useMemo, useState, type ReactNode } from 'react'; import { AlertTriangle, CalendarDays, Camera, ClipboardList, Key, Package, Smartphone, Sparkles, TrendingUp, UserRound } from 'lucide-react'; import { Head, Link, router, usePage } from '@inertiajs/react'; import AppLayout from '@/layouts/app-layout'; import { edit as passwordSettings } from '@/routes/password'; import profileRoutes from '@/routes/profile'; import { send as resendVerificationRoute } from '@/routes/verification'; import { type SharedData } from '@/types'; import { DashboardLanguageSwitcher } from '@/components/dashboard-language-switcher'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { Progress } from '@/components/ui/progress'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; type DashboardMetrics = { total_events: number; active_events: number; published_events: number; events_with_tasks: number; upcoming_events: number; new_photos: number; task_progress: number; active_package: { name: string; expires_at: string | null; remaining_events: number | null; price: number | null; is_active?: boolean; } | null; }; type DashboardEvent = { id: number; name: string; slug: string | null; status: string | null; isActive: boolean; date: string | null; photosCount: number; tasksCount: number; joinTokensCount: number; }; type DashboardPurchase = { id: number; packageName: string; price: number | null; purchasedAt: string | null; type: string | null; provider: string | null; source?: string | null; }; type DashboardOnboardingStep = { key: string; title: string; description: string; done: boolean; cta?: string | null; ctaLabel?: string | null; }; type TenantSummary = { id: number; name: string; subscriptionStatus: string | null; subscriptionExpiresAt: string | null; activePackage: { name: string; price: number | null; expiresAt: string | null; remainingEvents: number | null; isActive?: boolean; } | null; } | null; type DashboardPageProps = { metrics: DashboardMetrics | null; upcomingEvents: DashboardEvent[]; recentPurchases: DashboardPurchase[]; latestPurchase: DashboardPurchase | null; tenant: TenantSummary; emailVerification: { mustVerify: boolean; verified: boolean; }; onboarding: DashboardOnboardingStep[]; }; const getNestedValue = (source: unknown, path: string): unknown => { if (!source || typeof source !== 'object') { return undefined; } return path.split('.').reduce((accumulator, segment) => { if (accumulator && typeof accumulator === 'object' && segment in (accumulator as Record)) { return (accumulator as Record)[segment]; } return undefined; }, source); }; export default function Dashboard() { const page = usePage(); const { metrics, upcomingEvents, recentPurchases, latestPurchase, tenant, emailVerification, locale, onboarding } = page.props; const { auth, supportedLocales } = page.props; const translations = (page.props.translations?.dashboard ?? {}) as Record; const [verificationSent, setVerificationSent] = useState(false); const [sendingVerification, setSendingVerification] = useState(false); const translate = useCallback( (path: string, fallback: string): string => { const value = getNestedValue(translations, path); return typeof value === 'string' && value.length > 0 ? value : fallback; }, [translations], ); const formatMessage = useCallback( (path: string, fallback: string, replacements: Record = {}) => { const template = translate(path, fallback); return Object.entries(replacements).reduce((carry, [token, value]) => { return carry.replace(new RegExp(`:${token}`, 'g'), String(value)); }, template); }, [translate], ); const user = auth?.user; const greetingName = typeof user?.name === 'string' && user.name.trim() !== '' ? user.name : 'Fotospiel-Team'; const greetingTitle = formatMessage('hero.title', 'Willkommen zurück, :name!', { name: greetingName }); const heroBadge = translate('hero.badge', 'Kundenbereich'); const heroSubtitle = translate( 'hero.subtitle', 'Dein Überblick über Pakete, Rechnungen und Fortschritt – für alle Details öffne die Admin-App.', ); const heroDescription = translate( 'hero.description', 'Nutze das Dashboard für Überblick, Abrechnung und Insights – die Admin-App begleitet dich bei allen operativen Aufgaben vor Ort.', ); const needsEmailVerification = emailVerification.mustVerify && !emailVerification.verified; const taskProgress = metrics?.task_progress ?? 0; const dateFormatter = useMemo(() => new Intl.DateTimeFormat(locale ?? 'de-DE', { day: '2-digit', month: 'short', year: 'numeric', }), [locale]); const currencyFormatter = useMemo(() => new Intl.NumberFormat(locale ?? 'de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 2, }), [locale]); const onboardingSteps = useMemo(() => onboarding ?? [], [onboarding]); const stats = useMemo(() => { const activeEvents = metrics?.active_events ?? 0; const upcomingEventsCount = metrics?.upcoming_events ?? 0; const newPhotos = metrics?.new_photos ?? 0; return [ { key: 'active-events', label: translate('stats.active_events.label', 'Aktive Events'), value: activeEvents, description: activeEvents > 0 ? translate('stats.active_events.description_positive', 'Events sind live und für Gäste sichtbar.') : translate('stats.active_events.description_zero', 'Noch kein Event veröffentlicht – starte heute!'), icon: CalendarDays, }, { key: 'upcoming-events', label: translate('stats.upcoming_events.label', 'Bevorstehende Events'), value: upcomingEventsCount, description: upcomingEventsCount > 0 ? translate('stats.upcoming_events.description_positive', 'Planung läuft – behalte Checklisten und Aufgaben im Blick.') : translate('stats.upcoming_events.description_zero', 'Lass dich vom Assistenten beim Planen unterstützen.'), icon: TrendingUp, }, { key: 'new-photos', label: translate('stats.new_photos.label', 'Neue Fotos (7 Tage)'), value: newPhotos, description: newPhotos > 0 ? translate('stats.new_photos.description_positive', 'Frisch eingetroffene Erinnerungen deiner Gäste.') : translate('stats.new_photos.description_zero', 'Sammle erste Uploads über QR-Code oder Direktlink.'), icon: Camera, }, { key: 'task-progress', label: translate('stats.task_progress.label', 'Event-Checkliste'), value: `${taskProgress}%`, description: taskProgress > 0 ? translate('stats.task_progress.description_positive', 'Starker Fortschritt! Halte deine Aufgabenliste aktuell.') : translate('stats.task_progress.description_zero', 'Nutze Aufgaben und Vorlagen für einen strukturierten Ablauf.'), icon: ClipboardList, }, ]; }, [metrics, taskProgress, translate]); const quickActions = useMemo(() => { const languageAwarePackagesHref = `/${locale ?? supportedLocales?.[0] ?? 'de'}/packages`; return [ { key: 'tenant-admin', label: translate('cards_section.quick_actions.items.tenant_admin.label', 'Event-Admin öffnen'), description: translate('cards_section.quick_actions.items.tenant_admin.description', 'Detaillierte Eventverwaltung, Moderation und Live-Features.'), href: '/event-admin', icon: Sparkles, }, { key: 'profile', label: translate('cards_section.quick_actions.items.profile.label', 'Profil verwalten'), description: translate('cards_section.quick_actions.items.profile.description', 'Kontaktinformationen, Sprache und E-Mail-Adresse anpassen.'), href: profileRoutes.index().url, icon: UserRound, }, { key: 'password', label: translate('cards_section.quick_actions.items.password.label', 'Passwort aktualisieren'), description: translate('cards_section.quick_actions.items.password.description', 'Sichere dein Konto mit einem aktuellen Passwort.'), href: passwordSettings().url, icon: Key, }, { key: 'packages', label: translate('cards_section.quick_actions.items.packages.label', 'Pakete entdecken'), description: translate('cards_section.quick_actions.items.packages.description', 'Mehr Events oder Speicher buchen – du bleibst flexibel.'), href: languageAwarePackagesHref, icon: Package, }, ]; }, [locale, supportedLocales, translate]); const spotlight = useMemo(() => ({ title: translate('spotlight.title', 'Admin-App als Schaltzentrale'), description: translate('spotlight.description', 'Events planen, Uploads freigeben, Gäste begleiten – mobil und in Echtzeit.'), cta: translate('spotlight.cta', 'Admin-App öffnen'), items: [ { key: 'live', title: translate('spotlight.items.live.title', 'Live auf dem Event'), description: translate('spotlight.items.live.description', 'Moderation der Uploads, Freigaben & Push-Updates jederzeit griffbereit.'), }, { key: 'mobile', title: translate('spotlight.items.mobile.title', 'Optimiert für mobile Einsätze'), description: translate('spotlight.items.mobile.description', 'PWA heute, native Apps morgen – ein Zugang für das ganze Team.'), }, { key: 'overview', title: translate('spotlight.items.overview.title', 'Dashboard als Überblick'), description: translate('spotlight.items.overview.description', 'Pakete, Rechnungen und Fortschritt siehst du weiterhin hier im Webportal.'), }, ], }), [translate]); const handleResendVerification = () => { setSendingVerification(true); setVerificationSent(false); router.post(resendVerificationRoute(), {}, { preserveScroll: true, onSuccess: () => setVerificationSent(true), onFinish: () => setSendingVerification(false), }); }; const renderPrice = (price: number | null) => { if (price === null) { return '—'; } try { return currencyFormatter.format(price); } catch (error) { return `${price.toFixed(2)} €`; } }; const formatDate = (value: string | null) => (value ? dateFormatter.format(new Date(value)) : '—'); const emailTitle = translate('email_verification.title', 'Bitte bestätige deine E-Mail-Adresse'); const emailDescription = translate( 'email_verification.description', 'Du kannst alle Funktionen erst vollständig nutzen, sobald deine E-Mail-Adresse bestätigt ist. Prüfe dein Postfach oder fordere einen neuen Link an.', ); const emailSuccess = translate('email_verification.success', 'Wir haben dir gerade einen neuen Bestätigungslink geschickt.'); const resendIdleLabel = translate('email_verification.cta', 'Link erneut senden'); const resendPendingLabel = translate('email_verification.cta_pending', 'Sende...'); const onboardingCardTitle = translate('onboarding.card.title', 'Dein Start in fünf Schritten'); const onboardingCardDescription = translate( 'onboarding.card.description', 'Bearbeite die Schritte in der Admin-App – das Dashboard zeigt dir den Status.', ); const onboardingCompletedCopy = translate( 'onboarding.card.completed', 'Alle Schritte abgeschlossen – großartig! Du kannst jederzeit zur Admin-App wechseln.', ); const onboardingFallbackCta = translate('onboarding.card.cta_fallback', 'Jetzt starten'); const upcomingCardTitle = translate('events.card.title', 'Bevorstehende Events'); const upcomingCardDescription = translate( 'events.card.description', 'Status, Uploads und Aufgaben deiner nächsten Events im Überblick.', ); const upcomingEmptyCopy = translate( 'events.card.empty', 'Plane dein erstes Event und begleite den gesamten Ablauf – vom Briefing bis zur Nachbereitung – direkt in der Admin-App.', ); const upcomingBadgeLabel = upcomingEvents.length > 0 ? formatMessage('events.card.badge.plural', `${upcomingEvents.length} geplant`, { count: upcomingEvents.length }) : translate('events.card.badge.empty', 'Noch kein Event geplant'); const eventLiveStatus = translate('events.status.live', 'Live'); const eventUpcomingStatus = translate('events.status.upcoming', 'In Vorbereitung'); const eventPhotoLabel = translate('events.badges.photos', 'Fotos'); const eventTaskLabel = translate('events.badges.tasks', 'Aufgaben'); const eventLinksLabel = translate('events.badges.links', 'Links'); const packageCardTitle = translate('cards_section.package.title', 'Aktuelles Paket'); const packageCardDescription = translate('cards_section.package.description', 'Behalte Laufzeiten und verfügbaren Umfang stets im Blick.'); const packageExpiresLabel = translate('cards_section.package.expires_at', 'Läuft ab'); const packagePriceLabel = translate('cards_section.package.price', 'Preis'); const packageEmptyCopy = translate('cards_section.package.empty', 'Noch kein aktives Paket.'); const packageEmptyCta = translate('cards_section.package.empty_cta', 'Jetzt Paket auswählen'); const packageEmptySuffix = translate('cards_section.package.empty_suffix', 'und anschließend in der Admin-App Events anlegen.'); const purchasesCardTitle = translate('cards_section.purchases.title', 'Aktuelle Buchungen'); const purchasesCardDescription = translate('cards_section.purchases.description', 'Verfolge deine gebuchten Pakete und Erweiterungen.'); const purchasesBadge = formatMessage('cards_section.purchases.badge', `${recentPurchases.length} Einträge`, { count: recentPurchases.length, }); const purchasesEmptyCopy = translate( 'cards_section.purchases.empty', 'Noch keine Buchungen sichtbar. Starte jetzt und sichere dir dein erstes Kontingent.', ); const purchasesHeaders = { package: translate('cards_section.purchases.table.package', 'Paket'), type: translate('cards_section.purchases.table.type', 'Typ'), provider: translate('cards_section.purchases.table.provider', 'Anbieter'), date: translate('cards_section.purchases.table.date', 'Datum'), price: translate('cards_section.purchases.table.price', 'Preis'), }; const quickActionsCardTitle = translate('cards_section.quick_actions.title', 'Schnellzugriff'); const quickActionsCardDescription = translate( 'cards_section.quick_actions.description', 'Alles Wichtige für den nächsten Schritt nur einen Klick entfernt.', ); const quickActionsCta = translate('cards_section.quick_actions.cta', 'Weiter'); const taskProgressNote = formatMessage( 'stats.task_progress.note', ':value% deiner Event-Checkliste erledigt.', { value: taskProgress }, ); const latestPurchaseInfo = latestPurchase ? formatMessage( 'cards_section.package.latest', `Zuletzt gebucht am ${formatDate(latestPurchase.purchasedAt)} via ${latestPurchase.provider?.toUpperCase() ?? 'Checkout'}.`, { date: formatDate(latestPurchase.purchasedAt), provider: latestPurchase.provider?.toUpperCase() ?? 'Checkout', }, ) : null; return (
{heroBadge}

{greetingTitle}

{heroSubtitle}

{heroDescription}

Fotospiel
{needsEmailVerification && (
{emailTitle} {emailDescription}
{verificationSent && {emailSuccess}}
)}
{stats.map((stat) => (

{stat.label}

{stat.value}

{stat.description}

{stat.key === 'task-progress' && (
{taskProgressNote}
)}
))}
{spotlight.title}

{spotlight.description}

{spotlight.items.map((item) => (

{item.title}

{item.description}

))}
{onboardingCardTitle}

{onboardingCardDescription}

{onboardingSteps.map((step) => (

{step.title}

{step.description}

{!step.done && step.cta ? ( ) : null}
))} {onboardingSteps.length === 0 && (

{onboardingCompletedCopy}

)}
{upcomingCardTitle}

{upcomingCardDescription}

0 ? 'secondary' : 'outline'}>{upcomingBadgeLabel}
{upcomingEvents.length === 0 && (

{upcomingEmptyCopy}

)} {upcomingEvents.map((event) => (

{event.name}

{formatDate(event.date)} · {event.status === 'published' || event.isActive ? eventLiveStatus : eventUpcomingStatus}

{event.photosCount} {eventPhotoLabel} {event.tasksCount} {eventTaskLabel} {event.joinTokensCount} {eventLinksLabel}
))}
{packageCardTitle}

{packageCardDescription}

{tenant?.activePackage ? (
{tenant.activePackage.name} {formatMessage('cards_section.package.remaining', `${tenant.activePackage.remainingEvents ?? 0} Events`, { count: tenant.activePackage.remainingEvents ?? 0, })}
{packageExpiresLabel} {formatDate(tenant.activePackage.expiresAt)}
{packagePriceLabel} {renderPrice(tenant.activePackage.price)}
{latestPurchaseInfo && (
{latestPurchaseInfo}
)}
) : (
{packageEmptyCopy}{' '} {packageEmptyCta} {' '} {packageEmptySuffix}
)}
{purchasesCardTitle}

{purchasesCardDescription}

{purchasesBadge}
{recentPurchases.length === 0 ? (
{purchasesEmptyCopy}
) : ( {purchasesHeaders.package} {purchasesHeaders.type} {purchasesHeaders.provider} {purchasesHeaders.date} {purchasesHeaders.price} {recentPurchases.map((purchase) => ( {purchase.packageName} {purchase.type ?? '—'} {purchase.provider ?? 'Checkout'} {formatDate(purchase.purchasedAt)} {renderPrice(purchase.price)} ))}
)}
{quickActionsCardTitle}

{quickActionsCardDescription}

{quickActions.map((action) => (

{action.label}

{action.description}

))}
); } (Dashboard as any).layout = (page: ReactNode) => page;