import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { ListItem } from '@tamagui/list-item'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton } from './components/Primitives'; import { getEvent, getEvents, getEventTasks, 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 { useTheme } from '@tamagui/core'; import { RadioGroup } from '@tamagui/radio-group'; const inputBaseStyle = { width: '100%', height: 40, borderRadius: 10, padding: '0 12px', fontSize: 13, } as const; function InlineSeparator() { const theme = useTheme(); return ; } 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 theme = useTheme(); const text = String(theme.color12?.val ?? theme.color?.val ?? '#e5e7eb'); const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1'); const subtle = String(theme.gray8?.val ?? '#94a3b8'); const border = String(theme.borderColor?.val ?? '#334155'); const primary = String(theme.primary?.val ?? '#007AFF'); const danger = String(theme.red10?.val ?? '#ef4444'); const surface = String(theme.surface?.val ?? '#ffffff'); const inputStyle = React.useMemo( () => ({ ...inputBaseStyle, border: `1px solid ${border}`, background: surface, color: text, }), [border, surface, text], ); const [assignedTasks, setAssignedTasks] = React.useState([]); const [library, setLibrary] = React.useState([]); const [collections, setCollections] = React.useState([]); const [emotions, setEmotions] = React.useState([]); const [showCollectionSheet, setShowCollectionSheet] = React.useState(false); 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 [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 [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); 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); const [result, libraryTasks] = await Promise.all([ getEventTasks(event.id, 1), getTasks({ per_page: 200 }), ]); const collectionList = await getTaskCollections({ per_page: 50 }); 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 ?? []); } catch (err) { if (!isAuthError(err)) { const message = getApiErrorMessage(err, t('events.errors.loadFailed', 'Tasks 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; 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', 'Task hinzugefügt')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Task konnte nicht zugewiesen werden.'))); toast.error(t('events.tasks.updateFailed', 'Task konnte nicht zugewiesen werden.')); } } finally { setAssigningId(null); } } async function importCollection(collectionId: number) { if (!slug || !eventId) 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', 'Aufgabenpaket 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; try { if (newTask.id) { if (!Number.isFinite(Number(newTask.id))) { toast.error(t('events.tasks.updateFailed', 'Task 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', 'Aufgabe gespeichert')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Aufgabe konnte nicht erstellt werden.'))); toast.error(t('events.errors.saveFailed', 'Aufgabe 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)); toast.success(t('events.tasks.removed', 'Aufgabe entfernt')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Aufgabe konnte nicht entfernt werden.'))); toast.error(t('events.errors.saveFailed', 'Aufgabe konnte nicht entfernt werden.')); } } finally { setBusyId(null); } } 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; }); async function handleBulkAdd() { if (!eventId || !bulkLines.trim()) return; const lines = bulkLines .split('\n') .map((l) => l.trim()) .filter(Boolean); if (!lines.length) return; try { for (const line of lines) { 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', 'Aufgabe gespeichert')); } catch (err) { if (!isAuthError(err)) { toast.error(t('events.errors.saveFailed', 'Aufgabe 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.'))); } } } return ( navigate(-1)} headerActions={ load()}> } > {error ? ( {error} ) : null} {loading ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : assignedTasks.length === 0 ? ( {t('events.tasks.empty', 'Noch keine Aufgaben zugewiesen.')} {t('events.tasks.emptyHint', 'Lege jetzt Tasks an oder importiere ein Paket.')} setShowTaskSheet(true)}> {t('events.tasks.addTask', 'Aufgabe hinzufügen')} } subTitle={ {t('events.tasks.addTaskHint', 'Erstelle eine neue Aufgabe für dieses Event.')} } paddingVertical="$2" paddingHorizontal="$3" /> setShowCollectionSheet(true)}> {t('events.tasks.import', 'Aufgabenpaket importieren')} } subTitle={ {t('events.tasks.importHint', 'Nutze vordefinierte Pakete für deinen Event-Typ.')} } paddingVertical="$2" paddingHorizontal="$3" /> ) : ( setSearchTerm(e.target.value)} placeholder={t('events.tasks.search', 'Search tasks')} style={{ ...inputStyle, height: 38 }} /> setShowEmotionFilterSheet(true)}> {t('events.tasks.emotionFilter', 'Emotion filter')} {emotionFilter ? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion') : t('events.tasks.allEmotions', 'All')} {t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })} {filteredTasks.map((task, idx) => ( startEdit(task)}> {task.title} } subTitle={ task.description ? ( {task.description} ) : null } iconAfter={ {task.emotion ? ( ) : null} detachTask(task.id)}> } paddingVertical="$2" paddingHorizontal="$3" /> {idx < assignedTasks.length - 1 ? : null} ))} {t('events.tasks.library', 'Weitere Aufgaben')} setShowCollectionSheet(true)}> {t('events.tasks.import', 'Import Pack')} setExpandedLibrary((prev) => !prev)}> {expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')} {library.length === 0 ? ( {t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')} ) : ( {(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => ( {task.title} } subTitle={ task.description ? ( {task.description} ) : null } iconAfter={ quickAssign(task.id)}> {assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')} } paddingVertical="$2" paddingHorizontal="$3" /> {idx < arr.length - 1 ? : null} ))} )} )} setShowCollectionSheet(false)} title={t('events.tasks.import', 'Aufgabenpaket importieren')} footer={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', 'Keine Pakete vorhanden.')} ) : ( {(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => ( {collection.name} } subTitle={ collection.description ? ( {collection.description} ) : null } iconAfter={ importCollection(collection.id)}> {t('events.tasks.import', 'Import')} } paddingVertical="$2" paddingHorizontal="$3" /> {idx < arr.length - 1 ? : null} ))} )} setShowTaskSheet(false)} title={t('events.tasks.addTask', 'Aufgabe hinzufügen')} footer={ createNewTask()} /> } > setNewTask((prev) => ({ ...prev, title: e.target.value }))} placeholder={t('events.tasks.titlePlaceholder', 'z.B. Erstes Gruppenfoto')} style={inputStyle} />