import React from 'react'; import { useNavigate } from 'react-router-dom'; import { CalendarDays, MapPin, Plus, Search, Camera, Users, Sparkles, ChevronRight } from 'lucide-react'; import { Card } from '@tamagui/card'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { ScrollView } from '@tamagui/scroll-view'; import { ToggleGroup } from '@tamagui/toggle-group'; import { ListItem } from '@tamagui/list-item'; import { YGroup } from '@tamagui/group'; import { useTranslation } from 'react-i18next'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { PillBadge, CTAButton, FloatingActionButton, SkeletonCard } from './components/Primitives'; import { MobileInput } from './components/FormControls'; import { getEvents, getTenantPackagesOverview, TenantEvent, type TenantPackageSummary } from '../api'; import { adminPath } from '../constants'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { useBackNavigation } from './hooks/useBackNavigation'; import { buildEventStatusCounts, filterEventsByStatus, resolveEventStatusKey, type EventStatusKey } from './lib/eventFilters'; import { buildEventListStats } from './lib/eventListStats'; import { useAdminTheme } from './theme'; import { useAuth } from '../auth/context'; export default function MobileEventsPage() { const { t } = useTranslation('management'); const navigate = useNavigate(); const { user } = useAuth(); const isMember = user?.role === 'member'; const [events, setEvents] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [query, setQuery] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState('all'); const [packagesOverview, setPackagesOverview] = React.useState<{ packages: TenantPackageSummary[]; activePackage: TenantPackageSummary | null; } | null>(null); const [packagesLoading, setPackagesLoading] = React.useState(false); const searchRef = React.useRef(null); const back = useBackNavigation(); const { text, muted, subtle, border, primary, danger, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme(); React.useEffect(() => { (async () => { try { setEvents(await getEvents()); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed'))); } } finally { setLoading(false); } })(); }, [t]); React.useEffect(() => { if (isMember) { return; } let isActive = true; setPackagesLoading(true); getTenantPackagesOverview() .then((overview) => { if (isActive) { setPackagesOverview(overview); } }) .catch(() => { if (isActive) { setPackagesOverview(null); } }) .finally(() => { if (isActive) { setPackagesLoading(false); } }); return () => { isActive = false; }; }, [isMember]); const canCreateEvent = React.useMemo(() => { if (isMember || packagesLoading) { return false; } const activePackages = collectActivePackages(packagesOverview); return activePackages.some((pkg) => packageHasRemainingEvents(pkg)); }, [isMember, packagesLoading, packagesOverview]); return ( searchRef.current?.focus()} ariaLabel={t('events.detail.pickEvent')} > } > {error ? ( {error} ) : null} {t('events.list.overview.title')} setQuery(e.target.value)} placeholder={t('events.detail.pickEvent')} compact /> {t('events.list.subtitle')} {loading ? ( {Array.from({ length: 3 }).map((_, idx) => ( ))} ) : events.length === 0 ? ( {t('events.list.title')} {t('events.list.overview.empty')} {!isMember ? ( navigate(adminPath('/events/new'))} /> ) : null} ) : ( navigate(adminPath(`/mobile/events/${slug}`))} onEdit={!isMember ? (slug) => navigate(adminPath(`/mobile/events/${slug}/edit`)) : undefined} /> )} {canCreateEvent ? ( navigate(adminPath('/mobile/events/new'))} /> ) : null} ); } function EventsList({ events, query, statusFilter, onStatusChange, onOpen, onEdit, }: { events: TenantEvent[]; query: string; statusFilter: EventStatusKey; onStatusChange: (value: EventStatusKey) => void; onOpen: (slug: string) => void; onEdit?: (slug: string) => void; }) { const { t } = useTranslation('management'); const { text, muted, subtle, border, primary, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme(); const activeBg = accentSoft; const activeBorder = accent; const statusCounts = React.useMemo(() => buildEventStatusCounts(events), [events]); const filteredByStatus = React.useMemo( () => filterEventsByStatus(events, statusFilter), [events, statusFilter] ); const filteredEvents = React.useMemo(() => { if (!query.trim()) return filteredByStatus; const needle = query.toLowerCase(); return filteredByStatus.filter((event) => { const name = resolveEventSearchName(event.name, t); const location = resolveLocation(event, t); const hay = `${name} ${location}`.toLowerCase(); return hay.includes(needle); }); }, [filteredByStatus, query, t]); const filters: Array<{ key: EventStatusKey; label: string; count: number }> = [ { key: 'all', label: t('events.list.filters.all'), count: statusCounts.all }, { key: 'upcoming', label: t('events.list.filters.upcoming'), count: statusCounts.upcoming }, { key: 'draft', label: t('events.list.filters.draft'), count: statusCounts.draft }, { key: 'past', label: t('events.list.filters.past'), count: statusCounts.past }, ]; return ( {filteredEvents.length === 0 ? ( {t('events.list.empty.filtered')} {t('events.list.empty.filteredHint')} onStatusChange('all')} /> ) : ( {t('events.workspace.fields.status')} {t('events.list.subtitle')} value && onStatusChange(value as EventStatusKey)} > {filters.map((filter) => { const active = filter.key === statusFilter; return ( {filter.label} {filter.count} ); })} {filteredEvents.map((event) => { const statusKey = resolveEventStatusKey(event); const statusLabel = statusKey === 'draft' ? t('events.list.filters.draft') : statusKey === 'past' ? t('events.list.filters.past') : t('events.list.filters.upcoming'); const statusTone = statusKey === 'draft' ? 'warning' : statusKey === 'past' ? 'muted' : 'success'; return ( onOpen(event.slug)} title={ } iconAfter={ {t('events.list.actions.open')} } /> ); })} )} ); } function EventListItem({ event, text, muted, subtle, primary, statusLabel, statusTone, onEdit, }: { event: TenantEvent; text: string; muted: string; subtle: string; primary: string; statusLabel: string; statusTone: 'success' | 'warning' | 'muted'; onEdit?: (slug: string) => void; }) { const { t, i18n } = useTranslation('management'); const locale = i18n.language; const stats = buildEventListStats(event); return ( {renderName(event.name, t)} {statusLabel} {onEdit ? ( onEdit(event.slug)}> {t('events.list.actions.settings', 'Settings')} ) : null} {formatDate(event.event_date, t, locale)} {resolveLocation(event, t)} ); } function EventStatChip({ icon: Icon, label, value, muted, }: { icon: typeof Camera; label: string; value: number; muted: string; }) { return ( {value} {label} ); } function renderName(name: TenantEvent['name'], t: (key: string) => string): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? t('events.placeholders.untitled'); } return t('events.placeholders.untitled'); } function collectActivePackages( overview: { packages: TenantPackageSummary[]; activePackage: TenantPackageSummary | null } | null ): TenantPackageSummary[] { if (!overview) { return []; } const packages = overview.packages ?? []; const activePackage = overview.activePackage; const activeList = packages.filter((pkg) => pkg.active); if (activePackage && !activeList.some((pkg) => pkg.id === activePackage.id) && activePackage.active) { return [activePackage, ...activeList]; } return activeList; } function toNumber(value: unknown): number | null { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim() !== '') { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return null; } function packageHasRemainingEvents(pkg: TenantPackageSummary): boolean { const remaining = toNumber(pkg.remaining_events); if (pkg.package_type !== 'reseller') { if (remaining !== null) { return remaining > 0; } const usedEvents = toNumber(pkg.used_events) ?? 0; return usedEvents < 1; } if (remaining !== null) { return remaining > 0; } const limits = (pkg.package_limits ?? {}) as Record; const limitMaxEvents = toNumber(limits.max_events_per_year); if (limitMaxEvents === null) { return false; } const usedEvents = toNumber(pkg.used_events) ?? 0; return limitMaxEvents > usedEvents; } function resolveEventSearchName(name: TenantEvent['name'], t: (key: string) => string): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { const values = Object.values(name).filter((value) => typeof value === 'string'); if (values.length > 0) { return values.join(' '); } } return t('events.placeholders.untitled'); } function resolveLocation(event: TenantEvent, t: (key: string) => string): string { 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'); } function formatDate(iso: string | null, t: (key: string) => string, locale?: string): string { const fallback = t('events.detail.dateTbd'); if (!iso) return fallback; const date = new Date(iso); if (Number.isNaN(date.getTime())) { return fallback; } return date.toLocaleDateString(locale || undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }