diff --git a/resources/js/admin/pages/EventTasksPage.tsx b/resources/js/admin/pages/EventTasksPage.tsx index 79dd675..31782c5 100644 --- a/resources/js/admin/pages/EventTasksPage.tsx +++ b/resources/js/admin/pages/EventTasksPage.tsx @@ -1,7 +1,7 @@ // @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 { ArrowLeft, Layers, Loader2, PlusCircle, Search, Sparkles, Pencil, Check, X, CircleOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import toast from 'react-hot-toast'; @@ -95,7 +95,8 @@ export default function EventTasksPage() { const [batchSaving, setBatchSaving] = React.useState(false); const [inlineSavingId, setInlineSavingId] = React.useState(null); const [emotionFilterOpen, setEmotionFilterOpen] = React.useState(false); - const libraryRef = React.useRef(null); + const [libraryOpen, setLibraryOpen] = React.useState(false); + const [librarySearch, setLibrarySearch] = React.useState(''); React.useEffect(() => { const handle = window.setTimeout(() => setDebouncedTaskSearch(taskSearch.trim().toLowerCase()), 180); return () => window.clearTimeout(handle); @@ -451,7 +452,7 @@ export default function EventTasksPage() { return mode !== 'photo_only'; }, [event?.engagement_mode, event?.settings]); - const hasSelection = selectedAssignedIds.length > 0 || selectedAvailableIds.length > 0; + const hasSelection = selectedAssignedIds.length > 0; const tasksFirst = assignedTasks.length > 0; const tabOrder: Array<'tasks' | 'packs' | 'emotions'> = tasksFirst ? ['tasks', 'packs', 'emotions'] : ['packs', 'tasks', 'emotions']; const prevAssignedRef = React.useRef(assignedTasks.length); @@ -471,11 +472,18 @@ export default function EventTasksPage() { try { const nextMode = checked ? 'tasks' : 'photo_only'; - const updated = await updateEvent(slug, { + const payload = { + name: event.name, + slug: event.slug, + event_type_id: event.event_type_id ?? event.event_type?.id, + event_date: event.event_date ?? undefined, settings: { ...(event.settings ?? {}), engagement_mode: nextMode, }, + }; + const updated = await updateEvent(slug, { + ...payload, }); setEvent((prev) => ({ ...(prev ?? updated), @@ -513,6 +521,7 @@ export default function EventTasksPage() { setAssignedTasks((prev) => [...prev, ...move]); setAvailableTasks((prev) => prev.filter((task) => !nextAvailableSet.has(task.id))); setSelectedAvailableIds([]); + setLibraryOpen(false); setBatchSaving(true); try { await assignTasksToEvent(event.id, ids); @@ -557,7 +566,7 @@ export default function EventTasksPage() { }, [event, selectedAssignedIds, assignedTasks, availableTasks, t]); const handleInlineUpdate = React.useCallback( - async (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | '' }) => { + async (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | ''; emotion_id?: number | null }) => { if (!event) return; const prevAssigned = assignedTasks; @@ -573,13 +582,25 @@ export default function EventTasksPage() { ...payload, title: payload.title ?? optimistic.title, difficulty: payload.difficulty || null, + emotion_id: payload.emotion_id ?? optimistic.emotion_id ?? null, } as Partial; + const nextEmotion = + typeof payload.emotion_id === 'number' + ? emotions.find((emotion) => emotion.id === payload.emotion_id) ?? null + : payload.emotion_id === null + ? null + : optimistic.emotion ?? null; + if (existingAssigned) { - setAssignedTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...patch } : task))); + setAssignedTasks((prev) => + prev.map((task) => (task.id === taskId ? { ...task, ...patch, emotion: nextEmotion } : task)), + ); } if (existingAvailable) { - setAvailableTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...patch } : task))); + setAvailableTasks((prev) => + prev.map((task) => (task.id === taskId ? { ...task, ...patch, emotion: nextEmotion } : task)), + ); } setInlineSavingId(taskId); @@ -610,14 +631,22 @@ export default function EventTasksPage() { difficulty: payload.difficulty || undefined, description: optimistic.description ?? undefined, priority: optimistic.priority ?? undefined, - emotion_id: optimistic.emotion_id ?? undefined, + emotion_id: payload.emotion_id ?? optimistic.emotion_id ?? undefined, }); if (existingAssigned) { - setAssignedTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updated } : task))); + setAssignedTasks((prev) => + prev.map((task) => + task.id === taskId ? { ...task, ...updated, emotion: nextEmotion ?? updated.emotion ?? null } : task, + ), + ); } if (existingAvailable) { - setAvailableTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updated } : task))); + setAvailableTasks((prev) => + prev.map((task) => + task.id === taskId ? { ...task, ...updated, emotion: nextEmotion ?? updated.emotion ?? null } : task, + ), + ); } toast.success(t('actions.updated', 'Task aktualisiert.')); } @@ -666,25 +695,29 @@ export default function EventTasksPage() { ) : ( <> setTab(value as 'tasks' | 'packs' | 'emotions')} className="space-y-6"> -
- - {tabOrder.map((key) => ( - - {key === 'packs' - ? t('tabs.packs', 'Vorlagen / Aufgaben-Bundles') - : key === 'tasks' - ? t('tabs.tasks', 'Aufgaben') - : t('tabs.emotions', 'Emotionen')} - - ))} - - +
+
+ +
+
+ + {tabOrder.map((key) => ( + + {key === 'packs' + ? t('tabs.packs', 'Aufgaben-Sets') + : key === 'tasks' + ? t('tabs.tasks', 'Aufgaben') + : t('tabs.emotions', 'Emotionen')} + + ))} + +
@@ -747,6 +780,34 @@ export default function EventTasksPage() { {t('actions.addCustom', 'Eigene Aufgabe hinzufügen')}
+ {!tasksEnabled ? ( +
+
+ +
+

{t('modes.disabledTitle', 'Aufgabenmodus ist aus')}

+

+ {t('modes.disabledCopy', 'Gäste sehen keine Mission Cards. Aktivieren, um Aufgaben sichtbar zu machen.')} +

+
+
+
+ + {assignedTasks.length === 0 ? ( +

+ {t('modes.needTasks', 'Aktiviere Aufgaben, sobald mindestens eine Aufgabe zugewiesen ist.')} +

+ ) : null} +
+
+ ) : null} setDraggingId(Number(event.active.id))} onDragEnd={handleDragEnd} > - + + {!tasksEnabled ? ( +
+
+ +

{t('modes.disabledTitle', 'Aufgabenmodus ist aus')}

+

+ {t('modes.disabledOverlay', 'Aktiviere Aufgaben, um Listen und Aktionen zu nutzen.')} +

+ +
+
+ ) : null}
@@ -866,17 +946,18 @@ export default function EventTasksPage() { ) : null}
- {emotionChips.length > 0 && emotionFilterOpen ? ( -
+ {emotionChips.length > 0 && emotionFilterOpen ? ( +
)}
- -
-
-

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

-
-

- {t('sections.library.helper', 'Suche, filtere und füge einzelne Aufgaben hinzu. Eigene Aufgaben legst du über den Dialog an.')} -

- -
- {availableTasks.length === 0 ? ( - - ) : ( -
-
-
- 0 && availableTasks.every((task) => selectedAvailableIds.includes(task.id)) - ? true - : selectedAvailableIds.some((id) => availableTasks.some((task) => task.id === id)) - ? 'indeterminate' - : false - } - onCheckedChange={(checked) => { - if (checked) { - setSelectedAvailableIds((prev) => { - const next = new Set(prev); - availableTasks.forEach((task) => next.add(task.id)); - return Array.from(next); - }); - } else { - setSelectedAvailableIds([]); - } - }} - aria-label={t('sections.library.selectAll', 'Alle Bibliotheks-Tasks auswählen')} - /> - - {t('sections.library.selectedCount', { - defaultValue: '{{count}} ausgewählt', - count: selectedAvailableIds.length, - })} - -
- -
- {availableTasks.map((task) => ( - void handleAssignSingle(task.id)} - disabled={!tasksEnabled || saving || batchSaving} - showCheckbox - checked={selectedAvailableIds.includes(task.id)} - onCheckedChange={(checked) => { - setSelectedAvailableIds((prev) => { - if (checked) { - if (prev.includes(task.id)) return prev; - return [...prev, task.id]; - } - return prev.filter((id) => id !== task.id); - }); - }} - onInlineUpdate={handleInlineUpdate} - inlineSaving={inlineSavingId === task.id} - /> - ))} -
- )} -
-
-
{draggingId ? ( @@ -1053,36 +1052,21 @@ export default function EventTasksPage() { count: selectedAssignedIds.length, })} - - {t('sections.bulk.librarySelected', { - defaultValue: '{{count}} ausgewählt (Bibliothek)', - count: selectedAvailableIds.length, - })} -
-
+ + + { + setLibraryOpen(open); + if (!open) { + setSelectedAvailableIds([]); + setLibrarySearch(''); + } + }}> + + + {t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')} + +
+
+
+ + setLibrarySearch(e.target.value)} + placeholder={t('sections.library.search', 'Aufgaben suchen...')} + className="h-8 border-0 bg-transparent p-0 text-sm focus-visible:ring-0" + /> +
+
+ + {t('sections.library.selectedCount', { defaultValue: '{{count}} ausgewählt', count: selectedAvailableIds.length })} + + + {t('summary.library', 'Bibliothek')} · {availableTasks.length} + +
+
+ +
+ {availableTasks.length === 0 ? ( + + ) : ( + availableTasks + .filter((task) => + librarySearch.trim() + ? `${task.title ?? ''} ${task.description ?? ''}`.toLowerCase().includes(librarySearch.toLowerCase().trim()) + : true, + ) + .map((task) => ( + void handleAssignSingle(task.id)} + disabled={!tasksEnabled || saving || batchSaving} + showCheckbox + checked={selectedAvailableIds.includes(task.id)} + onCheckedChange={(checked) => { + setSelectedAvailableIds((prev) => { + if (checked) { + if (prev.includes(task.id)) return prev; + return [...prev, task.id]; + } + return prev.filter((id) => id !== task.id); + }); + }} + onInlineUpdate={handleInlineUpdate} + inlineSaving={inlineSavingId === task.id} + emotions={relevantEmotions} + /> + )) + )} +
+ + {selectedAvailableIds.length > 0 ? ( +
+
+ + {t('sections.library.selectedCount', { defaultValue: '{{count}} ausgewählt', count: selectedAvailableIds.length })} + +
+
+ + +
+
+ ) : null} +
+
@@ -1247,6 +1329,7 @@ function DraggableTaskCard({ onCheckedChange, onInlineUpdate, inlineSaving, + emotions, }: { task: TenantTask; origin: 'assigned' | 'library'; @@ -1256,39 +1339,30 @@ function DraggableTaskCard({ showCheckbox?: boolean; checked?: boolean; onCheckedChange?: (checked: boolean) => void; - onInlineUpdate?: (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | '' }) => void; + onInlineUpdate?: (taskId: number, payload: { title?: string; difficulty?: TenantTask['difficulty'] | ''; emotion_id?: number | null }) => void; inlineSaving?: boolean; + emotions?: TenantEmotion[]; }) { const { t } = useTranslation('management'); - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useDraggable({ - id: task.id, - data: { list: origin }, - }); const [editing, setEditing] = React.useState(false); const [titleValue, setTitleValue] = React.useState(task.title ?? ''); const [difficultyValue, setDifficultyValue] = React.useState(task.difficulty ?? ''); + const [emotionIdValue, setEmotionIdValue] = React.useState(task.emotion_id ?? ''); React.useEffect(() => { setTitleValue(task.title ?? ''); setDifficultyValue(task.difficulty ?? ''); - }, [task.title, task.difficulty]); - - const style = { - transform: transform ? CSS.Translate.toString(transform) : undefined, - transition: transition || undefined, - opacity: isDragging ? 0.8 : 1, - }; + setEmotionIdValue(task.emotion_id ?? ''); + }, [task.title, task.difficulty, task.emotion_id]); return (
setEditing(true)} > -
-
+
+
{showCheckbox ? ( ) : null} - +
+ + {origin === 'assigned' ? ( + + ) : ( + + )} +
-
+
{editing ? (
@@ -1327,6 +1427,21 @@ function DraggableTaskCard({ + {emotions && emotions.length > 0 ? ( + + ) : null}
)}
-
+
{!editing && task.emotion ? ( ) : null} - - {origin === 'assigned' ? ( - - ) : ( - - )}