771 lines
29 KiB
TypeScript
771 lines
29 KiB
TypeScript
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 <XStack height={1} backgroundColor="#e5e7eb" opacity={0.7} marginLeft="$3" />;
|
|
}
|
|
|
|
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<TenantTask[]>([]);
|
|
const [library, setLibrary] = React.useState<TenantTask[]>([]);
|
|
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
|
|
const [emotions, setEmotions] = React.useState<TenantEmotion[]>([]);
|
|
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<string | null>(null);
|
|
const [busyId, setBusyId] = React.useState<number | null>(null);
|
|
const [assigningId, setAssigningId] = React.useState<number | null>(null);
|
|
const [eventId, setEventId] = React.useState<number | null>(null);
|
|
const { go } = useMobileNav(slug);
|
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
const [emotionFilter, setEmotionFilter] = React.useState<string>('');
|
|
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<TenantEmotion | null>(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 (
|
|
<MobileScaffold
|
|
title={t('events.tasks.title', 'Tasks & Checklists')}
|
|
onBack={() => navigate(-1)}
|
|
rightSlot={
|
|
<XStack space="$2">
|
|
<Pressable onPress={() => load()}>
|
|
<RefreshCcw size={18} color="#0f172a" />
|
|
</Pressable>
|
|
<Pressable onPress={() => setShowActionsSheet(true)}>
|
|
<MoreHorizontal size={18} color="#0f172a" />
|
|
</Pressable>
|
|
</XStack>
|
|
}
|
|
footer={
|
|
<BottomNav active="tasks" onNavigate={go} />
|
|
}
|
|
>
|
|
{error ? (
|
|
<MobileCard>
|
|
<Text fontSize={13} fontWeight="600" color="#b91c1c">
|
|
{error}
|
|
</Text>
|
|
</MobileCard>
|
|
) : null}
|
|
|
|
{loading ? (
|
|
<YStack space="$2">
|
|
{Array.from({ length: 4 }).map((_, idx) => (
|
|
<MobileCard key={`tsk-${idx}`} height={70} opacity={0.6} />
|
|
))}
|
|
</YStack>
|
|
) : tasks.length === 0 ? (
|
|
<MobileCard alignItems="center" justifyContent="center" space="$2">
|
|
<Text fontSize={13} fontWeight="500" color="#4b5563">
|
|
{t('events.tasks.empty', 'Noch keine Aufgaben.')}
|
|
</Text>
|
|
</MobileCard>
|
|
) : (
|
|
<YStack space="$2">
|
|
<YStack space="$2">
|
|
<input
|
|
type="search"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder={t('events.tasks.search', 'Search tasks')}
|
|
style={{ ...inputStyle, height: 38 }}
|
|
/>
|
|
<XStack space="$2" flexWrap="wrap">
|
|
<Chip
|
|
active={!emotionFilter}
|
|
label={t('events.tasks.allEmotions', 'All')}
|
|
onPress={() => setEmotionFilter('')}
|
|
/>
|
|
{emotions.map((emotion) => (
|
|
<Chip
|
|
key={emotion.id}
|
|
label={emotion.name}
|
|
color={emotion.color ?? '#e5e7eb'}
|
|
active={emotionFilter === String(emotion.id)}
|
|
onPress={() => setEmotionFilter(String(emotion.id))}
|
|
/>
|
|
))}
|
|
</XStack>
|
|
</YStack>
|
|
|
|
<Text fontSize="$sm" color="#4b5563">
|
|
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
|
|
</Text>
|
|
<YStack borderWidth={1} borderColor="#e5e7eb" borderRadius="$4" overflow="hidden">
|
|
{filteredTasks.map((task, idx) => (
|
|
<React.Fragment key={task.id}>
|
|
<ListItem
|
|
title={
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{task.title}
|
|
</Text>
|
|
}
|
|
subTitle={
|
|
task.description ? (
|
|
<Text fontSize={11.5} fontWeight="400" color="#6b7280">
|
|
{task.description}
|
|
</Text>
|
|
) : null
|
|
}
|
|
iconAfter={
|
|
<XStack space="$2">
|
|
<Pressable onPress={() => startEdit(task)}>
|
|
<Pencil size={14} color="#007AFF" />
|
|
</Pressable>
|
|
<Pressable disabled={busyId === task.id} onPress={() => detachTask(task.id)}>
|
|
<Trash2 size={14} color="#ef4444" />
|
|
</Pressable>
|
|
</XStack>
|
|
}
|
|
paddingVertical="$2"
|
|
paddingHorizontal="$3"
|
|
>
|
|
{task.emotion ? (
|
|
<XStack marginTop="$1">
|
|
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? '#e5e7eb'} />
|
|
</XStack>
|
|
) : null}
|
|
</ListItem>
|
|
{idx < tasks.length - 1 ? <InlineSeparator /> : null}
|
|
</React.Fragment>
|
|
))}
|
|
</YStack>
|
|
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{t('events.tasks.library', 'Weitere Aufgaben')}
|
|
</Text>
|
|
<Pressable onPress={() => setShowCollectionSheet(true)}>
|
|
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
|
{t('events.tasks.import', 'Import Pack')}
|
|
</Text>
|
|
</Pressable>
|
|
</XStack>
|
|
<Pressable onPress={() => setExpandedLibrary((prev) => !prev)}>
|
|
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
|
{expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')}
|
|
</Text>
|
|
</Pressable>
|
|
{library.length === 0 ? (
|
|
<Text fontSize={12} fontWeight="500" color="#6b7280">
|
|
{t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')}
|
|
</Text>
|
|
) : (
|
|
<YStack borderWidth={1} borderColor="#e5e7eb" borderRadius="$4" overflow="hidden">
|
|
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
|
|
<React.Fragment key={`lib-${task.id}`}>
|
|
<ListItem
|
|
title={
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{task.title}
|
|
</Text>
|
|
}
|
|
subTitle={
|
|
task.description ? (
|
|
<Text fontSize={11.5} fontWeight="400" color="#6b7280">
|
|
{task.description}
|
|
</Text>
|
|
) : null
|
|
}
|
|
iconAfter={
|
|
<Pressable onPress={() => quickAssign(task.id)}>
|
|
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
|
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
|
|
</Text>
|
|
</Pressable>
|
|
}
|
|
paddingVertical="$2"
|
|
paddingHorizontal="$3"
|
|
/>
|
|
{idx < arr.length - 1 ? <InlineSeparator /> : null}
|
|
</React.Fragment>
|
|
))}
|
|
</YStack>
|
|
)}
|
|
</YStack>
|
|
)}
|
|
|
|
<MobileSheet
|
|
open={showCollectionSheet}
|
|
onClose={() => setShowCollectionSheet(false)}
|
|
title={t('events.tasks.import', 'Aufgabenpaket importieren')}
|
|
footer={null}
|
|
>
|
|
<YStack space="$2">
|
|
<Pressable onPress={() => setExpandedCollections((prev) => !prev)}>
|
|
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
|
{expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')}
|
|
</Text>
|
|
</Pressable>
|
|
{collections.length === 0 ? (
|
|
<Text fontSize={13} fontWeight="500" color="#4b5563">
|
|
{t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')}
|
|
</Text>
|
|
) : (
|
|
<YStack borderWidth={1} borderColor="#e5e7eb" borderRadius="$4" overflow="hidden">
|
|
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => (
|
|
<React.Fragment key={collection.id}>
|
|
<ListItem
|
|
title={
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{collection.title}
|
|
</Text>
|
|
}
|
|
subTitle={
|
|
collection.description ? (
|
|
<Text fontSize={11.5} fontWeight="400" color="#6b7280">
|
|
{collection.description}
|
|
</Text>
|
|
) : null
|
|
}
|
|
iconAfter={
|
|
<Pressable onPress={() => importCollection(collection.id)}>
|
|
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
|
{t('events.tasks.import', 'Import')}
|
|
</Text>
|
|
</Pressable>
|
|
}
|
|
paddingVertical="$2"
|
|
paddingHorizontal="$3"
|
|
/>
|
|
{idx < arr.length - 1 ? <InlineSeparator /> : null}
|
|
</React.Fragment>
|
|
))}
|
|
</YStack>
|
|
)}
|
|
</YStack>
|
|
</MobileSheet>
|
|
|
|
<MobileSheet
|
|
open={showTaskSheet}
|
|
onClose={() => setShowTaskSheet(false)}
|
|
title={t('events.tasks.addTask', 'Aufgabe hinzufügen')}
|
|
footer={
|
|
<CTAButton label={t('events.tasks.saveTask', 'Aufgabe speichern')} onPress={() => createNewTask()} />
|
|
}
|
|
>
|
|
<YStack space="$2">
|
|
<Field label={t('events.tasks.title', 'Titel')}>
|
|
<input
|
|
type="text"
|
|
value={newTask.title}
|
|
onChange={(e) => setNewTask((prev) => ({ ...prev, title: e.target.value }))}
|
|
placeholder={t('events.tasks.titlePlaceholder', 'z.B. Erstes Gruppenfoto')}
|
|
style={inputStyle}
|
|
/>
|
|
</Field>
|
|
<Field label={t('events.tasks.description', 'Beschreibung')}>
|
|
<textarea
|
|
value={newTask.description}
|
|
onChange={(e) => setNewTask((prev) => ({ ...prev, description: e.target.value }))}
|
|
placeholder={t('events.tasks.descriptionPlaceholder', 'Optionale Hinweise')}
|
|
style={{ ...inputStyle, minHeight: 80 }}
|
|
/>
|
|
</Field>
|
|
<Field label={t('events.tasks.emotion', 'Emotion')}>
|
|
<select
|
|
value={newTask.emotion_id}
|
|
onChange={(e) => setNewTask((prev) => ({ ...prev, emotion_id: e.target.value }))}
|
|
style={{ ...inputStyle, height: 42 }}
|
|
>
|
|
<option value="">{t('events.tasks.emotionNone', 'Keine')}</option>
|
|
{emotions.map((emotion) => (
|
|
<option key={emotion.id} value={emotion.id}>
|
|
{emotion.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
</YStack>
|
|
</MobileSheet>
|
|
|
|
<MobileSheet
|
|
open={showActionsSheet}
|
|
onClose={() => setShowActionsSheet(false)}
|
|
title={t('events.tasks.moreActions', 'Mehr Aktionen')}
|
|
footer={null}
|
|
>
|
|
<YStack space="$2">
|
|
<ListItem
|
|
title={
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{t('events.tasks.bulkAdd', 'Bulk add')}
|
|
</Text>
|
|
}
|
|
onPress={() => {
|
|
setShowActionsSheet(false);
|
|
setShowBulkSheet(true);
|
|
}}
|
|
paddingVertical="$2"
|
|
paddingHorizontal="$3"
|
|
/>
|
|
<ListItem
|
|
title={
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{t('events.tasks.manageEmotions', 'Manage emotions')}
|
|
</Text>
|
|
}
|
|
subTitle={
|
|
<Text fontSize={11.5} fontWeight="400" color="#6b7280">
|
|
{t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')}
|
|
</Text>
|
|
}
|
|
onPress={() => {
|
|
setShowActionsSheet(false);
|
|
setShowEmotionSheet(true);
|
|
}}
|
|
paddingVertical="$2"
|
|
paddingHorizontal="$3"
|
|
/>
|
|
</YStack>
|
|
</MobileSheet>
|
|
|
|
<MobileSheet
|
|
open={showBulkSheet}
|
|
onClose={() => setShowBulkSheet(false)}
|
|
title={t('events.tasks.bulkAdd', 'Bulk add')}
|
|
footer={<CTAButton label={t('events.tasks.saveTask', 'Aufgabe speichern')} onPress={() => handleBulkAdd()} />}
|
|
>
|
|
<YStack space="$2">
|
|
<Text fontSize={12} color="#4b5563">
|
|
{t('events.tasks.bulkHint', 'One task per line. These will be created and added to the event.')}
|
|
</Text>
|
|
<textarea
|
|
value={bulkLines}
|
|
onChange={(e) => setBulkLines(e.target.value)}
|
|
placeholder={t('events.tasks.bulkPlaceholder', 'e.g.\nBride & groom portrait\nGroup photo main guests')}
|
|
style={{ ...inputStyle, minHeight: 140, fontSize: 12.5 }}
|
|
/>
|
|
</YStack>
|
|
</MobileSheet>
|
|
|
|
<MobileSheet
|
|
open={showEmotionSheet}
|
|
onClose={() => {
|
|
setShowEmotionSheet(false);
|
|
setEditingEmotion(null);
|
|
setEmotionForm({ name: '', color: '#e5e7eb' });
|
|
}}
|
|
title={t('events.tasks.manageEmotions', 'Manage emotions')}
|
|
footer={
|
|
<CTAButton
|
|
label={savingEmotion ? t('common.saving', 'Saving...') : t('events.tasks.saveEmotion', 'Emotion speichern')}
|
|
onPress={() => saveEmotion()}
|
|
/>
|
|
}
|
|
>
|
|
<YStack space="$2">
|
|
<Field label={t('events.tasks.emotionName', 'Name')}>
|
|
<input
|
|
type="text"
|
|
value={emotionForm.name}
|
|
onChange={(e) => setEmotionForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
placeholder={t('events.tasks.emotionNamePlaceholder', 'z.B. Joy')}
|
|
style={inputStyle}
|
|
/>
|
|
</Field>
|
|
<Field label={t('events.tasks.emotionColor', 'Farbe')}>
|
|
<input
|
|
type="color"
|
|
value={emotionForm.color}
|
|
onChange={(e) => setEmotionForm((prev) => ({ ...prev, color: e.target.value }))}
|
|
style={{ width: '100%', height: 44, borderRadius: 10, border: '1px solid #e5e7eb', background: 'white' }}
|
|
/>
|
|
</Field>
|
|
<YStack space="$2">
|
|
{emotions.map((em) => (
|
|
<ListItem
|
|
key={`emo-${em.id}`}
|
|
title={
|
|
<XStack alignItems="center" space="$2">
|
|
<Tag label={em.name ?? ''} color={em.color ?? '#e5e7eb'} />
|
|
</XStack>
|
|
}
|
|
iconAfter={
|
|
<XStack space="$2">
|
|
<Pressable
|
|
onPress={() => {
|
|
setEditingEmotion(em);
|
|
setEmotionForm({ name: em.name ?? '', color: em.color ?? '#e5e7eb' });
|
|
}}
|
|
>
|
|
<Pencil size={14} color="#007AFF" />
|
|
</Pressable>
|
|
<Pressable onPress={() => removeEmotion(em.id)}>
|
|
<Trash2 size={14} color="#ef4444" />
|
|
</Pressable>
|
|
</XStack>
|
|
}
|
|
/>
|
|
))}
|
|
</YStack>
|
|
</YStack>
|
|
</MobileSheet>
|
|
|
|
<MobileSheet
|
|
open={showEmotionSheet}
|
|
onClose={() => {
|
|
setShowEmotionSheet(false);
|
|
setEditingEmotion(null);
|
|
setEmotionForm({ name: '', color: '#e5e7eb' });
|
|
}}
|
|
title={t('events.tasks.manageEmotions', 'Manage emotions')}
|
|
footer={
|
|
<CTAButton
|
|
label={t('events.tasks.saveEmotion', 'Emotion speichern')}
|
|
onPress={() => {
|
|
void saveEmotion();
|
|
}}
|
|
/>
|
|
}
|
|
>
|
|
<YStack space="$2">
|
|
<Field label={t('events.tasks.emotionName', 'Name')}>
|
|
<input
|
|
type="text"
|
|
value={emotionForm.name}
|
|
onChange={(e) => setEmotionForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
placeholder={t('events.tasks.emotionNamePlaceholder', 'z.B. Joy')}
|
|
style={inputStyle}
|
|
/>
|
|
</Field>
|
|
<Field label={t('events.tasks.emotionColor', 'Farbe')}>
|
|
<input
|
|
type="color"
|
|
value={emotionForm.color}
|
|
onChange={(e) => setEmotionForm((prev) => ({ ...prev, color: e.target.value }))}
|
|
style={{ width: '100%', height: 44, borderRadius: 10, border: '1px solid #e5e7eb', background: 'white' }}
|
|
/>
|
|
</Field>
|
|
<YStack space="$2">
|
|
{emotions.map((em) => (
|
|
<ListItem
|
|
key={`emo-${em.id}`}
|
|
title={
|
|
<XStack alignItems="center" space="$2">
|
|
<Tag label={em.name ?? ''} color={em.color ?? '#e5e7eb'} />
|
|
</XStack>
|
|
}
|
|
iconAfter={
|
|
<XStack space="$2">
|
|
<Pressable
|
|
onPress={() => {
|
|
setEditingEmotion(em);
|
|
setEmotionForm({ name: em.name ?? '', color: em.color ?? '#e5e7eb' });
|
|
}}
|
|
>
|
|
<Pencil size={14} color="#007AFF" />
|
|
</Pressable>
|
|
<Pressable onPress={() => removeEmotion(em.id)}>
|
|
<Trash2 size={14} color="#ef4444" />
|
|
</Pressable>
|
|
</XStack>
|
|
}
|
|
/>
|
|
))}
|
|
</YStack>
|
|
</YStack>
|
|
</MobileSheet>
|
|
|
|
<Pressable
|
|
onPress={() => setShowTaskSheet(true)}
|
|
style={{
|
|
position: 'fixed',
|
|
right: 20,
|
|
bottom: 90,
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: '#007AFF',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
boxShadow: '0 10px 25px rgba(0,122,255,0.35)',
|
|
zIndex: 60,
|
|
}}
|
|
>
|
|
<Plus size={20} color="#ffffff" />
|
|
</Pressable>
|
|
</MobileScaffold>
|
|
);
|
|
}
|
|
|
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<YStack space="$1">
|
|
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
|
{label}
|
|
</Text>
|
|
{children}
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
function Chip({ label, onPress, active, color }: { label: string; onPress: () => void; active: boolean; color?: string }) {
|
|
return (
|
|
<Pressable onPress={onPress}>
|
|
<XStack
|
|
alignItems="center"
|
|
paddingHorizontal="$3"
|
|
paddingVertical={8}
|
|
borderRadius={999}
|
|
backgroundColor={active ? '#e0f2fe' : '#f3f4f6'}
|
|
borderWidth={1}
|
|
borderColor={active ? '#93c5fd' : '#e5e7eb'}
|
|
>
|
|
<Text fontSize={12} fontWeight="600" color={color ?? (active ? '#0f172a' : '#4b5563')}>
|
|
{label}
|
|
</Text>
|
|
</XStack>
|
|
</Pressable>
|
|
);
|
|
}
|