// @ts-nocheck import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ArrowLeft, Layers, Loader2, PlusCircle, Search, Sparkles, Pencil, Check, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import toast from 'react-hot-toast'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, DragOverlay, useDraggable, useDroppable, } from '@dnd-kit/core'; import { CSS, } from '@dnd-kit/utilities'; import { AdminLayout } from '../components/AdminLayout'; import { assignTasksToEvent, detachTasksFromEvent, getEvent, getEventTasks, createTask, getTasks, getTaskCollections, importTaskCollection, getEmotions, updateEvent, updateTask, TenantEvent, TenantTask, TenantTaskCollection, TenantEmotion, } from '../api'; import { EmotionsSection } from './EmotionsPage'; import { isAuthError } from '../auth/tokens'; import { ADMIN_EVENTS_PATH, buildEngagementTabPath } from '../constants'; import { filterEmotionsByEventType } from '../lib/emotions'; import { buildEventTabs } from '../lib/eventTabs'; import { Trash2 } from 'lucide-react'; export default function EventTasksPage() { const { t } = useTranslation('management', { keyPrefix: 'eventTasks' }); const { t: tDashboard } = useTranslation('dashboard'); const params = useParams<{ slug?: string }>(); const [searchParams] = useSearchParams(); const slug = params.slug ?? searchParams.get('slug') ?? null; const navigate = useNavigate(); const [event, setEvent] = React.useState(null); const [assignedTasks, setAssignedTasks] = React.useState([]); const [availableTasks, setAvailableTasks] = React.useState([]); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [modeSaving, setModeSaving] = React.useState(false); const [error, setError] = React.useState(null); const [tab, setTab] = React.useState<'tasks' | 'packs'>('tasks'); const [taskSearch, setTaskSearch] = React.useState(''); const [debouncedTaskSearch, setDebouncedTaskSearch] = React.useState(''); const [difficultyFilter, setDifficultyFilter] = React.useState(''); const [collections, setCollections] = React.useState([]); const [collectionsLoading, setCollectionsLoading] = React.useState(false); const [collectionsError, setCollectionsError] = React.useState(null); const [importingCollectionId, setImportingCollectionId] = React.useState(null); const [emotions, setEmotions] = React.useState([]); const [emotionsLoading, setEmotionsLoading] = React.useState(false); const [emotionsError, setEmotionsError] = React.useState(null); const [emotionFilter, setEmotionFilter] = React.useState([]); const [emotionsModalOpen, setEmotionsModalOpen] = React.useState(false); const [newTaskTitle, setNewTaskTitle] = React.useState(''); const [newTaskDescription, setNewTaskDescription] = React.useState(''); const [newTaskEmotionId, setNewTaskEmotionId] = React.useState(null); const [newTaskDifficulty, setNewTaskDifficulty] = React.useState(''); const [creatingTask, setCreatingTask] = React.useState(false); const [draggingId, setDraggingId] = React.useState(null); const [selectedAssignedIds, setSelectedAssignedIds] = React.useState([]); const [selectedAvailableIds, setSelectedAvailableIds] = React.useState([]); const [batchSaving, setBatchSaving] = React.useState(false); const [inlineSavingId, setInlineSavingId] = React.useState(null); const [emotionFilterOpen, setEmotionFilterOpen] = React.useState(false); React.useEffect(() => { const handle = window.setTimeout(() => setDebouncedTaskSearch(taskSearch.trim().toLowerCase()), 180); return () => window.clearTimeout(handle); }, [taskSearch]); const hydrateTasks = React.useCallback(async (targetEvent: TenantEvent) => { try { const [refreshed, libraryTasks] = await Promise.all([ getEventTasks(targetEvent.id, 1), getTasks({ per_page: 200 }), ]); const assignedIds = new Set(refreshed.data.map((task) => task.id)); setAssignedTasks(refreshed.data); const eventTypeId = targetEvent.event_type_id ?? null; const filteredLibraryTasks = 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; }); setAvailableTasks(filteredLibraryTasks); } catch (err) { if (!isAuthError(err)) { setError(t('errors.assign', 'Tasks konnten nicht geladen werden.')); } } }, [t]); const relevantEmotions = React.useMemo(() => { const filtered = filterEmotionsByEventType(emotions, event?.event_type_id ?? event?.event_type?.id ?? null); return filtered.length > 0 ? filtered : emotions; }, [emotions, event?.event_type_id, event?.event_type?.id]); const emotionChips = React.useMemo(() => { const map: Record = {}; assignedTasks.forEach((task) => { if (task.emotion) { map[task.emotion.id] = { ...task.emotion, name_translations: task.emotion.name_translations ?? {}, description: null, description_translations: {}, sort_order: 0, is_active: true, tenant_id: null, is_global: false, event_types: [], created_at: null, updated_at: null, } as TenantEmotion; } }); return Object.values(map); }, [assignedTasks]); React.useEffect(() => { if (!slug) { setError(t('errors.missingSlug', 'Kein Event-Slug angegeben.')); setLoading(false); return; } let cancelled = false; (async () => { try { setLoading(true); const eventData = await getEvent(slug); const [eventTasksResponse, libraryTasks] = await Promise.all([ getEventTasks(eventData.id, 1), getTasks({ per_page: 200 }), ]); if (cancelled) return; setEvent(eventData); const assignedIds = new Set(eventTasksResponse.data.map((task) => task.id)); setAssignedTasks(eventTasksResponse.data); const eventTypeId = eventData.event_type_id ?? null; const filteredLibraryTasks = 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; }); setAvailableTasks(filteredLibraryTasks); setError(null); } catch (err) { if (!isAuthError(err)) { setError(t('errors.load', 'Event-Tasks konnten nicht geladen werden.')); } } finally { if (!cancelled) { setLoading(false); } } })(); return () => { cancelled = true; }; }, [slug, t]); const filteredAssignedTasks = React.useMemo(() => { let list = assignedTasks; if (emotionFilter.length > 0) { const set = new Set(emotionFilter); list = list.filter((task) => (task.emotion_id ? set.has(task.emotion_id) : false)); } if (difficultyFilter) { list = list.filter((task) => task.difficulty === difficultyFilter); } if (!debouncedTaskSearch) { return list; } return list.filter((task) => `${task.title ?? ''} ${task.description ?? ''}`.toLowerCase().includes(debouncedTaskSearch)); }, [assignedTasks, debouncedTaskSearch, emotionFilter, difficultyFilter]); React.useEffect(() => { setSelectedAssignedIds((prev) => prev.filter((id) => assignedTasks.some((task) => task.id === id))); }, [assignedTasks]); React.useEffect(() => { setSelectedAvailableIds((prev) => prev.filter((id) => availableTasks.some((task) => task.id === id))); }, [availableTasks]); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); const handleAssignSingle = React.useCallback( async (taskId: number) => { if (!event) return; const task = availableTasks.find((t) => t.id === taskId); if (task) { setAvailableTasks((prev) => prev.filter((t) => t.id !== taskId)); setAssignedTasks((prev) => [...prev, task]); } setSaving(true); try { await assignTasksToEvent(event.id, [taskId]); toast.success(t('actions.assignedToast', 'Tasks wurden zugewiesen.')); } catch (err) { if (!isAuthError(err)) { toast.error(t('errors.assign', 'Tasks konnten nicht zugewiesen werden.')); } // revert optimistic change if (task) { setAssignedTasks((prev) => prev.filter((t) => t.id !== taskId)); setAvailableTasks((prev) => [...prev, task]); } } finally { setSaving(false); } }, [availableTasks, event, t], ); const handleDetachSingle = React.useCallback( async (taskId: number) => { if (!event) return; const task = assignedTasks.find((t) => t.id === taskId); if (task) { setAssignedTasks((prev) => prev.filter((t) => t.id !== taskId)); setAvailableTasks((prev) => [...prev, task]); } setSaving(true); try { await detachTasksFromEvent(event.id, [taskId]); toast.success(t('actions.removedToast', 'Tasks wurden entfernt.')); } catch (err) { if (!isAuthError(err)) { toast.error(t('errors.remove', 'Tasks konnten nicht entfernt werden.')); } // revert optimistic change if (task) { setAvailableTasks((prev) => prev.filter((t) => t.id !== taskId)); setAssignedTasks((prev) => [...prev, task]); } } finally { setSaving(false); } }, [assignedTasks, event, t], ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || !active?.data?.current) { setDraggingId(null); return; } const originList = active.data.current.list as 'assigned' | 'library'; const overList = (over.data?.current?.list as 'assigned' | 'library' | undefined) ?? null; const targetList = overList ?? (over.id === 'assigned-dropzone' ? 'assigned' : over.id === 'library-dropzone' ? 'library' : null); setDraggingId(null); if (!targetList || targetList === originList) { return; } const taskId = Number(active.id); if (Number.isNaN(taskId)) { return; } if (targetList === 'assigned') { void handleAssignSingle(taskId); } else { void handleDetachSingle(taskId); } }; const handleCreateQuickTask = React.useCallback(async () => { if (!event || !newTaskTitle.trim()) return; setCreatingTask(true); const emotion = emotions.find((e) => e.id === newTaskEmotionId) ?? null; try { const created = await createTask({ title: newTaskTitle.trim(), description: newTaskDescription.trim() || null, emotion_id: newTaskEmotionId ?? undefined, event_type_id: event.event_type_id ?? undefined, difficulty: newTaskDifficulty || undefined, }); setAssignedTasks((prev) => [...prev, { ...created, emotion: emotion ?? created.emotion ?? null }]); setAvailableTasks((prev) => prev.filter((task) => task.id !== created.id)); await assignTasksToEvent(event.id, [created.id]); toast.success(t('actions.created', 'Aufgabe erstellt und zugewiesen.')); setNewTaskTitle(''); setNewTaskDescription(''); setNewTaskEmotionId(null); setNewTaskDifficulty(''); await hydrateTasks(event); } catch (err) { if (!isAuthError(err)) { toast.error(t('errors.create', 'Aufgabe konnte nicht erstellt werden.')); } } finally { setCreatingTask(false); } }, [event, newTaskDescription, newTaskEmotionId, newTaskTitle, newTaskDifficulty, emotions, hydrateTasks, t]); const eventTabs = React.useMemo(() => { if (!event) { return []; } const translateMenu = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const taskBadge = loading ? undefined : assignedTasks.length; return buildEventTabs(event, translateMenu, { photos: event.photo_count ?? undefined, tasks: taskBadge, }); }, [event, assignedTasks.length, t, loading]); React.useEffect(() => { let cancelled = false; setCollectionsLoading(true); setCollectionsError(null); const eventTypeSlug = event?.event_type?.slug ?? null; const query = eventTypeSlug ? { top_picks: true, limit: 6, event_type: eventTypeSlug } : { top_picks: true, limit: 6 }; getTaskCollections(query) .then((result) => { if (cancelled) return; setCollections(result.data); }) .catch((err) => { if (cancelled) return; if (!isAuthError(err)) { setCollectionsError(t('collections.error', 'Kollektionen konnten nicht geladen werden.')); } }) .finally(() => { if (!cancelled) { setCollectionsLoading(false); } }); return () => { cancelled = true; }; }, [event?.event_type?.slug, t]); React.useEffect(() => { let cancelled = false; setEmotionsLoading(true); setEmotionsError(null); getEmotions() .then((list) => { if (!cancelled) { setEmotions(list); } }) .catch((err) => { if (cancelled) { return; } if (!isAuthError(err)) { setEmotionsError(t('tasks.emotions.error', 'Emotionen konnten nicht geladen werden.')); } }) .finally(() => { if (!cancelled) { setEmotionsLoading(false); } }); return () => { cancelled = true; }; }, [t]); const handleImportCollection = React.useCallback(async (collection: TenantTaskCollection) => { if (!slug || !event) { return; } setImportingCollectionId(collection.id); try { await importTaskCollection(collection.id, slug); toast.success( t('collections.imported', { defaultValue: 'Mission Pack "{{name}}" importiert.', name: collection.name, }), ); await hydrateTasks(event); } catch (err) { if (!isAuthError(err)) { toast.error(t('collections.importFailed', 'Mission Pack konnte nicht importiert werden.')); } } finally { setImportingCollectionId(null); } }, [event, hydrateTasks, slug, t]); const tasksEnabled = React.useMemo(() => { const settingsMode = event?.settings && typeof event.settings === 'object' && 'engagement_mode' in (event.settings as Record) ? (event.settings as { engagement_mode?: string }).engagement_mode : undefined; const mode = event?.engagement_mode ?? settingsMode; return mode !== 'photo_only'; }, [event?.engagement_mode, event?.settings]); const summaryBadges = !loading && event ? (
{t('summary.assigned', 'Zugeordnete Tasks')} {assignedTasks.length} {t('summary.library', 'Bibliothek')} {availableTasks.length} {t('summary.mode', 'Aktiver Modus')} {tasksEnabled ? t('summary.tasksMode', 'Mission Cards') : t('summary.photoOnly', 'Nur Fotos')}
) : null; async function handleModeChange(checked: boolean) { if (!event || !slug) return; setModeSaving(true); setError(null); try { const nextMode = checked ? 'tasks' : 'photo_only'; const updated = await updateEvent(slug, { settings: { ...(event.settings ?? {}), engagement_mode: nextMode, }, }); setEvent((prev) => ({ ...(prev ?? updated), ...(updated ?? {}), engagement_mode: updated?.engagement_mode ?? nextMode, settings: { ...(prev?.settings ?? {}), ...(updated?.settings ?? {}), engagement_mode: nextMode, }, })); } catch (err) { if (!isAuthError(err)) { setError( checked ? t('errors.photoOnlyEnable', 'Foto-Modus konnte nicht aktiviert werden.') : t('errors.photoOnlyDisable', 'Foto-Modus konnte nicht deaktiviert werden.'), ); } } finally { setModeSaving(false); } } const handleAssignSelected = React.useCallback(async () => { if (!event || selectedAvailableIds.length === 0) return; const ids = selectedAvailableIds; const move = availableTasks.filter((task) => ids.includes(task.id)); if (!move.length) return; const nextAvailableSet = new Set(ids); const prevAssigned = assignedTasks; const prevAvailable = availableTasks; setAssignedTasks((prev) => [...prev, ...move]); setAvailableTasks((prev) => prev.filter((task) => !nextAvailableSet.has(task.id))); setSelectedAvailableIds([]); setBatchSaving(true); try { await assignTasksToEvent(event.id, ids); toast.success(t('actions.assignedToast', 'Tasks wurden zugewiesen.')); } catch (err) { if (!isAuthError(err)) { toast.error(t('errors.assign', 'Tasks konnten nicht zugewiesen werden.')); } setAssignedTasks(prevAssigned); setAvailableTasks(prevAvailable); } finally { setBatchSaving(false); } }, [event, selectedAvailableIds, availableTasks, assignedTasks, t]); const handleDetachSelected = React.useCallback(async () => { if (!event || selectedAssignedIds.length === 0) return; const ids = selectedAssignedIds; const move = assignedTasks.filter((task) => ids.includes(task.id)); if (!move.length) return; const nextAssignedSet = new Set(ids); const prevAssigned = assignedTasks; const prevAvailable = availableTasks; setAssignedTasks((prev) => prev.filter((task) => !nextAssignedSet.has(task.id))); setAvailableTasks((prev) => [...prev, ...move]); setSelectedAssignedIds([]); setBatchSaving(true); try { await detachTasksFromEvent(event.id, ids); toast.success(t('actions.removedToast', 'Tasks wurden entfernt.')); } catch (err) { if (!isAuthError(err)) { toast.error(t('errors.remove', 'Tasks konnten nicht entfernt werden.')); } setAssignedTasks(prevAssigned); setAvailableTasks(prevAvailable); } finally { setBatchSaving(false); } }, [event, selectedAssignedIds, assignedTasks, availableTasks, t]); const handleInlineUpdate = React.useCallback( async (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | '' }) => { if (!event) return; const prevAssigned = assignedTasks; const prevAvailable = availableTasks; const existingAssigned = assignedTasks.find((task) => task.id === taskId); const existingAvailable = availableTasks.find((task) => task.id === taskId); const optimistic = existingAssigned ?? existingAvailable; if (!optimistic) { return; } const patch: Partial = { ...payload, title: payload.title ?? optimistic.title, difficulty: payload.difficulty || null, } as Partial; if (existingAssigned) { setAssignedTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...patch } : task))); } if (existingAvailable) { setAvailableTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...patch } : task))); } setInlineSavingId(taskId); const duplicateAndAssign = async () => { const created = await createTask({ title: patch.title, difficulty: patch.difficulty ?? undefined, description: optimistic.description ?? null, priority: optimistic.priority ?? undefined, emotion_id: optimistic.emotion_id ?? undefined, event_type_id: optimistic.event_type_id ?? undefined, }); await assignTasksToEvent(event.id, [created.id]); setAssignedTasks((prev) => { const withoutOld = prev.filter((task) => task.id !== taskId); return [...withoutOld, { ...created, emotion: optimistic.emotion ?? created.emotion ?? null }]; }); setAvailableTasks((prev) => prev.filter((task) => task.id !== taskId)); toast.success(t('actions.created', 'Aufgabe erstellt und zugewiesen.')); }; try { if (optimistic.tenant_id === null) { await duplicateAndAssign(); } else { const updated = await updateTask(taskId, { title: patch.title, difficulty: payload.difficulty || undefined, description: optimistic.description ?? undefined, priority: optimistic.priority ?? undefined, emotion_id: optimistic.emotion_id ?? undefined, }); if (existingAssigned) { setAssignedTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updated } : task))); } if (existingAvailable) { setAvailableTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updated } : task))); } toast.success(t('actions.updated', 'Task aktualisiert.')); } } catch (err) { if (!isAuthError(err)) { toast.error(t('errors.update', 'Task konnte nicht aktualisiert werden.')); } setAssignedTasks(prevAssigned); setAvailableTasks(prevAvailable); } finally { setInlineSavingId(null); } }, [event, assignedTasks, availableTasks, t], ); const actions = ( ); return ( {summaryBadges} {error && ( {tDashboard('alerts.errorTitle', 'Fehler')} {error} )} {loading ? ( ) : !event ? ( {t('alerts.notFoundTitle', 'Event nicht gefunden')} {t('alerts.notFoundDescription', 'Bitte kehre zur Eventliste zurück.')} ) : ( <> setTab(value as 'tasks' | 'packs')} className="space-y-6"> {t('tabs.tasks', 'Aufgaben')} {t('tabs.packs', 'Mission Packs')}

{t('modes.title', 'Aufgaben & Foto-Modus')}

{tasksEnabled ? t('modes.tasksHint', 'Aufgaben sind aktiv. Gäste sehen Mission Cards in der App.') : t('modes.photoOnlyHint', 'Der Foto-Modus ist aktiv. Gäste können Fotos hochladen, sehen aber keine Aufgaben.')}

{tasksEnabled ? t('modes.tasks', 'Aufgaben aktiv') : t('modes.photoOnly', 'Foto-Modus')}
{modeSaving ? (
{t('modes.updating', 'Einstellung wird gespeichert ...')}
) : null}
{t('library.hintTitle', 'Weitere Vorlagen in der Aufgaben-Bibliothek')} {t('library.hintCopy', 'Lege eigene Aufgaben, Emotionen oder Mission Packs zentral an und nutze sie in mehreren Events.')} setDraggingId(Number(event.active.id))} onDragEnd={handleDragEnd} >

{t('sections.assigned.title', 'Zugeordnete Tasks')}

0 && filteredAssignedTasks.every((task) => selectedAssignedIds.includes(task.id)) ? true : selectedAssignedIds.some((id) => filteredAssignedTasks.some((task) => task.id === id)) ? 'indeterminate' : false } onCheckedChange={(checked) => { if (checked) { setSelectedAssignedIds((prev) => { const next = new Set(prev); filteredAssignedTasks.forEach((task) => next.add(task.id)); return Array.from(next); }); } else { const visibleIds = new Set(filteredAssignedTasks.map((task) => task.id)); setSelectedAssignedIds((prev) => prev.filter((id) => !visibleIds.has(id))); } }} aria-label={t('sections.assigned.selectAll', 'Alle sichtbaren Aufgaben auswählen')} /> {t('sections.assigned.selectedCount', { defaultValue: '{{count}} ausgewählt', count: selectedAssignedIds.length, })}
setTaskSearch(event.target.value)} placeholder={t('sections.assigned.search', 'Aufgaben suchen...')} className="h-8 border-0 bg-transparent text-sm focus-visible:ring-0" />
{t('sections.assigned.results', { defaultValue: '{{count}} Treffer', count: filteredAssignedTasks.length })}
{difficultyFilter ? (
{(['', 'easy', 'medium', 'hard'] as Array).map((value) => { const active = difficultyFilter === value; const label = value === 'easy' ? t('sections.assigned.difficulty.easy', 'Leicht') : value === 'medium' ? t('sections.assigned.difficulty.medium', 'Mittel') : value === 'hard' ? t('sections.assigned.difficulty.hard', 'Schwer') : t('sections.assigned.difficulty.all', 'Alle Stufen'); return ( ); })}
) : null}
{emotionChips.length > 0 ? ( ) : null}
{emotionChips.length > 0 && emotionFilterOpen ? (
{emotionChips.map((emotion) => { const active = emotionFilter.includes(emotion.id); return ( ); })}
) : null} {filteredAssignedTasks.length === 0 ? ( ) : (
{filteredAssignedTasks.map((task) => ( void handleDetachSingle(task.id)} showCheckbox checked={selectedAssignedIds.includes(task.id)} onCheckedChange={(checked) => { setSelectedAssignedIds((prev) => { if (checked) { if (prev.includes(task.id)) return prev; return [...prev, task.id]; } return prev.filter((id) => id !== task.id); }); }} disabled={batchSaving || saving} onInlineUpdate={handleInlineUpdate} inlineSaving={inlineSavingId === task.id} /> ))}
)}

{t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')}

{t('sections.library.quickCreate', 'Schnell neue Aufgabe anlegen')}

setNewTaskTitle(e.target.value)} placeholder={t('sections.library.quickTitle', 'Titel der Aufgabe')} disabled={!tasksEnabled || creatingTask} />