import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { CalendarDays, Image as ImageIcon, ListTodo, QrCode, Settings, Users, Sparkles } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell, renderEventLocation } from './components/MobileShell'; import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge } from './components/Primitives'; import { adminPath } from '../constants'; import { useEventContext } from '../context/EventContext'; import { getEventStats, EventStats, TenantEvent, getEvents } from '../api'; import { formatEventDate, resolveEventDisplayName } from '../lib/events'; import { useTheme } from '@tamagui/core'; export default function MobileDashboardPage() { const navigate = useNavigate(); const { t, i18n } = useTranslation('management'); const { events, activeEvent, hasEvents, hasMultipleEvents, isLoading, selectEvent } = useEventContext(); const [fallbackEvents, setFallbackEvents] = React.useState([]); const [fallbackLoading, setFallbackLoading] = React.useState(false); const [fallbackAttempted, setFallbackAttempted] = React.useState(false); const theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc'); const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1'); const border = String(theme.borderColor?.val ?? '#334155'); const surface = String(theme.surface?.val ?? '#ffffff'); const accentSoft = String(theme.blue3?.val ?? '#e0f2fe'); const accentText = String(theme.primary?.val ?? '#3b82f6'); const { data: stats, isLoading: statsLoading } = useQuery({ queryKey: ['mobile', 'dashboard', 'stats', activeEvent?.slug], enabled: Boolean(activeEvent?.slug), queryFn: async () => { if (!activeEvent?.slug) return null; return await getEventStats(activeEvent.slug); }, }); const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; const { data: dashboardEvents } = useQuery({ queryKey: ['mobile', 'dashboard', 'events'], queryFn: () => getEvents({ force: true }), staleTime: 60_000, }); const effectiveEvents = events.length ? events : dashboardEvents?.length ? dashboardEvents : fallbackEvents; const effectiveHasEvents = hasEvents || Boolean(dashboardEvents?.length) || fallbackEvents.length > 0; const effectiveMultiple = hasMultipleEvents || (dashboardEvents?.length ?? 0) > 1 || fallbackEvents.length > 1; React.useEffect(() => { if (events.length || isLoading || fallbackLoading || fallbackAttempted) { return; } setFallbackAttempted(true); setFallbackLoading(true); getEvents({ force: true }) .then((list: TenantEvent[]) => { setFallbackEvents(list ?? []); if (list?.length === 1 && !activeEvent) { selectEvent(list[0]?.slug ?? null); } }) .catch(() => { setFallbackEvents([]); }) .finally(() => setFallbackLoading(false)); }, [events.length, isLoading, activeEvent, selectEvent, fallbackLoading, fallbackAttempted]); if (isLoading || fallbackLoading) { return ( {Array.from({ length: 3 }).map((_, idx) => ( ))} ); } if (!effectiveHasEvents) { return ( ); } if (effectiveMultiple && !activeEvent) { return ( ); } return ( activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/photos`))} onManageTasks={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/tasks`))} onShowQr={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/qr`))} /> activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/members`))} onPrint={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/qr`))} onInvites={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/members`))} onSettings={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}`))} /> ); } function OnboardingEmptyState() { const { t } = useTranslation('management'); const navigate = useNavigate(); const theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc'); const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1'); return ( {t('mobileDashboard.emptyTitle', 'Create your first event')} {t('mobileDashboard.emptyBody', 'Start an event to manage tasks, QR posters and uploads.')} navigate(adminPath('/mobile/events/new'))} /> navigate(adminPath('/mobile/events'))} /> {t('mobileDashboard.highlightsTitle', 'What you can do')} {[ t('mobileDashboard.highlightImages', 'Review photos & uploads'), t('mobileDashboard.highlightTasks', 'Assign tasks & challenges'), t('mobileDashboard.highlightQr', 'Share QR posters'), t('mobileDashboard.highlightGuests', 'Invite helpers & guests'), ].map((item) => ( {item} ))} ); } function EventPickerList({ events, locale, text, muted, border }: { events: TenantEvent[]; locale: string; text: string; muted: string; border: string }) { const { t } = useTranslation('management'); const { selectEvent } = useEventContext(); const navigate = useNavigate(); const [localEvents, setLocalEvents] = React.useState(events); const [loading, setLoading] = React.useState(false); React.useEffect(() => { setLocalEvents(events); }, [events]); React.useEffect(() => { if (events.length > 0 || loading) { return; } setLoading(true); getEvents({ force: true }) .then((list) => setLocalEvents(list ?? [])) .catch(() => setLocalEvents([])) .finally(() => setLoading(false)); }, [events.length, loading]); return ( {t('mobileDashboard.pickEvent', 'Select an event')} {localEvents.map((event) => ( { selectEvent(event.slug ?? null); if (event.slug) { navigate(adminPath(`/mobile/events/${event.slug}`)); } }} > {resolveEventDisplayName(event)} {formatEventDate(event.event_date, locale) ?? t('mobileDashboard.status.draft', 'Draft')} {event.status === 'published' ? t('mobileDashboard.status.published', 'Live') : t('mobileDashboard.status.draft', 'Draft')} ))} ); } function FeaturedActions({ onReviewPhotos, onManageTasks, onShowQr, }: { onReviewPhotos: () => void; onManageTasks: () => void; onShowQr: () => void; }) { const { t } = useTranslation('management'); const theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc'); const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1'); const cards = [ { key: 'photos', label: t('mobileDashboard.photosLabel', 'Review photos'), desc: t('mobileDashboard.photosDesc', 'Moderate uploads and highlights'), icon: ImageIcon, color: '#0ea5e9', action: onReviewPhotos, }, { key: 'tasks', label: t('mobileDashboard.tasksLabel', 'Manage tasks & challenges'), desc: t('mobileDashboard.tasksDesc', 'Assign and track progress'), icon: ListTodo, color: '#22c55e', action: onManageTasks, }, { key: 'qr', label: t('mobileDashboard.qrLabel', 'Show / share QR code'), desc: t('mobileDashboard.qrDesc', 'Posters, cards, and links'), icon: QrCode, color: '#f59e0b', action: onShowQr, }, ]; return ( {cards.map((card) => ( {card.label} {card.desc} ˃ ))} ); } function SecondaryGrid({ event, onGuests, onPrint, onInvites, onSettings, }: { event: TenantEvent | null; onGuests: () => void; onPrint: () => void; onInvites: () => void; onSettings: () => void; }) { const { t } = useTranslation('management'); const theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc'); const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1'); const border = String(theme.borderColor?.val ?? '#334155'); const surface = String(theme.surface?.val ?? '#0b1220'); const tiles = [ { icon: Users, label: t('mobileDashboard.shortcutGuests', 'Guest management'), color: '#60a5fa', action: onGuests, }, { icon: QrCode, label: t('mobileDashboard.shortcutPrints', 'Print & poster downloads'), color: '#fbbf24', action: onPrint, }, { icon: Sparkles, label: t('mobileDashboard.shortcutInvites', 'Team / helper invites'), color: '#a855f7', action: onInvites, }, { icon: Settings, label: t('mobileDashboard.shortcutSettings', 'Event settings'), color: '#10b981', action: onSettings, }, ]; return ( {t('mobileDashboard.shortcutsTitle', 'Shortcuts')} {tiles.map((tile) => ( ))} {event ? ( {resolveEventDisplayName(event)} {renderEventLocation(event)} ) : null} ); } function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null; stats: EventStats | null | undefined; loading: boolean; locale: string }) { const { t } = useTranslation('management'); const theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc'); const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1'); if (!event) return null; const kpis = [ { label: t('mobileDashboard.kpiTasks', 'Open tasks'), value: event.tasks_count ?? '—', icon: ListTodo, }, { label: t('mobileDashboard.kpiPhotos', 'Photos'), value: stats?.uploads_total ?? event.photo_count ?? '—', icon: ImageIcon, }, { label: t('mobileDashboard.kpiGuests', 'Guests'), value: event.active_invites_count ?? event.total_invites_count ?? '—', icon: Users, }, ]; return ( {t('mobileDashboard.kpiTitle', 'Key performance indicators')} {loading ? ( {Array.from({ length: 3 }).map((_, idx) => ( ))} ) : ( {kpis.map((kpi) => ( ))} )} {formatEventDate(event.event_date, locale) ?? ''} ); } function AlertsAndHints({ event, stats }: { event: TenantEvent | null; stats: EventStats | null | undefined }) { const { t } = useTranslation('management'); const theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc'); const warningBg = String(theme.yellow3?.val ?? '#fff7ed'); const warningBorder = String(theme.yellow6?.val ?? '#fed7aa'); const warningText = String(theme.yellow11?.val ?? '#9a3412'); if (!event) return null; const alerts: string[] = []; if (stats?.pending_photos) { alerts.push(t('mobileDashboard.alertPending', '{{count}} new uploads awaiting moderation', { count: stats.pending_photos })); } if (event.tasks_count) { alerts.push(t('mobileDashboard.alertTasks', '{{count}} tasks due or open', { count: event.tasks_count })); } if (alerts.length === 0) { return null; } return ( {t('mobileDashboard.alertsTitle', 'Alerts')} {alerts.map((alert) => ( {alert} ))} ); }