import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RefreshCcw, Plus, Folder, Pencil, Trash2, MoreHorizontal } 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 { MobileScaffold } from './components/Scaffold'; import { MobileCard, CTAButton } from './components/Primitives'; import { BottomNav } from './components/BottomNav'; import { getEvent, getEventTasks, updateTask, TenantTask, 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 { useMobileNav } from './hooks/useMobileNav'; const inputStyle: React.CSSProperties = { width: '100%', height: 40, borderRadius: 10, border: '1px solid #e5e7eb', padding: '0 12px', fontSize: 13, background: 'white', }; function InlineSeparator() { return ; } export default function MobileEventTasksPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const slug = slugParam ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); const [tasks, setTasks] = 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 | '' }); 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 { go } = useMobileNav(slug); const [searchTerm, setSearchTerm] = React.useState(''); const [emotionFilter, setEmotionFilter] = React.useState(''); const [expandedLibrary, setExpandedLibrary] = React.useState(false); const [expandedCollections, setExpandedCollections] = React.useState(false); const [showActionsSheet, setShowActionsSheet] = 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: '#e5e7eb' }); const [savingEmotion, setSavingEmotion] = React.useState(false); const load = React.useCallback(async () => { if (!slug) { 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 = await getEventTasks(event.id, 1); const libraryTasks = await getTasks({ per_page: 50 }); const collectionList = await getTaskCollections({ per_page: 50 }); const emotionList = await getEmotions(); setTasks(result.data); setLibrary(libraryTasks.data.filter((task) => !result.data.find((t) => t.id === task.id))); setCollections(collectionList.data ?? []); setEmotions(emotionList ?? []); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Tasks konnten nicht geladen werden.'))); } } finally { setLoading(false); } }, [slug, t]); 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); setTasks(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 (!eventId) return; try { await importTaskCollection(collectionId, eventId); const result = await getEventTasks(eventId, 1); setTasks(result.data); 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) { await updateTask(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); setTasks(result.data); setShowTaskSheet(false); setNewTask({ id: null, title: '', description: '', emotion_id: '' }); 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]); setTasks((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) : '', }); setShowTaskSheet(true); }; const filteredTasks = tasks.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); setTasks(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: '#e5e7eb' }); 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)} rightSlot={ load()}> setShowActionsSheet(true)}> } footer={ } > {error ? ( {error} ) : null} {loading ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : tasks.length === 0 ? ( {t('events.tasks.empty', 'Noch keine Aufgaben.')} ) : ( setSearchTerm(e.target.value)} placeholder={t('events.tasks.search', 'Search tasks')} style={{ ...inputStyle, height: 38 }} /> setEmotionFilter('')} /> {emotions.map((emotion) => ( setEmotionFilter(String(emotion.id))} /> ))} {t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })} {filteredTasks.map((task, idx) => ( {task.title} } subTitle={ task.description ? ( {task.description} ) : null } iconAfter={ startEdit(task)}> detachTask(task.id)}> } paddingVertical="$2" paddingHorizontal="$3" > {task.emotion ? ( ) : null} {idx < tasks.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} > setExpandedCollections((prev) => !prev)}> {expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')} {collections.length === 0 ? ( {t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')} ) : ( {(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => ( {collection.title} } 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} />