import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { CalendarDays, Camera, AlertTriangle, Sparkles, Users, Plus, Settings, CheckCircle2, Circle, QrCode, ClipboardList, Package as PackageIcon, Loader2, } from 'lucide-react'; import toast from 'react-hot-toast'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { AdminLayout } from '../components/AdminLayout'; import { DashboardSummary, getDashboardSummary, getEvents, getTenantPackagesOverview, TenantEvent, TenantPackageSummary, } from '../api'; import { isAuthError } from '../auth/tokens'; import { useAuth } from '../auth/context'; import { adminPath, ADMIN_EVENT_VIEW_PATH, ADMIN_EVENTS_PATH, ADMIN_EVENT_TASKS_PATH, ADMIN_BILLING_PATH, ADMIN_SETTINGS_PATH, ADMIN_WELCOME_BASE_PATH, ADMIN_EVENT_CREATE_PATH, buildEngagementTabPath, } from '../constants'; import { useOnboardingProgress } from '../onboarding'; import { buildLimitWarnings } from '../lib/limitWarnings'; interface DashboardState { summary: DashboardSummary | null; events: TenantEvent[]; activePackage: TenantPackageSummary | null; loading: boolean; errorKey: string | null; } type ReadinessState = { hasEvent: boolean; hasTasks: boolean; hasQrInvites: boolean; hasPackage: boolean; primaryEventSlug: string | null; primaryEventName: string | null; loading: boolean; }; export default function DashboardPage() { const navigate = useNavigate(); const location = useLocation(); const { user } = useAuth(); const { progress, markStep } = useOnboardingProgress(); const { t, i18n } = useTranslation('dashboard', { keyPrefix: 'dashboard' }); const { t: tc } = useTranslation('common'); const translate = React.useCallback( (key: string, options?: Record) => { const value = t(key, options); if (value === `dashboard.${key}`) { const fallback = i18n.t(`dashboard:${key}`, options); return fallback === `dashboard:${key}` ? value : fallback; } return value; }, [t, i18n], ); const [state, setState] = React.useState({ summary: null, events: [], activePackage: null, loading: true, errorKey: null, }); const [readiness, setReadiness] = React.useState({ hasEvent: false, hasTasks: false, hasQrInvites: false, hasPackage: false, primaryEventSlug: null, primaryEventName: null, loading: false, }); React.useEffect(() => { let cancelled = false; (async () => { try { const [summary, events, packages] = await Promise.all([ getDashboardSummary().catch(() => null), getEvents().catch(() => [] as TenantEvent[]), getTenantPackagesOverview().catch(() => ({ packages: [], activePackage: null })), ]); if (cancelled) { return; } const fallbackSummary = buildSummaryFallback(events, packages.activePackage); const primaryEvent = events[0] ?? null; const primaryEventName = primaryEvent ? resolveEventName(primaryEvent.name, primaryEvent.slug) : null; setReadiness({ hasEvent: events.length > 0, hasTasks: primaryEvent ? (Number(primaryEvent.tasks_count ?? 0) > 0) : false, hasQrInvites: primaryEvent ? Number( primaryEvent.active_invites_count ?? primaryEvent.active_join_tokens_count ?? 0 ) > 0 : false, hasPackage: Boolean(packages.activePackage), primaryEventSlug: primaryEvent?.slug ?? null, primaryEventName, loading: false, }); setState({ summary: summary ?? fallbackSummary, events, activePackage: packages.activePackage, loading: false, errorKey: null, }); if (!primaryEvent && !cancelled) { setReadiness((prev) => ({ ...prev, hasTasks: false, hasQrInvites: false, loading: false, })); } } catch (error) { if (!isAuthError(error)) { setState((prev) => ({ ...prev, errorKey: 'loadFailed', loading: false, })); } } })(); return () => { cancelled = true; }; }, []); const { summary, events, activePackage, loading, errorKey } = state; React.useEffect(() => { if (loading) { return; } if (!progress.eventCreated && events.length === 0 && !location.pathname.startsWith(ADMIN_WELCOME_BASE_PATH)) { navigate(ADMIN_WELCOME_BASE_PATH, { replace: true }); return; } if (events.length > 0 && !progress.eventCreated) { markStep({ eventCreated: true }); } }, [loading, events.length, progress.eventCreated, navigate, location.pathname, markStep]); const greetingName = user?.name ?? translate('welcome.fallbackName'); const greetingTitle = translate('welcome.greeting', { name: greetingName }); const subtitle = translate('welcome.subtitle'); const errorMessage = errorKey ? translate(`errors.${errorKey}`) : null; const dateLocale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; const upcomingEvents = getUpcomingEvents(events); const publishedEvents = events.filter((event) => event.status === 'published'); const primaryEvent = events[0] ?? null; const primaryEventName = primaryEvent ? resolveEventName(primaryEvent.name, primaryEvent.slug) : null; const primaryEventLimits = primaryEvent?.limits ?? null; const limitTranslate = React.useCallback( (key: string, options?: Record) => tc(`limits.${key}`, options), [tc], ); const limitWarnings = React.useMemo( () => buildLimitWarnings(primaryEventLimits, limitTranslate), [primaryEventLimits, limitTranslate], ); const shownToastsRef = React.useRef>(new Set()); React.useEffect(() => { limitWarnings.forEach((warning) => { const toastKey = `${warning.id}-${warning.message}`; if (shownToastsRef.current.has(toastKey)) { return; } shownToastsRef.current.add(toastKey); toast(warning.message, { icon: warning.tone === 'danger' ? '🚨' : '⚠️', id: toastKey, }); }); }, [limitWarnings]); const limitScopeLabels = React.useMemo( () => ({ photos: tc('limits.photosTitle'), guests: tc('limits.guestsTitle'), gallery: tc('limits.galleryTitle'), }), [tc], ); const actions = ( <> {events.length === 0 && ( )} ); return ( {errorMessage && ( {t('dashboard.alerts.errorTitle')} {errorMessage} )} {loading ? ( ) : ( <> {events.length === 0 && ( {translate('welcomeCard.title')} {translate('welcomeCard.summary')}

{translate('welcomeCard.body1')}

{translate('welcomeCard.body2')}

)}
{translate('overview.title')} {translate('overview.description')}
{activePackage?.package_name ?? translate('overview.noPackage')}
} /> } /> } /> {activePackage ? ( } /> ) : null}
{primaryEventLimits ? (
{translate('limitsCard.title')} {primaryEventName ? translate('limitsCard.description', { name: primaryEventName }) : translate('limitsCard.descriptionFallback')}
{primaryEventName ?? translate('limitsCard.descriptionFallback')}
{limitWarnings.length > 0 && (
{limitWarnings.map((warning) => ( {limitScopeLabels[warning.scope]} {warning.message} ))}
)}
) : null}
{translate('quickActions.title')} {translate('quickActions.description')}
} label={translate('quickActions.createEvent.label')} description={translate('quickActions.createEvent.description')} onClick={() => navigate(ADMIN_EVENT_CREATE_PATH)} /> } label={translate('quickActions.moderatePhotos.label')} description={translate('quickActions.moderatePhotos.description')} onClick={() => navigate(ADMIN_EVENTS_PATH)} /> } label={translate('quickActions.organiseTasks.label')} description={translate('quickActions.organiseTasks.description')} onClick={() => navigate(buildEngagementTabPath('tasks'))} /> } label={translate('quickActions.managePackages.label')} description={translate('quickActions.managePackages.description')} onClick={() => navigate(ADMIN_BILLING_PATH)} />
navigate(ADMIN_EVENT_CREATE_PATH)} onOpenTasks={() => readiness.primaryEventSlug ? navigate(ADMIN_EVENT_TASKS_PATH(readiness.primaryEventSlug)) : navigate(buildEngagementTabPath('tasks')) } onOpenQr={() => readiness.primaryEventSlug ? navigate(`${ADMIN_EVENT_VIEW_PATH(readiness.primaryEventSlug)}#qr-invites`) : navigate(ADMIN_EVENTS_PATH) } onOpenPackages={() => navigate(ADMIN_BILLING_PATH)} />
{translate('upcoming.title')} {translate('upcoming.description')}
{upcomingEvents.length === 0 ? ( navigate(adminPath('/events/new'))} /> ) : ( upcomingEvents.map((event) => ( navigate(ADMIN_EVENT_VIEW_PATH(event.slug))} locale={dateLocale} labels={{ live: translate('upcoming.status.live'), planning: translate('upcoming.status.planning'), open: tc('actions.open'), noDate: translate('upcoming.status.noDate'), }} /> )) )}
)}
); } function formatDate(value: string | null, locale: string): string | null { if (!value) { return null; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return null; } try { return new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short', year: 'numeric', }).format(date); } catch { return date.toISOString().slice(0, 10); } } function resolveEventName(name: TenantEvent['name'], fallbackSlug: string): string { if (typeof name === 'string' && name.trim().length > 0) { return name; } if (name && typeof name === 'object') { if (typeof name.de === 'string' && name.de.trim().length > 0) { return name.de; } if (typeof name.en === 'string' && name.en.trim().length > 0) { return name.en; } const first = Object.values(name).find((value) => typeof value === 'string' && value.trim().length > 0); if (typeof first === 'string') { return first; } } return fallbackSlug || 'Event'; } function buildSummaryFallback( events: TenantEvent[], activePackage: TenantPackageSummary | null ): DashboardSummary { const activeEvents = events.filter((event) => Boolean(event.is_active || event.status === 'published')); const totalPhotos = events.reduce((sum, event) => sum + Number(event.photo_count ?? 0), 0); return { active_events: activeEvents.length, new_photos: totalPhotos, task_progress: 0, upcoming_events: activeEvents.length, active_package: activePackage ? { name: activePackage.package_name, remaining_events: activePackage.remaining_events, expires_at: activePackage.expires_at, } : null, }; } function getUpcomingEvents(events: TenantEvent[]): TenantEvent[] { const now = new Date(); return events .filter((event) => { if (!event.event_date) return false; const date = new Date(event.event_date); return !Number.isNaN(date.getTime()) && date >= now; }) .sort((a, b) => { const dateA = a.event_date ? new Date(a.event_date).getTime() : 0; const dateB = b.event_date ? new Date(b.event_date).getTime() : 0; return dateA - dateB; }) .slice(0, 4); } type ReadinessLabels = { title: string; description: string; pending: string; complete: string; items: { event: { title: string; hint: string }; tasks: { title: string; hint: string }; qr: { title: string; hint: string }; package: { title: string; hint: string }; }; actions: { createEvent: string; openTasks: string; openQr: string; openPackages: string; }; }; function LimitUsageRow({ label, summary, unlimitedLabel, usageLabel, remainingLabel, }: { label: string; summary: LimitUsageSummary | null; unlimitedLabel: string; usageLabel: string; remainingLabel: string; }) { if (!summary) { return (
{label} {unlimitedLabel}

{unlimitedLabel}

); } const limit = typeof summary.limit === 'number' && summary.limit > 0 ? summary.limit : null; const percent = limit ? Math.min(100, Math.round((summary.used / limit) * 100)) : 0; const remaining = typeof summary.remaining === 'number' ? summary.remaining : null; const barClass = summary.state === 'limit_reached' ? 'bg-rose-500' : summary.state === 'warning' ? 'bg-amber-500' : 'bg-emerald-500'; return (
{label} {limit ? usageLabel.replace('{{used}}', `${summary.used}`).replace('{{limit}}', `${limit}`) : unlimitedLabel}
{limit ? ( <>
{remaining !== null ? (

{remainingLabel .replace('{{remaining}}', `${Math.max(0, remaining)}`) .replace('{{limit}}', `${limit}`)}

) : null} ) : (

{unlimitedLabel}

)}
); } function GalleryStatusRow({ label, summary, locale, messages, }: { label: string; summary: GallerySummary | null; locale: string; messages: { expired: string; noExpiry: string; expires: string }; }) { const expiresAt = summary?.expires_at ? formatDate(summary.expires_at, locale) : null; let statusLabel = messages.noExpiry; let badgeClass = 'bg-emerald-500/20 text-emerald-700'; if (summary?.state === 'expired') { statusLabel = messages.expired; badgeClass = 'bg-rose-500/20 text-rose-700'; } else if (summary?.state === 'warning') { const days = Math.max(0, summary.days_remaining ?? 0); statusLabel = `${messages.expires.replace('{{date}}', expiresAt ?? '')} (${days}d)`; badgeClass = 'bg-amber-500/20 text-amber-700'; } else if (summary?.state === 'ok' && expiresAt) { statusLabel = messages.expires.replace('{{date}}', expiresAt); } return (
{label} {statusLabel}
); } function ReadinessCard({ readiness, labels, onCreateEvent, onOpenTasks, onOpenQr, onOpenPackages, }: { readiness: ReadinessState; labels: ReadinessLabels; onCreateEvent: () => void; onOpenTasks: () => void; onOpenQr: () => void; onOpenPackages: () => void; }) { const checklistItems = [ { key: 'event', icon: , completed: readiness.hasEvent, label: labels.items.event.title, hint: labels.items.event.hint, actionLabel: labels.actions.createEvent, onAction: onCreateEvent, showAction: !readiness.hasEvent, }, { key: 'tasks', icon: , completed: readiness.hasTasks, label: labels.items.tasks.title, hint: labels.items.tasks.hint, actionLabel: labels.actions.openTasks, onAction: onOpenTasks, showAction: readiness.hasEvent && !readiness.hasTasks, }, { key: 'qr', icon: , completed: readiness.hasQrInvites, label: labels.items.qr.title, hint: labels.items.qr.hint, actionLabel: labels.actions.openQr, onAction: onOpenQr, showAction: readiness.hasEvent && !readiness.hasQrInvites, }, { key: 'package', icon: , completed: readiness.hasPackage, label: labels.items.package.title, hint: labels.items.package.hint, actionLabel: labels.actions.openPackages, onAction: onOpenPackages, showAction: !readiness.hasPackage, }, ] as const; const activeEventName = readiness.primaryEventName; return ( {labels.title} {labels.description} {activeEventName ? (

{activeEventName}

) : null}
{readiness.loading ? (
{labels.pending}
) : ( checklistItems.map((item) => ( )) )}
); } function ChecklistRow({ icon, label, hint, completed, status, action, }: { icon: React.ReactNode; label: string; hint: string; completed: boolean; status: { complete: string; pending: string }; action?: { label: string; onClick: () => void; disabled?: boolean }; }) { return (
{icon}

{label}

{hint}

{completed ? : } {completed ? status.complete : status.pending} {action ? ( ) : null}
); } function StatCard({ label, value, hint, icon, }: { label: string; value: string | number; hint?: string; icon: React.ReactNode; }) { return (
{label} {icon}
{value}
{hint &&

{hint}

}
); } function QuickAction({ icon, label, description, onClick, }: { icon: React.ReactNode; label: string; description: string; onClick: () => void; }) { return ( ); } function UpcomingEventRow({ event, onView, locale, labels, }: { event: TenantEvent; onView: () => void; locale: string; labels: { live: string; planning: string; open: string; noDate: string; }; }) { const date = event.event_date ? new Date(event.event_date) : null; const formattedDate = date ? date.toLocaleDateString(locale, { day: '2-digit', month: 'short', year: 'numeric' }) : labels.noDate; return (
{formattedDate}
{event.status === 'published' ? labels.live : labels.planning}
); } function EmptyState({ message, ctaLabel, onCta }: { message: string; ctaLabel: string; onCta: () => void }) { return (

{message}

); } function DashboardSkeleton() { return (
{Array.from({ length: 3 }).map((_, index) => (
{Array.from({ length: 4 }).map((__ , cardIndex) => (
))}
))}
); }