import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Heart, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; import { Button } from '@tamagui/button'; import { Card } from '@tamagui/card'; import { YGroup } from '@tamagui/group'; import { ListItem } from '@tamagui/list-item'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { Image } from '@tamagui/image'; import { Switch } from '@tamagui/switch'; import { Separator } from 'tamagui'; import { isSameDay, isPast, parseISO, differenceInDays, startOfDay } from 'date-fns'; import toast from 'react-hot-toast'; import { MobileShell } from './components/MobileShell'; import { ADMIN_EVENTS_PATH, adminPath } from '../constants'; import { useEventContext } from '../context/EventContext'; import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent, getLiveShowQueue } from '../api'; import { formatEventDate } from '../lib/events'; import { useAuth } from '../auth/context'; import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme'; import { buildLimitWarnings } from '../lib/limitWarnings'; import { withAlpha } from './components/colors'; import { useEventReadiness } from './hooks/useEventReadiness'; import { SetupChecklist } from './components/SetupChecklist'; import { KpiStrip, PillBadge } from './components/Primitives'; import { getApiErrorMessage } from '../lib/apiError'; import { ContextHelpLink } from './components/ContextHelpLink'; // --- HELPERS --- function translateLimits(t: any) { return (key: string, options?: any) => t(`management:limits.${key}`, key, options); } // --- TAMAGUI-ALIGNED PRIMITIVES --- function DashboardCard({ children, style, variant = 'default', ...rest }: React.ComponentProps & { variant?: 'default' | 'embedded' }) { const theme = useAdminTheme(); const isEmbedded = variant === 'embedded'; const cardSurface = isEmbedded ? (theme.surfaceMuted ?? theme.glassSurface ?? theme.surface) : (theme.glassSurfaceStrong ?? theme.surface); const cardBorder = theme.glassBorder ?? theme.border; const cardShadow = theme.glassShadow ?? theme.shadow; return ( {children} ); } function SectionHeader({ title, subtitle, action, showSeparator = true, compact = false, }: { title: string; subtitle?: string; action?: React.ReactNode; showSeparator?: boolean; compact?: boolean; }) { const theme = useAdminTheme(); const titleSize = compact ? '$md' : '$lg'; const subtitleSize = compact ? '$xs' : '$sm'; const spacing = compact ? '$1' : '$1.5'; return ( {title} {action ?? null} {subtitle ? ( {subtitle} ) : null} {showSeparator ? : null} ); } function StatusBadge({ status }: { status: string }) { const { t } = useTranslation('management'); type StatusTone = 'success' | 'warning' | 'danger' | 'muted'; const statuses: Record = { published: { tone: 'success', label: t('events.status.published', 'Live') }, draft: { tone: 'warning', label: t('events.status.draft', 'Draft') }, archived: { tone: 'muted', label: t('events.status.archived', 'Archived') }, }; const config = statuses[status] ?? { tone: 'muted', label: status }; return {config.label}; } // --- MAIN PAGE COMPONENT --- export default function MobileDashboardPage() { const navigate = useNavigate(); const { slug: slugParam } = useParams<{ slug?: string }>(); const { t, i18n } = useTranslation(['management', 'dashboard', 'mobile']); const { events, activeEvent, hasEvents, isLoading, selectEvent, refetch } = useEventContext(); const { user } = useAuth(); const theme = useAdminTheme(); const isMember = user?.role === 'member'; // --- LOGIC --- const memberPermissions = React.useMemo(() => { if (!isMember) return ['*']; return Array.isArray(activeEvent?.member_permissions) ? activeEvent?.member_permissions ?? [] : []; }, [activeEvent?.member_permissions, isMember]); function allowPermission(permissions: string[], permission: string): boolean { if (permissions.includes('*') || permissions.includes(permission)) return true; if (permission.includes(':')) { const [prefix] = permission.split(':'); return permissions.includes(`${prefix}:*`); } return false; } const canManageEvents = React.useMemo(() => allowPermission(memberPermissions, 'events:manage'), [memberPermissions]); const { data: stats } = useQuery({ queryKey: ['mobile', 'dashboard', 'stats', activeEvent?.slug], enabled: Boolean(activeEvent?.slug), queryFn: async () => { if (!activeEvent?.slug) return null; return await getEventStats(activeEvent.slug); }, }); const { data: photoData } = useQuery({ queryKey: ['mobile', 'dashboard', 'recent-photos', activeEvent?.slug], enabled: Boolean(activeEvent?.slug), queryFn: async () => { if (!activeEvent?.slug) return { photos: [] }; return await getEventPhotos(activeEvent.slug, { perPage: 6, sort: 'desc' }); }, }); const { data: liveShowApprovedCount = 0 } = useQuery({ queryKey: ['mobile', 'dashboard', 'live-show-approved', activeEvent?.slug], enabled: Boolean(activeEvent?.slug), queryFn: async () => { if (!activeEvent?.slug) return 0; const result = await getLiveShowQueue(activeEvent.slug, { liveStatus: 'approved', perPage: 1 }); if (result.photos.length === 0) { return 0; } return result.meta.total ?? result.photos.length; }, }); const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; React.useEffect(() => { if (!slugParam || slugParam === activeEvent?.slug) return; selectEvent(slugParam); }, [activeEvent?.slug, selectEvent, slugParam]); const shouldRedirectToSelector = !isLoading && !activeEvent && !slugParam; React.useEffect(() => { if (!shouldRedirectToSelector) return; navigate(ADMIN_EVENTS_PATH, { replace: true }); }, [navigate, shouldRedirectToSelector]); // --- RENDER --- if (shouldRedirectToSelector) { return null; } if (isLoading) { return ( ); } if (!hasEvents && !events.length) { return ( navigate(adminPath('/mobile/events/new'))} /> ); } // Calculate Readiness const readiness = useEventReadiness(activeEvent, t as any); const phase = activeEvent ? getEventPhase(activeEvent) : 'setup'; const isCompleted = phase === 'post'; return ( } /> {/* 1. LIFECYCLE HERO */} {/* 2. PULSE STRIP */} {/* 3. ALERTS */} {/* 4. UNIFIED COMMAND GRID */} {/* 5. RECENT PHOTOS */} ); } // --- SUB COMPONENTS --- type EventPhase = 'setup' | 'live' | 'post'; function getEventPhase(event: TenantEvent): EventPhase { if (event.status === 'archived') return 'post'; if (!event.event_date) return 'setup'; const today = startOfDay(new Date()); const eventDate = parseISO(event.event_date); if (isSameDay(today, eventDate)) return 'live'; if (isPast(eventDate)) return 'post'; return 'setup'; } function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'default', canManage = false, onUpdated, }: any) { const theme = useAdminTheme(); const { t } = useTranslation(['management', 'dashboard']); const isEmbedded = variant === 'embedded'; const cardVariant = isEmbedded ? 'embedded' : 'default'; const cardPadding = isEmbedded ? '$3' : '$3.5'; const [isPublishing, setIsPublishing] = React.useState(false); const [published, setPublished] = React.useState(() => event?.status === 'published'); if (!event) return null; const phase = getEventPhase(event); const pendingPhotos = stats?.pending_photos ?? event.pending_photo_count ?? 0; const isPostEvent = phase === 'post'; const showQuickControls = canManage && !isPostEvent; const displayStatus = event.status === 'archived' ? 'archived' : published ? 'published' : 'draft'; // Header Row const Header = () => ( {formatEventDate(event.event_date, locale)} ); if (phase === 'live') { return (
{t('dashboard:liveNow.status', 'Happening Now')} {pendingPhotos > 0 ? `${pendingPhotos} ${t('management:photos.filters.pending', 'Pending')}` : t('dashboard:liveNow.description', 'Event is Running')} {pendingPhotos > 0 ? ( ) : null} ); } const daysToGo = event.event_date ? differenceInDays(parseISO(event.event_date), new Date()) : 0; const isSetupPhase = phase === 'setup'; const nextStep = readiness.nextStep; const showNextStep = isSetupPhase && Boolean(nextStep); const showChecklist = isSetupPhase; React.useEffect(() => { setPublished(event.status === 'published'); }, [event.settings?.guest_upload_visibility, event.status]); const handlePublishChange = React.useCallback( async (checked: boolean) => { if (!event.slug) return; const previous = published; setPublished(checked); setIsPublishing(true); try { await updateEvent(event.slug, { status: checked ? 'published' : 'draft' }); onUpdated?.(); } catch (err) { setPublished(previous); toast.error( getApiErrorMessage(err, t('eventForm.errors.saveFailed', 'Event could not be saved.')) ); } finally { setIsPublishing(false); } }, [event.slug, onUpdated, published, t], ); if (phase === 'post') { return (
{t('events.recap.completedTitle', 'Event completed')} {t('events.recap.galleryOpen', 'Gallery online')} ); } // SETUP return (
{showQuickControls ? ( navigate(adminPath(`/mobile/events/${event.slug}/edit`))}> {t('dashboard:readiness.quickSettings', 'Event settings')} {t('dashboard:readiness.publishToggle', 'Live')} handlePublishChange(Boolean(checked))} size="$2" disabled={isPublishing} aria-label={t('eventForm.fields.publish.label', 'Publish immediately')} > ) : null} {t('dashboard:upcoming.status.planning', 'Countdown')} {daysToGo}{' '} {t('management:galleryStatus.daysLabel', 'days')} {showNextStep && nextStep ? ( {t('dashboard:readiness.nextStepTitle', 'Next step')} navigate(adminPath(nextStep.targetPath))} title={ {nextStep.label} } subTitle={ nextStep.description ? ( {nextStep.description} ) : undefined } iconAfter={ {nextStep.ctaLabel} } /> ) : null} {showChecklist ? ( <> ) : null} ); } function PulseStrip({ event, stats, liveShowApprovedCount }: any) { const theme = useAdminTheme(); const { t } = useTranslation(['management', 'dashboard']); const uploadCount = stats?.uploads_total ?? event?.photo_count ?? 0; const guestCount = event?.active_invites_count ?? event?.total_invites_count ?? 0; const pendingCount = stats?.pending_photos ?? event?.pending_photo_count ?? 0; const likesTotal = stats?.likes_total ?? stats?.likes ?? 0; const showPending = (event?.settings?.guest_upload_visibility ?? 'review') !== 'immediate'; const approvedCount = typeof liveShowApprovedCount === 'number' ? liveShowApprovedCount : 0; const items = [ { icon: ImageIcon, value: uploadCount, label: t('management:events.list.stats.photos', 'Photos'), color: theme.primary, }, { icon: Users, value: guestCount, label: t('management:events.list.stats.guests', 'Guests'), tone: 'neutral' as const, }, { icon: Tv, value: approvedCount, label: t('dashboard:kpis.liveShowApproved', 'Live Show approved'), color: ADMIN_ACTION_COLORS.liveShow, }, showPending ? { icon: ShieldCheck, value: pendingCount, label: t('management:photos.filters.pending', 'Pending'), note: pendingCount > 0 ? t('management:common.actionNeeded', 'Review') : undefined, color: pendingCount > 0 ? '#8B5CF6' : theme.textMuted, } : { icon: Heart, value: likesTotal, label: t('dashboard:kpis.likesTotal', 'Likes total'), color: ADMIN_ACTION_COLORS.images, }, ]; return ; } function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) { const theme = useAdminTheme(); const { t } = useTranslation(['management', 'dashboard']); const slug = event?.slug; if (!slug) return null; type ToolItem = { label: string; icon: any; path: string; color?: string }; const experienceItems: ToolItem[] = [ { label: t('management:photos.gallery.title', 'Photos'), icon: ImageIcon, path: `/mobile/events/${slug}/control-room`, color: theme.primary }, !isCompleted ? { label: t('management:events.quick.liveShowSettings', 'Slide Show'), icon: Tv, path: `/mobile/events/${slug}/live-show/settings`, color: '#F59E0B' } : null, !isCompleted ? { label: t('events.tasks.badge', 'Photo tasks'), icon: ListTodo, path: `/mobile/events/${slug}/tasks`, color: theme.accent } : null, !isCompleted ? { label: t('management:events.quick.photobooth', 'Photobooth'), icon: Camera, path: `/mobile/events/${slug}/photobooth`, color: '#8B5CF6' } : null, ].filter(Boolean) as ToolItem[]; const operationsItems: ToolItem[] = [ !isCompleted ? { label: t('management:invites.badge', 'QR Codes'), icon: QrCode, path: `/mobile/events/${slug}/qr`, color: '#10B981' } : null, { label: t('management:events.quick.guests', 'Guests'), icon: Users, path: `/mobile/events/${slug}/members`, color: ADMIN_ACTION_COLORS.guests }, !isCompleted ? { label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: ADMIN_ACTION_COLORS.guestMessages } : null, !isCompleted ? { label: t('events.branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: ADMIN_ACTION_COLORS.branding } : null, ].filter(Boolean) as ToolItem[]; const adminItems: ToolItem[] = [ { label: t('management:mobileDashboard.shortcutAnalytics', 'Analytics'), icon: TrendingUp, path: `/mobile/events/${slug}/analytics`, color: ADMIN_ACTION_COLORS.analytics }, !isCompleted ? { label: t('events.recap.exportTitleShort', 'Exports'), icon: Download, path: `/mobile/exports`, color: ADMIN_ACTION_COLORS.recap } : null, { label: t('management:mobileProfile.settings', 'Settings'), icon: Settings, path: `/mobile/events/${slug}/edit`, color: ADMIN_ACTION_COLORS.settings }, ].filter(Boolean) as ToolItem[]; const sections = [ { title: t('management:mobileDashboard.quickActionsTitle', 'Experience'), items: experienceItems, }, { title: t('management:events.quickActions.title', 'Operations'), items: operationsItems, }, { title: t('management:settings.hero.badge', 'Admin'), items: adminItems, } ].filter((section) => section.items.length > 0); return ( {sections.map((section) => ( {section.title} {section.items.map((item) => { const iconColor = item.color || theme.textStrong; return ( navigate(adminPath(item.path))} title={ {item.label} } iconAfter={} /> ); })} ))} ); } function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]; navigate: any; slug?: string }) { const theme = useAdminTheme(); const { t } = useTranslation('management'); if (!photos.length || !slug) return null; return ( {t('photos.recentTitle', 'Latest Uploads')} {photos.map((photo) => ( navigate(adminPath(`/mobile/events/${slug}/control-room`))}> {photo.thumbnail_url ? ( ) : ( )} ))} ); } function AlertsSection({ event, stats, t }: any) { const theme = useAdminTheme(); const limitWarnings = buildLimitWarnings(event?.limits ?? null, translateLimits(t)); if (!limitWarnings.length) return null; return ( {t('management:alertsTitle', 'Alerts')} {limitWarnings.map((w: any, idx: number) => { const isDanger = w.tone === 'danger'; const bg = isDanger ? theme.dangerBg : theme.warningBg; const border = isDanger ? theme.dangerText : theme.warningBorder; const text = isDanger ? theme.dangerText : theme.warningText; const Icon = isDanger ? AlertCircle : Bell; return ( {w.message} ); })} ); } function EmptyState({ canManage, onCreate }: any) { const theme = useAdminTheme(); const { t } = useTranslation(['management', 'mobile']); return ( {t('mobile:header.appName', 'Event Admin')} {t('mobile:header.noEventsBody', 'Create your first event.')} {canManage ? ( ) : null} ); }