import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react'; 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 { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, CTAButton, SkeletonCard, PillBadge, FloatingActionButton } from './components/Primitives'; import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls'; 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 { RadioGroup } from '@tamagui/radio-group'; import { useBackNavigation } from './hooks/useBackNavigation'; import { buildTaskSummary } from './lib/taskSummary'; import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts'; import { useAdminTheme } from './theme'; function TaskSummaryCard({ summary, text, muted, border, surfaceMuted, }: { summary: ReturnType; text: string; muted: string; border: string; surfaceMuted: string; }) { const { t } = useTranslation('management'); return ( ); } function SummaryItem({ label, value, text, muted, surfaceMuted, }: { label: string; value: number; text: string; muted: string; surfaceMuted: string; }) { return ( {label} {value} ); } 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 { textStrong, muted, subtle, border, primary, danger, surface, surfaceMuted, dangerBg, dangerText, overlay } = useAdminTheme(); 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 [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 [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 text = textStrong; const assignedRef = React.useRef(null); const libraryRef = React.useRef(null); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); const summary = buildTaskSummary({ assigned: assignedTasks.length, library: library.length, collections: collections.length, emotions: emotions.length, }); const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]); 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 scrollToSection = (ref: React.RefObject) => { if (ref.current) { ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; const handleQuickNav = (key: TaskSectionKey) => { if (key === 'assigned') { scrollToSection(assignedRef); return; } if (key === 'library') { scrollToSection(libraryRef); return; } if (key === 'collections') { setShowCollectionSheet(true); return; } setShowEmotionSheet(true); }; 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); } } async function confirmDeleteTask() { if (!deleteCandidate) return; const taskId = deleteCandidate.id; setDeleteCandidate(null); await detachTask(taskId); } 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 ( load()} ariaLabel={t('common.refresh', 'Refresh')}> } > {error ? ( {error} load()} /> ) : null} {!loading ? ( ) : null} {!loading ? ( {t('events.tasks.quickNav', 'Quick jump')} {sectionCounts.map((section) => ( ))} ) : null} {loading ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : assignedTasks.length === 0 ? ( {t('events.tasks.emptyTitle', 'No tasks yet')} {t('events.tasks.emptyBody', 'Create tasks or import a pack for your event.')} setShowTaskSheet(true)} fullWidth={false} /> setShowCollectionSheet(true)} fullWidth={false} /> setShowTaskSheet(true)} title={ {t('events.tasks.addTask', 'Aufgabe hinzufügen')} } subTitle={ {t('events.tasks.addTaskHint', 'Erstelle eine neue Aufgabe für dieses Event.')} } paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> setShowCollectionSheet(true)} title={ {t('events.tasks.import', 'Aufgabenpaket importieren')} } subTitle={ {t('events.tasks.importHint', 'Nutze vordefinierte Pakete für deinen Event-Typ.')} } paddingVertical="$2" paddingHorizontal="$3" iconAfter={} /> ) : (
setSearchTerm(e.target.value)} placeholder={t('events.tasks.search', 'Search tasks')} compact /> 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)} title={ {task.title} } subTitle={ task.description ? ( {task.description} ) : null } iconAfter={ {task.emotion ? ( ) : null}