// @ts-nocheck import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ArrowLeft, Layers, Loader2, PlusCircle, Search, Sparkles } 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; 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, TenantEvent, TenantTask, TenantTaskCollection, TenantEmotion, } from '../api'; import { EmotionsSection } from './EmotionsPage'; import { isAuthError } from '../auth/tokens'; import { ADMIN_EVENTS_PATH, ADMIN_EVENT_BRANDING_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 [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 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 (!taskSearch.trim()) { return list; } const term = taskSearch.toLowerCase(); return list.filter((task) => `${task.title ?? ''} ${task.description ?? ''}`.toLowerCase().includes(term)); }, [assignedTasks, taskSearch, emotionFilter]); 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, hydrateTasks, 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, hydrateTasks, 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, 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 mode = event?.engagement_mode ?? (event?.settings as any)?.engagement_mode; 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 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')}

setTaskSearch(event.target.value)} placeholder={t('sections.assigned.search', 'Aufgaben suchen...')} className="h-8 border-0 bg-transparent text-sm focus-visible:ring-0" />
{emotionChips.length > 0 ? (
{emotionChips.map((emotion) => { const active = emotionFilter.includes(emotion.id); return ( ); })}
) : null} {filteredAssignedTasks.length === 0 ? ( ) : (
{filteredAssignedTasks.map((task) => ( void handleDetachSingle(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} />