|
|
|
|
@@ -11,13 +11,15 @@ 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 } from '../api';
|
|
|
|
|
import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent } from '../api';
|
|
|
|
|
import { formatEventDate } from '../lib/events';
|
|
|
|
|
import { useAuth } from '../auth/context';
|
|
|
|
|
import { useAdminTheme } from './theme';
|
|
|
|
|
@@ -26,6 +28,7 @@ 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';
|
|
|
|
|
|
|
|
|
|
// --- HELPERS ---
|
|
|
|
|
|
|
|
|
|
@@ -115,7 +118,7 @@ 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 } = useEventContext();
|
|
|
|
|
const { events, activeEvent, hasEvents, isLoading, selectEvent, refetch } = useEventContext();
|
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
const theme = useAdminTheme();
|
|
|
|
|
const isMember = user?.role === 'member';
|
|
|
|
|
@@ -216,6 +219,8 @@ export default function MobileDashboardPage() {
|
|
|
|
|
navigate={navigate}
|
|
|
|
|
readiness={readiness}
|
|
|
|
|
variant="embedded"
|
|
|
|
|
canManage={canManageEvents}
|
|
|
|
|
onUpdated={refetch}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 2. PULSE STRIP */}
|
|
|
|
|
@@ -261,16 +266,31 @@ function getEventPhase(event: TenantEvent): EventPhase {
|
|
|
|
|
return 'setup';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'default' }: any) {
|
|
|
|
|
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 [isAutoApproving, setIsAutoApproving] = React.useState(false);
|
|
|
|
|
const [published, setPublished] = React.useState(false);
|
|
|
|
|
const [autoApproveUploads, setAutoApproveUploads] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// Header Row
|
|
|
|
|
const Header = () => (
|
|
|
|
|
@@ -332,6 +352,62 @@ function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
const visibility = (event.settings?.guest_upload_visibility as string | undefined) ?? 'review';
|
|
|
|
|
setAutoApproveUploads(visibility === 'immediate');
|
|
|
|
|
}, [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],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleAutoApproveChange = React.useCallback(
|
|
|
|
|
async (checked: boolean) => {
|
|
|
|
|
if (!event.slug) return;
|
|
|
|
|
const previous = autoApproveUploads;
|
|
|
|
|
setAutoApproveUploads(checked);
|
|
|
|
|
setIsAutoApproving(true);
|
|
|
|
|
try {
|
|
|
|
|
const nextSettings = {
|
|
|
|
|
...(event.settings ?? {}),
|
|
|
|
|
guest_upload_visibility: checked ? 'immediate' : 'review',
|
|
|
|
|
};
|
|
|
|
|
await updateEvent(event.slug, { settings: nextSettings });
|
|
|
|
|
onUpdated?.();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setAutoApproveUploads(previous);
|
|
|
|
|
toast.error(
|
|
|
|
|
getApiErrorMessage(err, t('eventForm.errors.saveFailed', 'Event could not be saved.'))
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsAutoApproving(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[autoApproveUploads, event.settings, event.slug, onUpdated, t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (phase === 'post') {
|
|
|
|
|
return (
|
|
|
|
|
@@ -388,12 +464,61 @@ function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SETUP
|
|
|
|
|
const nextStep = readiness.nextStep;
|
|
|
|
|
const showNextStep = Boolean(nextStep);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<YStack space="$2">
|
|
|
|
|
<Header />
|
|
|
|
|
{showQuickControls ? (
|
|
|
|
|
<XStack
|
|
|
|
|
alignItems="center"
|
|
|
|
|
justifyContent="space-between"
|
|
|
|
|
borderWidth={1}
|
|
|
|
|
borderColor={theme.border}
|
|
|
|
|
borderRadius={12}
|
|
|
|
|
paddingHorizontal="$2.5"
|
|
|
|
|
paddingVertical="$2"
|
|
|
|
|
>
|
|
|
|
|
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${event.slug}/edit`))}>
|
|
|
|
|
<XStack alignItems="center" space="$1.5">
|
|
|
|
|
<Settings size={16} color={theme.primary} />
|
|
|
|
|
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
|
|
|
|
{t('dashboard:readiness.quickSettings', 'Event settings')}
|
|
|
|
|
</Text>
|
|
|
|
|
</XStack>
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
|
|
|
|
<XStack alignItems="center" space="$3">
|
|
|
|
|
<YStack alignItems="center" space="$1">
|
|
|
|
|
<Text fontSize="$xs" color={theme.muted} textTransform="uppercase" letterSpacing={0.8}>
|
|
|
|
|
{t('dashboard:readiness.publishToggle', 'Live')}
|
|
|
|
|
</Text>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={published}
|
|
|
|
|
onCheckedChange={(checked) => handlePublishChange(Boolean(checked))}
|
|
|
|
|
size="$2"
|
|
|
|
|
disabled={isPublishing}
|
|
|
|
|
aria-label={t('eventForm.fields.publish.label', 'Publish immediately')}
|
|
|
|
|
>
|
|
|
|
|
<Switch.Thumb />
|
|
|
|
|
</Switch>
|
|
|
|
|
</YStack>
|
|
|
|
|
|
|
|
|
|
<YStack alignItems="center" space="$1">
|
|
|
|
|
<Text fontSize="$xs" color={theme.muted} textTransform="uppercase" letterSpacing={0.8}>
|
|
|
|
|
{t('dashboard:readiness.autoApproveToggle', 'Auto')}
|
|
|
|
|
</Text>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={autoApproveUploads}
|
|
|
|
|
onCheckedChange={(checked) => handleAutoApproveChange(Boolean(checked))}
|
|
|
|
|
size="$2"
|
|
|
|
|
disabled={isAutoApproving}
|
|
|
|
|
aria-label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')}
|
|
|
|
|
>
|
|
|
|
|
<Switch.Thumb />
|
|
|
|
|
</Switch>
|
|
|
|
|
</YStack>
|
|
|
|
|
</XStack>
|
|
|
|
|
</XStack>
|
|
|
|
|
) : null}
|
|
|
|
|
<DashboardCard variant={cardVariant} padding={cardPadding}>
|
|
|
|
|
<YStack space={isEmbedded ? '$2.5' : '$3'}>
|
|
|
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
|
|
|
@@ -413,7 +538,7 @@ function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'd
|
|
|
|
|
</YStack>
|
|
|
|
|
</XStack>
|
|
|
|
|
|
|
|
|
|
{showNextStep ? (
|
|
|
|
|
{showNextStep && nextStep ? (
|
|
|
|
|
<YStack space="$2">
|
|
|
|
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
|
|
|
|
{t('dashboard:readiness.nextStepTitle', 'Next step')}
|
|
|
|
|
@@ -453,12 +578,16 @@ function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'd
|
|
|
|
|
</YStack>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
|
|
|
<SetupChecklist
|
|
|
|
|
steps={readiness.steps}
|
|
|
|
|
title={t('dashboard:readiness.title', 'Bereit für den Eventstart')}
|
|
|
|
|
variant="inline"
|
|
|
|
|
/>
|
|
|
|
|
{showChecklist ? (
|
|
|
|
|
<>
|
|
|
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
|
|
|
<SetupChecklist
|
|
|
|
|
steps={readiness.steps}
|
|
|
|
|
title={t('dashboard:readiness.title', 'Bereit für den Eventstart')}
|
|
|
|
|
variant="inline"
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
</YStack>
|
|
|
|
|
</DashboardCard>
|
|
|
|
|
</YStack>
|
|
|
|
|
|