From 9e4ea3dafbdf3bc5eeb98b97caf4e9445ebd0e24 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 16 Jan 2026 14:58:24 +0100 Subject: [PATCH] Add tasks toggle card --- .../js/admin/i18n/locales/de/management.json | 12 +++ .../js/admin/i18n/locales/en/management.json | 12 +++ resources/js/admin/mobile/EventTasksPage.tsx | 100 +++++++++++++++++- .../mobile/__tests__/EventTasksPage.test.tsx | 14 +++ 4 files changed, 137 insertions(+), 1 deletion(-) diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index f770091..bd41e0f 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -2070,6 +2070,18 @@ "tasks": { "disabledTitle": "Task-Modus ist für dieses Event aus", "disabledBody": "Gäste sehen nur den Fotofeed. Aktiviere Tasks in den Event-Einstellungen, um sie wieder anzuzeigen.", + "toggle": { + "title": "1. Aufgaben aktivieren", + "description": "Aktiviere Aufgaben, damit Gäste Challenges und Hinweise in der App sehen.", + "active": "AKTIV", + "inactive": "INAKTIV", + "onLabel": "Gäste sehen Aufgaben-Prompts", + "offLabel": "Gäste sehen nur Fotos", + "switchLabel": "Aufgaben aktiv", + "enabled": "Aufgaben aktiviert", + "disabled": "Aufgaben deaktiviert", + "permissionHint": "Du hast keine Berechtigung, Aufgaben zu ändern." + }, "title": "Tasks & Checklisten", "quickNav": "Schnellzugriff", "sections": { diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index 15992c0..f8d9157 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -2074,6 +2074,18 @@ "tasks": { "disabledTitle": "Task mode is off for this event", "disabledBody": "Guests only see the photo feed. Enable tasks in the event settings to show them again.", + "toggle": { + "title": "1. Activate tasks", + "description": "Enable tasks so guests see challenges and prompts in the app.", + "active": "ACTIVE", + "inactive": "INACTIVE", + "onLabel": "Guests see task prompts", + "offLabel": "Guest app shows photos only", + "switchLabel": "Tasks enabled", + "enabled": "Tasks activated", + "disabled": "Tasks disabled", + "permissionHint": "You do not have permission to change tasks." + }, "title": "Tasks & checklists", "quickNav": "Quick jump", "sections": { diff --git a/resources/js/admin/mobile/EventTasksPage.tsx b/resources/js/admin/mobile/EventTasksPage.tsx index acae514..9a9375b 100644 --- a/resources/js/admin/mobile/EventTasksPage.tsx +++ b/resources/js/admin/mobile/EventTasksPage.tsx @@ -12,13 +12,15 @@ import { Button } from '@tamagui/button'; import { AlertDialog } from '@tamagui/alert-dialog'; import { ScrollView } from '@tamagui/scroll-view'; import { ToggleGroup } from '@tamagui/toggle-group'; +import { Switch } from '@tamagui/switch'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; -import { MobileCard, CTAButton, SkeletonCard, FloatingActionButton } from './components/Primitives'; +import { MobileCard, CTAButton, SkeletonCard, FloatingActionButton, PillBadge } from './components/Primitives'; import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls'; import { getEvent, getEvents, getEventTasks, + updateEvent, updateTask, TenantTask, TenantEvent, @@ -48,6 +50,19 @@ import { buildTaskSummary } from './lib/taskSummary'; import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts'; import { withAlpha } from './components/colors'; import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme'; +import { resolveEngagementMode } from '../lib/events'; +import { useAuth } from '../auth/context'; + +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; +} function TaskSummaryCard({ summary }: { summary: ReturnType }) { const { t } = useTranslation('management'); @@ -238,7 +253,9 @@ export default function MobileEventTasksPage() { const slug = slugParam ?? activeEvent?.slug ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); + const { user } = useAuth(); const { textStrong, muted, subtle, border, primary, danger, surface, surfaceMuted, dangerBg, dangerText, overlay } = useAdminTheme(); + const isMember = user?.role === 'member'; const [assignedTasks, setAssignedTasks] = React.useState([]); const [library, setLibrary] = React.useState([]); const [collections, setCollections] = React.useState([]); @@ -271,6 +288,8 @@ export default function MobileEventTasksPage() { const [savingEmotion, setSavingEmotion] = React.useState(false); const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false); const [quickNavSelection, setQuickNavSelection] = React.useState(''); + const [eventRecord, setEventRecord] = React.useState(null); + const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false); const text = textStrong; const assignedRef = React.useRef(null); const libraryRef = React.useRef(null); @@ -281,6 +300,13 @@ export default function MobileEventTasksPage() { collections: collections.length, emotions: emotions.length, }); + const permissionSource = eventRecord ?? activeEvent; + const memberPermissions = Array.isArray(permissionSource?.member_permissions) ? permissionSource?.member_permissions ?? [] : []; + const canManageTasks = React.useMemo( + () => (isMember ? allowPermission(memberPermissions, 'tasks:manage') : true), + [isMember, memberPermissions] + ); + const tasksEnabled = resolveEngagementMode(permissionSource ?? null) !== 'photo_only'; const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]); React.useEffect(() => { if (slugParam && activeEvent?.slug !== slugParam) { @@ -339,6 +365,7 @@ export default function MobileEventTasksPage() { try { const event = await getEvent(slug); setEventId(event.id); + setEventRecord(event); const [result, libraryTasks] = await Promise.all([ getEventTasks(event.id, 1), getTasks({ per_page: 200 }), @@ -574,6 +601,30 @@ export default function MobileEventTasksPage() { } } + async function handleTasksToggle(nextEnabled: boolean) { + if (!slug || tasksToggleBusy || !canManageTasks) return; + setTasksToggleBusy(true); + try { + const updated = await updateEvent(slug, { + settings: { + engagement_mode: nextEnabled ? 'tasks' : 'photo_only', + }, + }); + setEventRecord(updated); + toast.success( + nextEnabled + ? t('events.tasks.toggle.enabled', 'Tasks activated') + : t('events.tasks.toggle.disabled', 'Tasks disabled') + ); + } catch (err) { + if (!isAuthError(err)) { + toast.error(t('events.errors.toggleFailed', 'Status could not be updated.')); + } + } finally { + setTasksToggleBusy(false); + } + } + return ( ) : null} + {!loading ? ( + + + + {t('events.tasks.toggle.title', '1. Activate tasks')} + + + {t( + 'events.tasks.toggle.description', + 'Enable tasks so guests see challenges and prompts in the app.' + )} + + + + + {tasksEnabled + ? t('events.tasks.toggle.active', 'ACTIVE') + : t('events.tasks.toggle.inactive', 'INACTIVE')} + + + {tasksEnabled + ? t('events.tasks.toggle.onLabel', 'Guests see task prompts') + : t('events.tasks.toggle.offLabel', 'Guest app shows photos only')} + + + + + {t('events.tasks.toggle.switchLabel', 'Tasks enabled')} + + + + + + {isMember && !canManageTasks ? ( + + {t('events.tasks.toggle.permissionHint', 'You do not have permission to change tasks.')} + + ) : null} + + ) : null} + {!loading ? ( ) : null} diff --git a/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx b/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx index 6822751..baed899 100644 --- a/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx @@ -57,6 +57,10 @@ vi.mock('../../context/EventContext', () => ({ }), })); +vi.mock('../../auth/context', () => ({ + useAuth: () => ({ user: { role: 'tenant_admin' } }), +})); + vi.mock('../../api', () => ({ getEvent: vi.fn().mockResolvedValue(fixtures.event), getEvents: vi.fn().mockResolvedValue([fixtures.event]), @@ -65,6 +69,7 @@ vi.mock('../../api', () => ({ getTaskCollections: vi.fn().mockResolvedValue({ data: fixtures.collections }), getEmotions: vi.fn().mockResolvedValue(fixtures.emotions), assignTasksToEvent: vi.fn(), + updateEvent: vi.fn().mockResolvedValue(fixtures.event), updateTask: vi.fn(), importTaskCollection: vi.fn(), createTask: vi.fn(), @@ -110,6 +115,13 @@ vi.mock('@tamagui/text', () => ({ SizableText: ({ children }: { children: React.ReactNode }) => {children}, })); +vi.mock('@tamagui/switch', () => ({ + Switch: Object.assign( + ({ children }: { children: React.ReactNode }) =>
{children}
, + { Thumb: () =>
}, + ), +})); + vi.mock('@tamagui/list-item', () => ({ ListItem: ({ title, subTitle, iconAfter }: { title?: React.ReactNode; subTitle?: React.ReactNode; iconAfter?: React.ReactNode }) => (
@@ -162,6 +174,7 @@ vi.mock('../components/MobileShell', () => ({ vi.mock('../components/Primitives', () => ({ MobileCard: ({ children }: { children: React.ReactNode }) =>
{children}
, + PillBadge: ({ children }: { children: React.ReactNode }) =>
{children}
, CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (