diff --git a/resources/js/admin/i18n/locales/de/dashboard.json b/resources/js/admin/i18n/locales/de/dashboard.json
index 191c834..b3dff7e 100644
--- a/resources/js/admin/i18n/locales/de/dashboard.json
+++ b/resources/js/admin/i18n/locales/de/dashboard.json
@@ -51,6 +51,9 @@
"title": "Bereit für den Eventstart",
"description": "Erledige diese Schritte, damit Gäste ohne Reibung loslegen können.",
"nextStepTitle": "Nächster Schritt",
+ "quickSettings": "Event-Einstellungen",
+ "publishToggle": "Live",
+ "autoApproveToggle": "Auto",
"pending": "Noch offen",
"complete": "Erledigt",
"items": {
diff --git a/resources/js/admin/i18n/locales/en/dashboard.json b/resources/js/admin/i18n/locales/en/dashboard.json
index 92d0c93..64ef0dd 100644
--- a/resources/js/admin/i18n/locales/en/dashboard.json
+++ b/resources/js/admin/i18n/locales/en/dashboard.json
@@ -51,6 +51,9 @@
"title": "Ready for event day",
"description": "Complete these steps so guests can join without friction.",
"nextStepTitle": "Next step",
+ "quickSettings": "Event settings",
+ "publishToggle": "Live",
+ "autoApproveToggle": "Auto",
"pending": "Pending",
"complete": "Done",
"items": {
diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx
index 963d188..a0295f4 100644
--- a/resources/js/admin/mobile/DashboardPage.tsx
+++ b/resources/js/admin/mobile/DashboardPage.tsx
@@ -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 (
+ {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')}
+ >
+
+
+
+
+
+
+ {t('dashboard:readiness.autoApproveToggle', 'Auto')}
+
+ handleAutoApproveChange(Boolean(checked))}
+ size="$2"
+ disabled={isAutoApproving}
+ aria-label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')}
+ >
+
+
+
+
+
+ ) : null}
@@ -413,7 +538,7 @@ function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'd
- {showNextStep ? (
+ {showNextStep && nextStep ? (
{t('dashboard:readiness.nextStepTitle', 'Next step')}
@@ -453,12 +578,16 @@ function LifecycleHero({ event, stats, locale, navigate, readiness, variant = 'd
) : null}
-
-
+ {showChecklist ? (
+ <>
+
+
+ >
+ ) : null}
diff --git a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx
index 21dfdb8..821a181 100644
--- a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx
+++ b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx
@@ -230,6 +230,27 @@ vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => {children},
}));
+vi.mock('@tamagui/switch', () => ({
+ Switch: Object.assign(
+ ({
+ children,
+ checked,
+ onCheckedChange,
+ }: {
+ children: React.ReactNode;
+ checked?: boolean;
+ onCheckedChange?: (checked: boolean) => void;
+ }) => (
+
+ ),
+ {
+ Thumb: ({ children }: { children?: React.ReactNode }) => {children},
+ },
+ ),
+}));
+
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (