import React from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { Bell, CalendarDays, Camera, CheckCircle2, ChevronDown, Download, Image as ImageIcon, Layout, ListTodo, MapPin, Megaphone, MessageCircle, Pencil, QrCode, Settings, ShieldCheck, Smartphone, Sparkles, TrendingUp, Tv, Users } 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 } from './components/MobileShell'; import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge, SkeletonCard } from './components/Primitives'; import { MobileSheet } from './components/Sheet'; import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants'; import { useEventContext } from '../context/EventContext'; import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview } from '../api'; import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events'; import { useAdminPushSubscription } from './hooks/useAdminPushSubscription'; import { useDevicePermissions } from './hooks/useDevicePermissions'; import { useInstallPrompt } from './hooks/useInstallPrompt'; import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour'; import { collectPackageFeatures, formatPackageLimit, getPackageFeatureLabel, getPackageLimitEntries } from './lib/packageSummary'; import { trackOnboarding } from '../api'; import { useAuth } from '../auth/context'; import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme'; import { isPastEvent } from './eventDate'; type DeviceSetupProps = { installPrompt: ReturnType; pushState: ReturnType; devicePermissions: ReturnType; onOpenSettings: () => void; }; export default function MobileDashboardPage() { const navigate = useNavigate(); const location = useLocation(); const { slug: slugParam } = useParams<{ slug?: string }>(); const { t, i18n } = useTranslation('management'); const { events, activeEvent, hasEvents, hasMultipleEvents, isLoading, selectEvent } = useEventContext(); const { status } = useAuth(); const [fallbackEvents, setFallbackEvents] = React.useState([]); const [fallbackLoading, setFallbackLoading] = React.useState(false); const [fallbackAttempted, setFallbackAttempted] = React.useState(false); const [tourOpen, setTourOpen] = React.useState(false); const [tourStep, setTourStep] = React.useState(0); const [summaryOpen, setSummaryOpen] = React.useState(false); const [summarySeenOverride, setSummarySeenOverride] = React.useState(null); const [eventSwitcherOpen, setEventSwitcherOpen] = React.useState(false); const onboardingTrackedRef = React.useRef(false); const installPrompt = useInstallPrompt(); const pushState = useAdminPushSubscription(); const devicePermissions = useDevicePermissions(); const { textStrong, muted, accentSoft, primary } = useAdminTheme(); const text = textStrong; const accentText = primary; 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 tasksEnabled = resolveEngagementMode(activeEvent ?? undefined) !== 'photo_only'; 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 { data: onboardingStatus, isLoading: onboardingLoading } = useQuery({ queryKey: ['mobile', 'onboarding', 'status'], queryFn: fetchOnboardingStatus, staleTime: 60_000, }); const { data: packagesOverview, isLoading: packagesLoading, isError: packagesError } = useQuery({ queryKey: ['mobile', 'onboarding', 'packages-overview'], queryFn: () => getTenantPackagesOverview({ 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; const tourTargetSlug = activeEvent?.slug ?? effectiveEvents[0]?.slug ?? null; const tourStepKeys = React.useMemo(() => resolveTourStepKeys(effectiveHasEvents), [effectiveHasEvents]); React.useEffect(() => { if (!slugParam || slugParam === activeEvent?.slug) { return; } selectEvent(slugParam); }, [activeEvent?.slug, selectEvent, slugParam]); React.useEffect(() => { if (status !== 'authenticated' || onboardingTrackedRef.current) { return; } onboardingTrackedRef.current = true; if (typeof window !== 'undefined') { try { const stored = window.localStorage.getItem('admin-onboarding-opened-v1'); if (stored) { return; } window.localStorage.setItem('admin-onboarding-opened-v1', '1'); } catch { // Ignore storage errors. } } void trackOnboarding('admin_app_opened'); }, [status]); const forceTour = React.useMemo(() => { const params = new URLSearchParams(location.search); return params.get('tour') === '1'; }, [location.search]); React.useEffect(() => { if (forceTour) { setTourStep(0); setTourOpen(true); setTourSeen(false); navigate(location.pathname, { replace: true }); return; } if (getTourSeen()) { return; } setTourOpen(true); }, [forceTour, location.pathname, navigate]); const activePackage = packagesOverview?.activePackage ?? packagesOverview?.packages?.find((pkg) => pkg.active) ?? null; const remainingEvents = activePackage?.remaining_events ?? null; const summarySeenPackageId = summarySeenOverride ?? onboardingStatus?.steps?.summary_seen_package_id ?? null; const hasSummaryPackage = Boolean(activePackage?.id && activePackage.id !== summarySeenPackageId); const shouldRedirectToBilling = !packagesLoading && !packagesError && !effectiveHasEvents && (activePackage === null || (remainingEvents !== null && remainingEvents <= 0)); React.useEffect(() => { if (packagesLoading || !shouldRedirectToBilling) { return; } navigate(adminPath('/mobile/billing#packages'), { replace: true }); }, [navigate, packagesLoading, shouldRedirectToBilling]); React.useEffect(() => { if (packagesLoading || packagesError || onboardingLoading) { return; } if (shouldRedirectToBilling) { return; } if (hasSummaryPackage) { setSummaryOpen(true); } }, [hasSummaryPackage, onboardingLoading, packagesLoading, shouldRedirectToBilling]); const markTourSeen = React.useCallback(() => { if (typeof window === 'undefined') { return; } setTourSeen(true); }, []); const closeTour = React.useCallback(() => { setTourOpen(false); markTourSeen(); }, [markTourSeen]); const tourSteps = React.useMemo(() => { const stepMap: Record; title: string; body: string; actionLabel?: string; action?: () => void; showAction?: boolean; }> = { event: { key: 'event', icon: Sparkles, title: t('mobileTour.eventTitle', 'Create your first event'), body: t('mobileTour.eventBody', 'Add the date, location, and package to unlock sharing.'), actionLabel: t('mobileTour.eventAction', 'Create event'), action: () => { closeTour(); navigate(adminPath('/mobile/events/new')); }, showAction: true, }, qr: { key: 'qr', icon: QrCode, title: t('mobileTour.qrTitle', 'Share your QR code'), body: t('mobileTour.qrBody', 'Guests join instantly by scanning or using the invite link.'), actionLabel: t('mobileTour.qrAction', 'Open QR'), action: () => { if (!tourTargetSlug) { return; } closeTour(); navigate(adminPath(`/mobile/events/${tourTargetSlug}/qr`)); }, showAction: Boolean(tourTargetSlug), }, photos: { key: 'photos', icon: ImageIcon, title: t('mobileTour.photosTitle', 'Moderate uploads'), body: t('mobileTour.photosBody', 'Approve, feature, or hide photos with quick swipes.'), actionLabel: t('mobileTour.photosAction', 'Review photos'), action: () => { if (!tourTargetSlug) { return; } closeTour(); navigate(adminPath(`/mobile/events/${tourTargetSlug}/control-room`)); }, showAction: Boolean(tourTargetSlug), }, push: { key: 'push', icon: Bell, title: t('mobileTour.pushTitle', 'Stay in the loop'), body: t('mobileTour.pushBody', 'Enable push alerts to catch limits and new uploads fast.'), actionLabel: t('mobileTour.pushAction', 'Notification settings'), action: () => { closeTour(); navigate(adminPath('/mobile/settings')); }, showAction: true, }, }; return tourStepKeys.map((key) => stepMap[key]); }, [closeTour, navigate, t, tourStepKeys, tourTargetSlug]); const activeTourStep = tourSteps[tourStep] ?? tourSteps[0]; const totalTourSteps = tourSteps.length; React.useEffect(() => { if (tourStep >= tourSteps.length) { setTourStep(0); } }, [tourStep, tourSteps.length]); const handleTourNext = React.useCallback(() => { if (tourStep >= totalTourSteps - 1) { closeTour(); return; } setTourStep((prev) => prev + 1); }, [closeTour, tourStep, totalTourSteps]); const handleTourBack = React.useCallback(() => { setTourStep((prev) => Math.max(prev - 1, 0)); }, []); const tourSheet = activeTourStep ? ( {t('mobileTour.progress', 'Step {{current}} of {{total}}', { current: tourStep + 1, total: totalTourSteps, })} {t('mobileTour.skip', 'Skip')} {activeTourStep.title} {activeTourStep.body} {activeTourStep.showAction && activeTourStep.action && activeTourStep.actionLabel ? ( ) : null} {tourStep > 0 ? ( ) : ( )} = totalTourSteps - 1 ? t('mobileTour.done', 'Done') : t('mobileTour.next', 'Next')} fullWidth={false} onPress={handleTourNext} /> ) : null; const handleSummaryClose = React.useCallback(() => { setSummaryOpen(false); if (activePackage?.id) { setSummarySeenOverride(activePackage.id); void trackOnboarding('summary_seen', { package_id: activePackage.id }); } }, [activePackage?.id]); const packageSummarySheet = activePackage ? ( { handleSummaryClose(); navigate(adminPath('/mobile/events/new')); }} packageName={activePackage.package_name ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary')} packageType={activePackage.package_type ?? null} remainingEvents={remainingEvents} purchasedAt={activePackage.purchased_at} expiresAt={activePackage.expires_at} limits={activePackage.package_limits ?? null} features={activePackage.features ?? null} locale={locale} /> ) : null; const showPackageSummaryBanner = Boolean(activePackage && summarySeenPackageId === activePackage.id) && !summaryOpen && !packagesLoading && !packagesError; 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) => ( ))} {tourSheet} {packageSummarySheet} ); } if (!effectiveHasEvents) { return ( {showPackageSummaryBanner ? ( setSummaryOpen(true)} /> ) : null} navigate(adminPath('/mobile/settings'))} /> {tourSheet} {packageSummarySheet} ); } if (effectiveMultiple && !activeEvent) { return ( {showPackageSummaryBanner ? ( setSummaryOpen(true)} /> ) : null} {tourSheet} {packageSummarySheet} ); } return ( {showPackageSummaryBanner ? ( setSummaryOpen(true)} /> ) : null} setEventSwitcherOpen(true)} onEdit={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/edit`))} /> navigate(path)} /> navigate(adminPath('/mobile/settings'))} /> {tourSheet} {packageSummarySheet} setEventSwitcherOpen(false)} events={effectiveEvents} locale={locale} /> ); } function PackageSummarySheet({ open, onClose, onContinue, packageName, packageType, remainingEvents, purchasedAt, expiresAt, limits, features, locale, }: { open: boolean; onClose: () => void; onContinue: () => void; packageName: string; packageType: string | null; remainingEvents: number | null | undefined; purchasedAt: string | null | undefined; expiresAt: string | null | undefined; limits: Record | null; features: string[] | null; locale: string; }) { const { t } = useTranslation('management'); const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme(); const text = textStrong; const resolvedFeatures = collectPackageFeatures({ features, package_limits: limits, branding_allowed: (limits as any)?.branding_allowed ?? null, watermark_allowed: (limits as any)?.watermark_allowed ?? null, package_type: packageType, } as any); const limitEntries = getPackageLimitEntries(limits, t, { remainingEvents }, { packageType }); const hasFeatures = resolvedFeatures.length > 0; const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown'); return ( {packageName} {t('mobileDashboard.packageSummary.subtitle', 'Here is a quick overview of your active package.')} {expiresAt ? ( ) : null} {limitEntries.length ? ( {t('mobileDashboard.packageSummary.limitsTitle', 'Limits')} {limitEntries.map((entry) => ( ))} ) : null} {hasFeatures ? ( {t('mobileDashboard.packageSummary.featuresTitle', 'Included features')} {resolvedFeatures.map((feature) => ( {getPackageFeatureLabel(feature, t)} ))} ) : null} {t('mobileDashboard.packageSummary.hint', 'You can revisit billing any time to review or upgrade your package.')} ); } function SummaryRow({ label, value }: { label: string; value: string }) { const { textStrong, muted } = useAdminTheme(); return ( {label} {value} ); } function PackageSummaryBanner({ packageName, onOpen, }: { packageName?: string | null; onOpen: () => void; }) { const { t } = useTranslation('management'); const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme(); const text = textStrong; return ( {t('mobileDashboard.packageSummary.bannerTitle', 'Your package summary')} {t('mobileDashboard.packageSummary.bannerSubtitle', { name: packageName ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary'), defaultValue: '{{name}} is active. Review limits & features.', })} ); } function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) { const { t } = useTranslation('management'); const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); const text = textStrong; const accent = primary; const iconBg = accentSoft; const iconColor = primary; const items: Array<{ key: string; icon: React.ComponentType<{ size?: number; color?: string }>; title: string; description: string; badge: string; badgeTone: 'success' | 'warning' | 'muted'; actionLabel?: string; action?: () => void; actionDisabled?: boolean; }> = []; const showInstall = !installPrompt.isInstalled && (installPrompt.canInstall || installPrompt.isIos); if (showInstall) { items.push({ key: 'install', icon: Smartphone, title: t('mobileDashboard.setup.installTitle', 'Install app'), description: installPrompt.isIos ? t('mobileDashboard.setup.installIosHint', 'On iOS: Share → Add to Home Screen.') : t('mobileDashboard.setup.installDescription', 'Get full-screen access and faster launch.'), badge: installPrompt.canInstall ? t('mobileDashboard.setup.installReady', 'Ready') : t('mobileDashboard.setup.installManual', 'Manual'), badgeTone: installPrompt.canInstall ? 'warning' : 'muted', actionLabel: installPrompt.canInstall ? t('mobileDashboard.setup.installAction', 'Install now') : undefined, action: installPrompt.canInstall ? () => void installPrompt.promptInstall() : undefined, actionDisabled: !installPrompt.canInstall, }); } if (pushState.supported && !pushState.subscribed) { const pushBlocked = pushState.permission === 'denied'; items.push({ key: 'push', icon: Bell, title: t('mobileDashboard.setup.pushTitle', 'Enable push notifications'), description: pushBlocked ? t('mobileDashboard.setup.pushBlockedDesc', 'Notifications are blocked in the browser settings.') : t('mobileDashboard.setup.pushDescription', 'Stay on top of uploads and quota alerts.'), badge: pushBlocked ? t('mobileDashboard.setup.pushBlocked', 'Blocked') : t('mobileDashboard.setup.pushOff', 'Off'), badgeTone: 'warning', actionLabel: pushBlocked ? t('mobileDashboard.setup.pushSettings', 'Open settings') : t('mobileDashboard.setup.pushAction', 'Enable push'), action: pushBlocked ? onOpenSettings : () => void pushState.enable(), actionDisabled: pushState.loading, }); } if (devicePermissions.storage === 'available') { items.push({ key: 'storage', icon: ShieldCheck, title: t('mobileDashboard.setup.storageTitle', 'Protect offline data'), description: t('mobileDashboard.setup.storageDescription', 'Keep cached uploads available when you go offline.'), badge: t('mobileDashboard.setup.storageUnprotected', 'Not protected'), badgeTone: 'warning', actionLabel: t('mobileDashboard.setup.storageAction', 'Protect now'), action: () => void devicePermissions.requestPersistentStorage(), actionDisabled: devicePermissions.loading, }); } if (items.length === 0) { return null; } return ( {t('mobileDashboard.setupTitle', 'Set up this device')} {t('mobileDashboard.setupSubtitle', 'Finish these steps for the best mobile experience.')} {items.map((item) => ( {item.title} {item.description} {item.badge} {item.actionLabel && item.action ? ( {item.actionLabel} ) : null} ))} ); } function OnboardingEmptyState({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) { const { t } = useTranslation('management'); const navigate = useNavigate(); const { textStrong, muted, border, accentSoft, accentStrong, surfaceMuted, primary, shadow } = useAdminTheme(); const text = textStrong; const accent = primary; const stepBg = surfaceMuted; const stepBorder = border; const supportBg = surfaceMuted; const supportBorder = border; const steps = [ t('mobileDashboard.emptyStepDetails', 'Add name & date'), t('mobileDashboard.emptyStepQr', 'Share your QR poster'), t('mobileDashboard.emptyStepReview', 'Review first uploads'), ]; const previews = [ { icon: QrCode, title: t('mobileDashboard.emptyPreviewQr', 'Share QR poster'), desc: t('mobileDashboard.emptyPreviewQrDesc', 'Print-ready codes for guests and crew.'), }, { icon: ImageIcon, title: t('mobileDashboard.emptyPreviewGallery', 'Gallery & highlights'), desc: t('mobileDashboard.emptyPreviewGalleryDesc', 'Moderate uploads, feature the best moments.'), }, { icon: ListTodo, title: t('mobileDashboard.emptyPreviewTasks', 'Tasks & challenges'), desc: t('mobileDashboard.emptyPreviewTasksDesc', 'Guide guests with playful prompts.'), }, ]; return ( {t('mobileDashboard.emptyBadge', 'Welcome aboard')} {t('mobileDashboard.emptyTitle', "Welcome! Let's launch your first event")} {t('mobileDashboard.emptyBody', 'Print a QR, collect uploads, and start moderating in minutes.')} navigate(adminPath('/mobile/events/new'))} /> navigate(ADMIN_WELCOME_BASE_PATH)} /> {t('mobileDashboard.emptyChecklistTitle', 'Quick steps to go live')} {t('mobileDashboard.emptyChecklistProgress', '{{done}}/{{total}} steps', { done: 0, total: steps.length })} {steps.map((label) => ( {label} ))} {t('mobileDashboard.emptyPreviewTitle', "Here's what awaits")} {previews.map(({ icon: Icon, title, desc }) => ( {title} {desc} ))} {t('mobileDashboard.emptySupportTitle', 'Need help?')} {t('mobileDashboard.emptySupportBody', 'We are here if you need a hand getting started.')} {t('mobileDashboard.emptySupportDocs', 'Docs: Getting started')} {t('mobileDashboard.emptySupportEmail', 'Email support')} ); } function EventPickerList({ events, locale, onPick, navigateOnSelect = true, }: { events: TenantEvent[]; locale: string; onPick?: (event: TenantEvent) => void; navigateOnSelect?: boolean; }) { const { t } = useTranslation('management'); const { textStrong, muted, border } = useAdminTheme(); const text = textStrong; 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); onPick?.(event); if (navigateOnSelect && 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 EventSwitcherSheet({ open, onClose, events, locale, }: { open: boolean; onClose: () => void; events: TenantEvent[]; locale: string; }) { const { t } = useTranslation('management'); return ( ); } function resolveLocation(event: TenantEvent | null, t: (key: string, fallback: string) => string): string { if (!event) return t('events.detail.locationPlaceholder', 'Location'); const settings = (event.settings ?? {}) as Record; const candidate = (settings.location as string | undefined) ?? (settings.address as string | undefined) ?? (settings.city as string | undefined); if (candidate && candidate.trim()) { return candidate; } return t('events.detail.locationPlaceholder', 'Location'); } function EventHeaderCard({ event, locale, canSwitch, onSwitch, onEdit, }: { event: TenantEvent | null; locale: string; canSwitch: boolean; onSwitch: () => void; onEdit: () => void; }) { const { t } = useTranslation('management'); const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme(); if (!event) { return null; } const dateLabel = formatEventDate(event.event_date, locale) ?? t('events.detail.dateTbd', 'Date tbd'); const locationLabel = resolveLocation(event, t); return ( {canSwitch ? ( {resolveEventDisplayName(event)} ) : ( {resolveEventDisplayName(event)} )} {event.status === 'published' ? t('events.status.published', 'Live') : t('events.status.draft', 'Draft')} {dateLabel} {locationLabel} ); } function EventManagementGrid({ event, tasksEnabled, onNavigate, }: { event: TenantEvent | null; tasksEnabled: boolean; onNavigate: (path: string) => void; }) { const { t } = useTranslation('management'); const { textStrong } = useAdminTheme(); const slug = event?.slug ?? null; const brandingAllowed = isBrandingAllowed(event ?? null); if (!event) { return null; } const tiles = [ { icon: Pencil, label: t('mobileDashboard.shortcutSettings', 'Event settings'), color: ADMIN_ACTION_COLORS.settings, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/edit`)) : undefined, disabled: !slug, }, { icon: Sparkles, label: tasksEnabled ? t('events.quick.tasks', 'Tasks & Checklists') : `${t('events.quick.tasks', 'Tasks & Checklists')} (${t('common:states.disabled', 'Disabled')})`, color: ADMIN_ACTION_COLORS.tasks, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/tasks`)) : undefined, disabled: !tasksEnabled || !slug, }, { icon: QrCode, label: t('events.quick.qr', 'QR Code Layouts'), color: ADMIN_ACTION_COLORS.qr, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/qr`)) : undefined, disabled: !slug, }, { icon: ImageIcon, label: t('events.quick.images', 'Image Management'), color: ADMIN_ACTION_COLORS.images, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/control-room`)) : undefined, disabled: !slug, }, { icon: Tv, label: t('events.quick.controlRoom', 'Moderation & Live Show'), color: ADMIN_ACTION_COLORS.liveShow, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/control-room`)) : undefined, disabled: !slug, }, { icon: Settings, label: t('events.quick.liveShowSettings', 'Live Show settings'), color: ADMIN_ACTION_COLORS.liveShowSettings, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/live-show/settings`)) : undefined, disabled: !slug, }, { icon: Users, label: t('events.quick.guests', 'Guest Management'), color: ADMIN_ACTION_COLORS.guests, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/members`)) : undefined, disabled: !slug, }, { icon: Megaphone, label: t('events.quick.guestMessages', 'Guest messages'), color: ADMIN_ACTION_COLORS.guestMessages, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/guest-notifications`)) : undefined, disabled: !slug, }, { icon: Layout, label: t('events.quick.branding', 'Branding & Theme'), color: ADMIN_ACTION_COLORS.branding, onPress: slug && brandingAllowed ? () => onNavigate(adminPath(`/mobile/events/${slug}/branding`)) : undefined, disabled: !brandingAllowed || !slug, }, { icon: Camera, label: t('events.quick.photobooth', 'Photobooth'), color: ADMIN_ACTION_COLORS.photobooth, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/photobooth`)) : undefined, disabled: !slug, }, { icon: TrendingUp, label: t('mobileDashboard.shortcutAnalytics', 'Analytics'), color: ADMIN_ACTION_COLORS.analytics, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/analytics`)) : undefined, disabled: !slug, }, ]; if (event && isPastEvent(event.event_date)) { tiles.push({ icon: Sparkles, label: t('events.quick.recap', 'Recap & Archive'), color: ADMIN_ACTION_COLORS.recap, onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/recap`)) : undefined, disabled: !slug, }); } return ( {t('events.detail.managementTitle', 'Event management')} {tiles.map((tile, index) => ( ))} ); } function KpiStrip({ event, stats, loading, locale, tasksEnabled, }: { event: TenantEvent | null; stats: EventStats | null | undefined; loading: boolean; locale: string; tasksEnabled: boolean; }) { const { t } = useTranslation('management'); const { textStrong, muted } = useAdminTheme(); const text = textStrong; if (!event) return null; const kpis = [ { 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, }, ]; if (tasksEnabled) { kpis.unshift({ label: t('mobileDashboard.kpiTasks', 'Open tasks'), value: event.tasks_count ?? '—', icon: ListTodo, }); } 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, tasksEnabled }: { event: TenantEvent | null; stats: EventStats | null | undefined; tasksEnabled: boolean }) { const { t } = useTranslation('management'); const { textStrong, warningBg, warningBorder, warningText } = useAdminTheme(); const text = textStrong; 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 (tasksEnabled && 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} ))} ); }