import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight, Check, Info } from 'lucide-react'; import { Card } from '@tamagui/card'; import { YStack, XStack } from '@tamagui/stacks'; import { YGroup } from '@tamagui/group'; import { SizableText as Text } from '@tamagui/text'; import { ListItem } from '@tamagui/list-item'; import { Pressable } from '@tamagui/react-native-web-lite'; import { Button } from '@tamagui/button'; import { AlertDialog } from '@tamagui/alert-dialog'; import { Switch } from '@tamagui/switch'; import { Checkbox } from '@tamagui/checkbox'; import { Tabs } from 'tamagui'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; 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, assignTasksToEvent, getTasks, getTaskCollections, importTaskCollection, createTask, TenantTaskCollection, getEmotions, TenantEmotion, detachTasksFromEvent, createEmotion, updateEmotion as updateEmotionApi, deleteEmotion as deleteEmotionApi, } from '../api'; import { adminPath } from '../constants'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import toast from 'react-hot-toast'; import { MobileSheet } from './components/Sheet'; import { Tag } from './components/Tag'; import { useEventContext } from '../context/EventContext'; import { RadioGroup } from '@tamagui/radio-group'; import { useBackNavigation } from './hooks/useBackNavigation'; import { type TaskSectionKey } from './lib/taskSectionCounts'; import { withAlpha } from './components/colors'; import { useAdminTheme } from './theme'; import { resolveEngagementMode } from '../lib/events'; import { useAuth } from '../auth/context'; import { ContextHelpLink } from './components/ContextHelpLink'; 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; } export default function MobileEventTasksPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const { activeEvent, selectEvent } = useEventContext(); 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, shadow, glassSurface, glassBorder, glassShadow, } = useAdminTheme(); const isMember = user?.role === 'member'; const [assignedTasks, setAssignedTasks] = React.useState([]); const [library, setLibrary] = React.useState([]); const [collections, setCollections] = React.useState([]); const [emotions, setEmotions] = React.useState([]); const [activeTab, setActiveTab] = React.useState('assigned'); const [showTaskSheet, setShowTaskSheet] = React.useState(false); const [newTask, setNewTask] = React.useState({ id: null as number | null, title: '', description: '', emotion_id: '' as string | '', tenant_id: null as number | null, }); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [busyId, setBusyId] = React.useState(null); const [assigningId, setAssigningId] = React.useState(null); const [deleteCandidate, setDeleteCandidate] = React.useState(null); const [eventId, setEventId] = React.useState(null); const [searchTerm, setSearchTerm] = React.useState(''); const [emotionFilter, setEmotionFilter] = React.useState(''); const [expandedLibrary, setExpandedLibrary] = React.useState(false); const [expandedCollections, setExpandedCollections] = React.useState(false); const [selectionMode, setSelectionMode] = React.useState(false); const [selectedTaskIds, setSelectedTaskIds] = React.useState>(new Set()); const [bulkDeleteOpen, setBulkDeleteOpen] = React.useState(false); const [bulkDeleteBusy, setBulkDeleteBusy] = React.useState(false); const longPressTimer = React.useRef(null); const longPressTriggered = React.useRef(false); const [showFabMenu, setShowFabMenu] = React.useState(false); const [showBulkSheet, setShowBulkSheet] = React.useState(false); const [bulkLines, setBulkLines] = React.useState(''); const [showEmotionSheet, setShowEmotionSheet] = React.useState(false); const [editingEmotion, setEditingEmotion] = React.useState(null); const [emotionForm, setEmotionForm] = React.useState({ name: '', color: String(border) }); const [savingEmotion, setSavingEmotion] = React.useState(false); const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false); const [showTaskDetails, setShowTaskDetails] = React.useState(false); const [eventRecord, setEventRecord] = React.useState(null); const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false); const text = textStrong; const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); 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 maxTasks = React.useMemo(() => { const limit = eventRecord?.limits?.tasks?.limit; return typeof limit === 'number' && Number.isFinite(limit) ? limit : null; }, [eventRecord?.limits?.tasks?.limit]); const remainingTasks = React.useMemo(() => { const remaining = eventRecord?.limits?.tasks?.remaining; if (typeof remaining === 'number' && Number.isFinite(remaining)) { return Math.max(0, remaining); } if (maxTasks === null) { return null; } return Math.max(0, maxTasks - assignedTasks.length); }, [assignedTasks.length, eventRecord?.limits?.tasks?.remaining, maxTasks]); const canAddTasks = maxTasks === null || (remainingTasks ?? 0) > 0; const limitReachedMessage = t('events.tasks.limitReached', 'Photo task limit reached.'); const limitReachedHint = maxTasks === null ? null : t('events.tasks.limitReachedHint', { count: maxTasks, defaultValue: 'This event allows up to {{count}} photo tasks.', }); React.useEffect(() => { if (slugParam && activeEvent?.slug !== slugParam) { selectEvent(slugParam); } }, [slugParam, activeEvent?.slug, selectEvent]); // Reset filters when switching events to avoid empty lists due to stale filters. React.useEffect(() => { setEmotionFilter(''); setSearchTerm(''); }, [slug]); const load = React.useCallback(async () => { if (!slug) { try { const available = await getEvents({ force: true }); if (available.length) { const target = available[0]; selectEvent(target.slug ?? null); navigate(adminPath(`/mobile/events/${target.slug ?? ''}/tasks`)); return; } } catch { // ignore } finally { setError(t('events.errors.missingSlug', 'Kein Event-Slug angegeben.')); setLoading(false); } return; } setLoading(true); setError(null); try { const event = await getEvent(slug); setEventId(event.id); setEventRecord(event); const eventTypeSlug = event.event_type?.slug ?? null; const [result, libraryTasks] = await Promise.all([ getEventTasks(event.id, 1), getTasks({ per_page: 200 }), ]); const collectionList = await getTaskCollections({ per_page: 50, event_type: eventTypeSlug ?? undefined, }); const emotionList = await getEmotions(); const assignedIds = new Set(result.data.map((t) => t.id)); const eventTypeId = event.event_type_id ?? event.event_type?.id ?? null; const filteredLibrary = libraryTasks.data.filter((task) => { if (assignedIds.has(task.id)) return false; if (eventTypeId && task.event_type_id && task.event_type_id !== eventTypeId) return false; return true; }); setAssignedTasks(result.data); setLibrary(filteredLibrary); setCollections(collectionList.data ?? []); setEmotions(emotionList ?? []); setSelectionMode(false); setSelectedTaskIds(new Set()); } catch (err) { if (!isAuthError(err)) { const message = getApiErrorMessage(err, t('events.errors.loadFailed', 'Fotoaufgaben konnten nicht geladen werden.')); setError(message); toast.error(message); // If the current slug is invalid, attempt to recover to a valid event to avoid empty lists. try { const available = await getEvents({ force: true }); const fallback = available.find((e: TenantEvent) => e.slug !== slug) ?? available[0]; if (fallback?.slug) { selectEvent(fallback.slug); navigate(adminPath(`/mobile/events/${fallback.slug}/tasks`)); } } catch { // ignore } } } finally { setLoading(false); } }, [slug, t, navigate, selectEvent]); React.useEffect(() => { void load(); }, [load]); async function quickAssign(taskId: number) { if (!eventId) return; if (!canAddTasks) { toast.error(limitReachedMessage); return; } setAssigningId(taskId); try { await assignTasksToEvent(eventId, [taskId]); const result = await getEventTasks(eventId, 1); setAssignedTasks(result.data); setLibrary((prev) => prev.filter((t) => t.id !== taskId)); toast.success(t('events.tasks.assigned', 'Fotoaufgabe hinzugefügt')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht zugewiesen werden.'))); toast.error(t('events.tasks.updateFailed', 'Fotoaufgabe konnte nicht zugewiesen werden.')); } } finally { setAssigningId(null); } } async function importCollection(collectionId: number) { if (!slug || !eventId) return; if (!canAddTasks) { toast.error(limitReachedMessage); return; } try { await importTaskCollection(collectionId, slug); const result = await getEventTasks(eventId, 1); const assignedIds = new Set(result.data.map((t) => t.id)); setAssignedTasks(result.data); setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id))); toast.success(t('events.tasks.imported', 'Fotoaufgabenpaket importiert')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Paket konnte nicht importiert werden.'))); toast.error(t('events.errors.saveFailed', 'Paket konnte nicht importiert werden.')); } } } async function createNewTask() { if (!eventId || !newTask.title.trim()) return; if (!canAddTasks) { toast.error(limitReachedMessage); return; } try { if (newTask.id) { if (!Number.isFinite(Number(newTask.id))) { toast.error(t('events.tasks.updateFailed', 'Fotoaufgabe konnte nicht gespeichert werden (ID fehlt).')); return; } const isGlobal = !newTask.tenant_id; // Global tasks must not be edited in place: clone and replace. if (isGlobal) { const cloned = await createTask({ title: newTask.title.trim(), description: newTask.description.trim() || null, emotion_id: newTask.emotion_id ? Number(newTask.emotion_id) : undefined, } as any); await assignTasksToEvent(eventId, [cloned.id]); await detachTasksFromEvent(eventId, [Number(newTask.id)]); } else { // Tenant-owned task: update in place. await updateTask(Number(newTask.id), { id: Number(newTask.id), title: newTask.title.trim(), description: newTask.description.trim() || null, emotion_id: newTask.emotion_id ? Number(newTask.emotion_id) : undefined, } as any); } } else { const created = await createTask({ title: newTask.title.trim(), description: newTask.description.trim() || null, emotion_id: newTask.emotion_id ? Number(newTask.emotion_id) : undefined, } as any); await assignTasksToEvent(eventId, [created.id]); } const result = await getEventTasks(eventId, 1); const assignedIds = new Set(result.data.map((t) => t.id)); setAssignedTasks(result.data); setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id))); setShowTaskSheet(false); setNewTask({ id: null, title: '', description: '', emotion_id: '', tenant_id: null }); toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht erstellt werden.'))); toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht erstellt werden.')); } } } async function detachTask(taskId: number) { if (!eventId) return; setBusyId(taskId); try { await detachTasksFromEvent(eventId, [taskId]); setAssignedTasks((prev) => prev.filter((task) => task.id !== taskId)); setSelectedTaskIds((prev) => { if (!prev.has(taskId)) return prev; const next = new Set(prev); next.delete(taskId); if (next.size === 0) { setSelectionMode(false); } return next; }); toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht entfernt werden.'))); toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht entfernt werden.')); } } finally { setBusyId(null); } } async function confirmDeleteTask() { if (!deleteCandidate) return; const taskId = deleteCandidate.id; setDeleteCandidate(null); await detachTask(taskId); } async function confirmBulkDelete() { if (!eventId || selectedTaskIds.size === 0) { setBulkDeleteOpen(false); return; } const ids = Array.from(selectedTaskIds); setBulkDeleteOpen(false); setBulkDeleteBusy(true); try { await detachTasksFromEvent(eventId, ids); setAssignedTasks((prev) => prev.filter((task) => !selectedTaskIds.has(task.id))); setSelectedTaskIds(new Set()); setSelectionMode(false); toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt')); } catch (err) { if (!isAuthError(err)) { toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht entfernt werden.')); } } finally { setBulkDeleteBusy(false); } } const startEdit = (task: TenantTask) => { setNewTask({ id: task.id, title: task.title, description: task.description ?? '', emotion_id: task.emotion?.id ? String(task.emotion.id) : '', tenant_id: (task as any).tenant_id ?? null, }); setShowTaskSheet(true); }; const filteredTasks = assignedTasks.filter((task) => { const matchText = !searchTerm || task.title.toLowerCase().includes(searchTerm.toLowerCase()) || (task.description ?? '').toLowerCase().includes(searchTerm.toLowerCase()); const matchEmotion = !emotionFilter || task.emotion?.id === Number(emotionFilter); return matchText && matchEmotion; }); const toggleSelectedTask = React.useCallback((taskId: number) => { setSelectedTaskIds((prev) => { const next = new Set(prev); if (next.has(taskId)) { next.delete(taskId); } else { next.add(taskId); } if (next.size === 0) { setSelectionMode(false); } return next; }); }, []); const clearSelection = React.useCallback(() => { setSelectedTaskIds(new Set()); setSelectionMode(false); }, []); const startLongPress = React.useCallback( (taskId: number) => { if (selectionMode) return; if (longPressTimer.current) { window.clearTimeout(longPressTimer.current); } longPressTriggered.current = false; longPressTimer.current = window.setTimeout(() => { longPressTriggered.current = true; setSelectionMode(true); toggleSelectedTask(taskId); }, 450); }, [selectionMode, toggleSelectedTask] ); const cancelLongPress = React.useCallback(() => { if (longPressTimer.current) { window.clearTimeout(longPressTimer.current); longPressTimer.current = null; } }, []); const handleTaskPress = React.useCallback( (task: TenantTask) => { if (longPressTriggered.current) { longPressTriggered.current = false; return; } if (selectionMode) { toggleSelectedTask(task.id); return; } startEdit(task); }, [selectionMode, startEdit, toggleSelectedTask] ); React.useEffect(() => { return () => cancelLongPress(); }, [cancelLongPress]); async function handleBulkAdd() { if (!eventId || !bulkLines.trim()) return; if (!canAddTasks) { toast.error(limitReachedMessage); return; } const lines = bulkLines .split('\n') .map((l) => l.trim()) .filter(Boolean); if (!lines.length) return; const capacity = remainingTasks === null ? lines.length : Math.max(0, remainingTasks); const slicedLines = lines.slice(0, capacity); if (!slicedLines.length) { toast.error(limitReachedMessage); return; } try { if (slicedLines.length < lines.length) { toast.error( t('events.tasks.limitSkipped', { count: lines.length - slicedLines.length, defaultValue: 'Skipped {{count}} tasks due to limit.', }), ); } for (const line of slicedLines) { const created = await createTask({ title: line } as any); await assignTasksToEvent(eventId, [created.id]); } const result = await getEventTasks(eventId, 1); setAssignedTasks(result.data); setBulkLines(''); setShowBulkSheet(false); toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert')); } catch (err) { if (!isAuthError(err)) { toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht erstellt werden.')); } } } async function saveEmotion() { if (!emotionForm.name.trim()) return; setSavingEmotion(true); try { if (editingEmotion) { const updated = await updateEmotionApi(editingEmotion.id, { name: emotionForm.name.trim(), color: emotionForm.color }); setEmotions((prev) => prev.map((em) => (em.id === editingEmotion.id ? updated : em))); } else { const created = await createEmotion({ name: emotionForm.name.trim(), color: emotionForm.color }); setEmotions((prev) => [...prev, created]); } setShowEmotionSheet(false); setEditingEmotion(null); setEmotionForm({ name: '', color: border }); toast.success(t('events.tasks.emotionSaved', 'Emotion gespeichert')); } catch (err) { if (!isAuthError(err)) { toast.error(getApiErrorMessage(err, t('events.errors.saveFailed', 'Konnte nicht gespeichert werden.'))); } } finally { setSavingEmotion(false); } } async function removeEmotion(emotionId: number) { try { await deleteEmotionApi(emotionId); setEmotions((prev) => prev.filter((em) => em.id !== emotionId)); toast.success(t('events.tasks.emotionRemoved', 'Emotion entfernt')); } catch (err) { if (!isAuthError(err)) { toast.error(getApiErrorMessage(err, t('events.errors.saveFailed', 'Konnte nicht gespeichert werden.'))); } } } 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', 'Photo tasks activated') : t('events.tasks.toggle.disabled', 'Photo tasks disabled') ); } catch (err) { if (!isAuthError(err)) { toast.error(t('events.errors.toggleFailed', 'Status could not be updated.')); } } finally { setTasksToggleBusy(false); } } const taskPanel = assignedTasks.length === 0 ? ( {t('events.tasks.emptyTitle', 'No photo tasks yet')} {t('events.tasks.emptyBody', 'Create photo tasks or import a pack for your event.')} {!canAddTasks ? ( {limitReachedMessage} {limitReachedHint ? ` ${limitReachedHint}` : ''} ) : null} setShowTaskSheet(true)} disabled={!canAddTasks} fullWidth={false} /> setActiveTab('collections')} disabled={!canAddTasks} fullWidth={false} /> { if (!canAddTasks) { toast.error(limitReachedMessage); return; } setShowTaskSheet(true); }} title={ {t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')} } subTitle={ {t('events.tasks.addTaskHint', 'Erstelle eine neue Fotoaufgabe für dieses Event.')} } paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> { if (!canAddTasks) { toast.error(limitReachedMessage); return; } setActiveTab('collections'); }} title={ {t('events.tasks.import', 'Fotoaufgabenpaket importieren')} } subTitle={ {t('events.tasks.importHint', 'Nutze vordefinierte Pakete für deinen Event-Typ.')} } paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> ) : ( {t('events.tasks.assignedTitle', 'Task list')} {t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })} {typeof remainingTasks === 'number' && typeof maxTasks === 'number' ? ( ) : null} {selectionMode ? ( {t('events.tasks.selectionCount', '{{count}} ausgewählt', { count: selectedTaskIds.size })} setBulkDeleteOpen(true)} /> clearSelection()} /> ) : null} {filteredTasks.map((task, idx) => ( handleTaskPress(task)} onPointerDown={() => startLongPress(task.id)} onPointerUp={cancelLongPress} onPointerLeave={cancelLongPress} onPointerCancel={cancelLongPress} title={ {selectionMode ? ( toggleSelectedTask(task.id)} onPress={(event: any) => event?.stopPropagation?.()} aria-label={t('events.tasks.select', 'Select photo task')} > ) : null} {task.title} } subTitle={ task.description ? ( {task.description} ) : null } iconAfter={ selectionMode ? null : ( {task.emotion ? ( ) : null} {!canAddTasks ? ( {limitReachedMessage} {limitReachedHint ? ` ${limitReachedHint}` : ''} ) : null} {library.length > 6 ? ( setExpandedLibrary((prev) => !prev)}> {expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')} ) : null} {library.length === 0 ? ( {t('events.tasks.libraryEmpty', 'No more photo tasks available.')} ) : ( {(expandedLibrary ? library : library.slice(0, 6)).map((task) => ( {task.title} } subTitle={ task.description ? ( {task.description} ) : null } iconAfter={ { if (!canAddTasks) { toast.error(limitReachedMessage); return; } quickAssign(task.id); }} > {assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')} } paddingVertical="$2" paddingHorizontal="$3" /> ))} )} ); const collectionsPanel = ( {t('events.tasks.importHint', 'Use predefined packs for your event type.')} {!canAddTasks ? ( {limitReachedMessage} {limitReachedHint ? ` ${limitReachedHint}` : ''} ) : null} {collections.length > 6 ? ( setExpandedCollections((prev) => !prev)}> {expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')} ) : null} {collections.length === 0 ? ( {t('events.tasks.collectionsEmpty', 'No collections available.')} ) : ( {(expandedCollections ? collections : collections.slice(0, 6)).map((collection) => ( {collection.name} } subTitle={ collection.description ? ( {collection.description} ) : null } iconAfter={ } paddingVertical="$2" paddingHorizontal="$3" /> ))} )} ); const emotionsPanel = ( {t('events.tasks.tabs.emotions', 'Emotions')} {emotions.length === 0 ? ( {t('events.tasks.emotionsEmpty', 'No emotions yet. Add one to help categorize tasks.')} ) : ( {emotions.map((emotion) => ( } iconAfter={ { setEditingEmotion(emotion); setEmotionForm({ name: emotion.name ?? '', color: emotion.color ?? border }); setShowEmotionSheet(true); }} > removeEmotion(emotion.id)}> } paddingVertical="$2" paddingHorizontal="$3" /> ))} )} ); return ( load()} ariaLabel={t('common.refresh', 'Refresh')}> } > {error ? ( {error} load()} /> ) : null} {loading ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : ( {t('events.tasks.toggle.title', 'Photo tasks for guests')} setShowTaskDetails((prev) => !prev)} aria-label={t( 'events.tasks.toggle.description', 'Control whether guests see mission cards and prompts.' )} > {t('events.tasks.toggle.switchLabel', 'Photo task mode')} {tasksEnabled ? t('events.tasks.toggle.active', 'ACTIVE') : t('events.tasks.toggle.inactive', 'INACTIVE')} {tasksEnabled ? t('events.tasks.toggle.onLabel', 'Mission cards active') : t('events.tasks.toggle.offLabel', 'Photo feed only')} {showTaskDetails ? ( {t('events.tasks.toggle.description', 'Control whether guests see mission cards and prompts.')} ) : null} {isMember && !canManageTasks ? ( {t('events.tasks.toggle.permissionHint', 'You do not have permission to change photo tasks.')} ) : null} setActiveTab(value as TaskSectionKey)} flexDirection="column" alignItems="stretch" width="100%" > {[ { value: 'assigned', label: t('events.tasks.tabs.tasks', 'Tasks') }, { value: 'library', label: t('events.tasks.tabs.library', 'Task Library') }, { value: 'emotions', label: t('events.tasks.tabs.emotions', 'Emotions') }, { value: 'collections', label: t('events.tasks.tabs.collections', 'Collections') }, ].map((tab, index, arr) => { const isActive = activeTab === tab.value; return ( {tab.label} ); })} setSearchTerm(e.target.value)} placeholder={t('events.tasks.search', 'Search photo tasks')} compact /> setShowEmotionFilterSheet(true)}> {t('events.tasks.emotionFilterShort', 'Emotion')} {emotionFilter ? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion') : t('events.tasks.allEmotions', 'All')} {taskPanel} {libraryPanel} {emotionsPanel} {collectionsPanel} )} setShowTaskSheet(false)} title={t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')} footer={ createNewTask()} disabled={!canAddTasks} /> } > {!canAddTasks ? ( {limitReachedMessage} {limitReachedHint ? ` ${limitReachedHint}` : ''} ) : null} setNewTask((prev) => ({ ...prev, title: e.target.value }))} placeholder={t('events.tasks.titlePlaceholder', 'z.B. Erstes Gruppenfoto')} /> setNewTask((prev) => ({ ...prev, description: e.target.value }))} placeholder={t('events.tasks.descriptionPlaceholder', 'Optionale Hinweise')} compact style={{ minHeight: 80 }} /> setNewTask((prev) => ({ ...prev, emotion_id: e.target.value }))} > {emotions.map((emotion) => ( ))} setShowBulkSheet(false)} title={t('events.tasks.bulkAdd', 'Bulk add')} footer={ handleBulkAdd()} disabled={!canAddTasks} /> } > {!canAddTasks ? ( {limitReachedMessage} {limitReachedHint ? ` ${limitReachedHint}` : ''} ) : null} {maxTasks !== null ? ( {t('events.tasks.limitRemaining', { count: remainingTasks ?? 0, total: maxTasks, defaultValue: '{{count}} of {{total}} photo tasks remaining.', })} ) : null} {t('events.tasks.bulkHint', 'One photo task per line. These will be created and added to the event.')} setBulkLines(e.target.value)} placeholder={t('events.tasks.bulkPlaceholder', 'e.g.\nBride & groom portrait\nGroup photo main guests')} style={{ minHeight: 140, fontSize: 12.5 }} /> { setShowEmotionSheet(false); setEditingEmotion(null); setEmotionForm({ name: '', color: border }); }} title={t('events.tasks.manageEmotions', 'Manage emotions')} footer={ saveEmotion()} /> } > setEmotionForm((prev) => ({ ...prev, name: e.target.value }))} placeholder={t('events.tasks.emotionNamePlaceholder', 'z.B. Joy')} /> setEmotionForm((prev) => ({ ...prev, color: e.target.value }))} style={{ padding: 0 }} /> setShowEmotionFilterSheet(false)} title={t('events.tasks.emotionFilter', 'Emotion filter')} footer={ setShowEmotionFilterSheet(false)} /> } > { setEmotionFilter(val); setShowEmotionFilterSheet(false); }} > {t('events.tasks.allEmotions', 'All')} {emotions.map((emotion) => ( {emotion.name ?? ''} ))} { if (!open) { setDeleteCandidate(null); } }} > {t('events.tasks.removeTitle', 'Remove photo task?')} {deleteCandidate ? t('events.tasks.removeBody', 'This will remove "{{title}}" from the event.', { title: deleteCandidate.title }) : t('events.tasks.removeBodyFallback', 'This will remove the photo task from the event.')} setDeleteCandidate(null)} /> confirmDeleteTask()} /> { if (!open) { setBulkDeleteOpen(false); } }} > {t('events.tasks.bulkRemoveTitle', 'Auswahl löschen')} {t('events.tasks.bulkRemoveBody', 'This will remove the selected photo tasks from the event.')} setBulkDeleteOpen(false)} /> confirmBulkDelete()} disabled={bulkDeleteBusy} /> {canAddTasks ? ( { if (!canAddTasks) { toast.error(limitReachedMessage); return; } setShowFabMenu(true); }} label={t('events.tasks.add', 'Add')} icon={Plus} /> ) : null} setShowFabMenu(false)} title={t('events.tasks.actions', 'Aktionen')} footer={null} > {t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')} } onPress={() => { if (!canAddTasks) { toast.error(limitReachedMessage); return; } setShowFabMenu(false); setShowTaskSheet(true); }} paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> {t('events.tasks.bulkAdd', 'Bulk add')} } onPress={() => { if (!canAddTasks) { toast.error(limitReachedMessage); return; } setShowFabMenu(false); setShowBulkSheet(true); }} paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> {t('events.tasks.manageEmotions', 'Manage emotions')} } subTitle={ {t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')} } onPress={() => { setShowFabMenu(false); setActiveTab('emotions'); }} paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> ); }