import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RefreshCcw } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { MobileSelect, MobileField } from './components/FormControls'; import { useEventContext } from '../context/EventContext'; import { approveAndLiveShowPhoto, approveLiveShowPhoto, clearLiveShowPhoto, getEvents, getLiveShowQueue, LiveShowQueueStatus, rejectLiveShowPhoto, TenantEvent, TenantPhoto, } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { adminPath } from '../constants'; import toast from 'react-hot-toast'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; import { useOnlineStatus } from './hooks/useOnlineStatus'; const STATUS_OPTIONS: Array<{ value: LiveShowQueueStatus; labelKey: string; fallback: string }> = [ { value: 'pending', labelKey: 'liveShowQueue.statusPending', fallback: 'Pending' }, { value: 'approved', labelKey: 'liveShowQueue.statusApproved', fallback: 'Approved' }, { value: 'rejected', labelKey: 'liveShowQueue.statusRejected', fallback: 'Rejected' }, { value: 'none', labelKey: 'liveShowQueue.statusNone', fallback: 'Not queued' }, ]; export default function MobileEventLiveShowQueuePage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const navigate = useNavigate(); const { t } = useTranslation('management'); const { activeEvent, selectEvent } = useEventContext(); const slug = slugParam ?? activeEvent?.slug ?? null; const online = useOnlineStatus(); const { textStrong, text, muted, border, danger } = useAdminTheme(); const [photos, setPhotos] = React.useState([]); const [statusFilter, setStatusFilter] = React.useState('pending'); const [page, setPage] = React.useState(1); const [hasMore, setHasMore] = React.useState(false); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [busyId, setBusyId] = React.useState(null); const [fallbackAttempted, setFallbackAttempted] = React.useState(false); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); React.useEffect(() => { if (slugParam && activeEvent?.slug !== slugParam) { selectEvent(slugParam); } }, [slugParam, activeEvent?.slug, selectEvent]); const loadQueue = React.useCallback(async () => { if (!slug) { if (!fallbackAttempted) { setFallbackAttempted(true); try { const events = await getEvents({ force: true }); const first = events[0] as TenantEvent | undefined; if (first?.slug) { selectEvent(first.slug); navigate(adminPath(`/mobile/events/${first.slug}/live-show`), { replace: true }); } } catch { // ignore } } setLoading(false); setError(t('events.errors.missingSlug', 'No event selected.')); return; } setLoading(true); setError(null); try { const result = await getLiveShowQueue(slug, { page, perPage: 20, liveStatus: statusFilter, }); setPhotos((prev) => (page === 1 ? result.photos : [...prev, ...result.photos])); const lastPage = result.meta?.last_page ?? 1; setHasMore(page < lastPage); } catch (err) { if (!isAuthError(err)) { const message = getApiErrorMessage(err, t('liveShowQueue.loadFailed', 'Live Show queue could not be loaded.')); setError(message); toast.error(message); } } finally { setLoading(false); } }, [slug, page, statusFilter, fallbackAttempted, navigate, selectEvent, t]); React.useEffect(() => { setPage(1); }, [statusFilter]); React.useEffect(() => { void loadQueue(); }, [loadQueue]); async function handleApprove(photo: TenantPhoto) { if (!slug || busyId) return; setBusyId(photo.id); try { const updated = await approveLiveShowPhoto(slug, photo.id); setPhotos((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success(t('liveShowQueue.approveSuccess', 'Photo approved for Live Show')); } catch (err) { if (!isAuthError(err)) { toast.error(t('liveShowQueue.actionFailed', 'Live Show update failed.')); } } finally { setBusyId(null); } } async function handleApproveAndLive(photo: TenantPhoto) { if (!slug || busyId) return; setBusyId(photo.id); try { const updated = await approveAndLiveShowPhoto(slug, photo.id); setPhotos((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success(t('liveShowQueue.approveAndLiveSuccess', 'Photo approved and added to Live Show')); } catch (err) { if (!isAuthError(err)) { toast.error(t('liveShowQueue.actionFailed', 'Live Show update failed.')); } } finally { setBusyId(null); } } async function handleReject(photo: TenantPhoto) { if (!slug || busyId) return; setBusyId(photo.id); try { const updated = await rejectLiveShowPhoto(slug, photo.id); setPhotos((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success(t('liveShowQueue.rejectSuccess', 'Photo removed from Live Show')); } catch (err) { if (!isAuthError(err)) { toast.error(t('liveShowQueue.actionFailed', 'Live Show update failed.')); } } finally { setBusyId(null); } } async function handleClear(photo: TenantPhoto) { if (!slug || busyId) return; setBusyId(photo.id); try { const updated = await clearLiveShowPhoto(slug, photo.id); setPhotos((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success(t('liveShowQueue.clearSuccess', 'Live Show approval removed')); } catch (err) { if (!isAuthError(err)) { toast.error(t('liveShowQueue.actionFailed', 'Live Show update failed.')); } } finally { setBusyId(null); } } function resolveStatusTone(status?: string | null): 'success' | 'warning' | 'muted' { if (status === 'approved') return 'success'; if (status === 'pending') return 'warning'; return 'muted'; } function resolveGalleryLabel(status?: string | null): string { const fallbackMap: Record = { approved: 'Gallery approved', pending: 'Gallery pending', rejected: 'Gallery rejected', hidden: 'Hidden', }; const key = status ?? 'pending'; return t(`liveShowQueue.galleryStatus.${key}`, fallbackMap[key] ?? key); } return ( loadQueue()} ariaLabel={t('common.refresh', 'Refresh')}> } > {t( 'liveShowQueue.galleryApprovedOnly', 'Gallery and Live Show approvals are separate. Pending photos can be approved here.' )} {!online ? ( {t('liveShowQueue.offlineNotice', 'You are offline. Live Show actions are disabled.')} ) : null} setStatusFilter(event.target.value as LiveShowQueueStatus)} > {STATUS_OPTIONS.map((option) => ( ))} {error ? ( {error} ) : null} {loading && page === 1 ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : photos.length === 0 ? ( {t('liveShowQueue.empty', 'No photos waiting for Live Show.')} ) : ( {photos.map((photo) => { const isBusy = busyId === photo.id; const liveStatus = photo.live_status ?? 'pending'; const galleryStatus = photo.status ?? 'pending'; const canApproveGallery = galleryStatus === 'pending'; const canApproveLiveOnly = galleryStatus === 'approved'; const canApproveLive = canApproveGallery || canApproveLiveOnly; const showApproveAction = liveStatus !== 'approved'; return ( {photo.thumbnail_url ? ( {photo.original_name ) : null} {resolveGalleryLabel(galleryStatus)} {t(`liveShowQueue.status.${liveStatus}`, liveStatus)} {photo.uploaded_at} {showApproveAction ? ( { if (canApproveGallery) { void handleApproveAndLive(photo); return; } if (canApproveLiveOnly) { void handleApprove(photo); } }} disabled={!online || !canApproveLive} loading={isBusy} tone="primary" /> ) : ( handleClear(photo)} disabled={!online} loading={isBusy} tone="ghost" /> )} {liveStatus !== 'rejected' ? ( handleReject(photo)} disabled={!online} loading={isBusy} tone="danger" /> ) : ( handleClear(photo)} disabled={!online} loading={isBusy} tone="ghost" /> )} ); })} )} {hasMore ? ( setPage((prev) => prev + 1)} disabled={loading} /> ) : null} ); }