weitere perfektionierung der neuen mobile app
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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 { RefreshCcw, Plus, Pencil, Trash2, ChevronDown } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { ListItem } from '@tamagui/list-item';
|
||||
@@ -10,9 +10,11 @@ import { MobileShell } from './components/MobileShell';
|
||||
import { MobileCard, CTAButton } from './components/Primitives';
|
||||
import {
|
||||
getEvent,
|
||||
getEvents,
|
||||
getEventTasks,
|
||||
updateTask,
|
||||
TenantTask,
|
||||
TenantEvent,
|
||||
assignTasksToEvent,
|
||||
getTasks,
|
||||
getTaskCollections,
|
||||
@@ -33,19 +35,20 @@ import toast from 'react-hot-toast';
|
||||
import { MobileSheet } from './components/Sheet';
|
||||
import { Tag } from './components/Tag';
|
||||
import { useEventContext } from '../context/EventContext';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
import { RadioGroup } from '@tamagui/radio-group';
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
const inputBaseStyle = {
|
||||
width: '100%',
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '0 12px',
|
||||
fontSize: 13,
|
||||
background: 'white',
|
||||
};
|
||||
} as const;
|
||||
|
||||
function InlineSeparator() {
|
||||
return <XStack height={1} backgroundColor="#e5e7eb" opacity={0.7} marginLeft="$3" />;
|
||||
const theme = useTheme();
|
||||
return <XStack height={1} opacity={0.7} marginLeft="$3" backgroundColor={theme.borderColor?.val ?? '#e5e7eb'} />;
|
||||
}
|
||||
|
||||
export default function MobileEventTasksPage() {
|
||||
@@ -54,14 +57,37 @@ export default function MobileEventTasksPage() {
|
||||
const slug = slugParam ?? activeEvent?.slug ?? null;
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('management');
|
||||
const theme = useTheme();
|
||||
const text = String(theme.color12?.val ?? theme.color?.val ?? '#e5e7eb');
|
||||
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
|
||||
const subtle = String(theme.gray8?.val ?? '#94a3b8');
|
||||
const border = String(theme.borderColor?.val ?? '#334155');
|
||||
const primary = String(theme.primary?.val ?? '#007AFF');
|
||||
const danger = String(theme.red10?.val ?? '#ef4444');
|
||||
const surface = String(theme.surface?.val ?? '#ffffff');
|
||||
const inputStyle = React.useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
...inputBaseStyle,
|
||||
border: `1px solid ${border}`,
|
||||
background: surface,
|
||||
color: text,
|
||||
}),
|
||||
[border, surface, text],
|
||||
);
|
||||
|
||||
const [tasks, setTasks] = React.useState<TenantTask[]>([]);
|
||||
const [assignedTasks, setAssignedTasks] = 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 [newTask, setNewTask] = React.useState({
|
||||
id: null as number | null,
|
||||
title: '',
|
||||
description: '',
|
||||
emotion_id: '' as string | '',
|
||||
tenant_id: null as number | null,
|
||||
});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [busyId, setBusyId] = React.useState<number | null>(null);
|
||||
@@ -71,23 +97,42 @@ export default function MobileEventTasksPage() {
|
||||
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 [showFabMenu, setShowFabMenu] = 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 [emotionForm, setEmotionForm] = React.useState({ name: '', color: String(border) });
|
||||
const [savingEmotion, setSavingEmotion] = React.useState(false);
|
||||
const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (slugParam && activeEvent?.slug !== slugParam) {
|
||||
selectEvent(slugParam);
|
||||
}
|
||||
}, [slugParam, activeEvent?.slug, selectEvent]);
|
||||
|
||||
// Reset filters when switching events to avoid empty lists due to stale filters.
|
||||
React.useEffect(() => {
|
||||
setEmotionFilter('');
|
||||
setSearchTerm('');
|
||||
}, [slug]);
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
if (!slug) {
|
||||
setError(t('events.errors.missingSlug', 'Kein Event-Slug angegeben.'));
|
||||
setLoading(false);
|
||||
try {
|
||||
const available = await getEvents({ force: true });
|
||||
if (available.length) {
|
||||
const target = available[0];
|
||||
selectEvent(target.slug ?? null);
|
||||
navigate(adminPath(`/mobile/events/${target.slug ?? ''}/tasks`));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setError(t('events.errors.missingSlug', 'Kein Event-Slug angegeben.'));
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -95,22 +140,44 @@ export default function MobileEventTasksPage() {
|
||||
try {
|
||||
const event = await getEvent(slug);
|
||||
setEventId(event.id);
|
||||
const result = await getEventTasks(event.id, 1);
|
||||
const libraryTasks = await getTasks({ per_page: 50 });
|
||||
const [result, libraryTasks] = await Promise.all([
|
||||
getEventTasks(event.id, 1),
|
||||
getTasks({ per_page: 200 }),
|
||||
]);
|
||||
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)));
|
||||
const assignedIds = new Set(result.data.map((t) => t.id));
|
||||
const eventTypeId = event.event_type_id ?? event.event_type?.id ?? null;
|
||||
const filteredLibrary = 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;
|
||||
});
|
||||
setAssignedTasks(result.data);
|
||||
setLibrary(filteredLibrary);
|
||||
setCollections(collectionList.data ?? []);
|
||||
setEmotions(emotionList ?? []);
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Tasks konnten nicht geladen werden.')));
|
||||
const message = getApiErrorMessage(err, t('events.errors.loadFailed', 'Tasks konnten nicht geladen werden.'));
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
// If the current slug is invalid, attempt to recover to a valid event to avoid empty lists.
|
||||
try {
|
||||
const available = await getEvents({ force: true });
|
||||
const fallback = available.find((e: TenantEvent) => e.slug !== slug) ?? available[0];
|
||||
if (fallback?.slug) {
|
||||
selectEvent(fallback.slug);
|
||||
navigate(adminPath(`/mobile/events/${fallback.slug}/tasks`));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [slug, t]);
|
||||
}, [slug, t, navigate, selectEvent]);
|
||||
|
||||
React.useEffect(() => {
|
||||
void load();
|
||||
@@ -122,7 +189,7 @@ export default function MobileEventTasksPage() {
|
||||
try {
|
||||
await assignTasksToEvent(eventId, [taskId]);
|
||||
const result = await getEventTasks(eventId, 1);
|
||||
setTasks(result.data);
|
||||
setAssignedTasks(result.data);
|
||||
setLibrary((prev) => prev.filter((t) => t.id !== taskId));
|
||||
toast.success(t('events.tasks.assigned', 'Task hinzugefügt'));
|
||||
} catch (err) {
|
||||
@@ -140,7 +207,9 @@ export default function MobileEventTasksPage() {
|
||||
try {
|
||||
await importTaskCollection(collectionId, slug);
|
||||
const result = await getEventTasks(eventId, 1);
|
||||
setTasks(result.data);
|
||||
const assignedIds = new Set(result.data.map((t) => t.id));
|
||||
setAssignedTasks(result.data);
|
||||
setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id)));
|
||||
toast.success(t('events.tasks.imported', 'Aufgabenpaket importiert'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -154,11 +223,31 @@ export default function MobileEventTasksPage() {
|
||||
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);
|
||||
if (!Number.isFinite(Number(newTask.id))) {
|
||||
toast.error(t('events.tasks.updateFailed', 'Task konnte nicht gespeichert werden (ID fehlt).'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isGlobal = !newTask.tenant_id;
|
||||
|
||||
// Global tasks must not be edited in place: clone and replace.
|
||||
if (isGlobal) {
|
||||
const cloned = 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, [cloned.id]);
|
||||
await detachTasksFromEvent(eventId, [Number(newTask.id)]);
|
||||
} else {
|
||||
// Tenant-owned task: update in place.
|
||||
await updateTask(Number(newTask.id), {
|
||||
id: Number(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(),
|
||||
@@ -168,9 +257,11 @@ export default function MobileEventTasksPage() {
|
||||
await assignTasksToEvent(eventId, [created.id]);
|
||||
}
|
||||
const result = await getEventTasks(eventId, 1);
|
||||
setTasks(result.data);
|
||||
const assignedIds = new Set(result.data.map((t) => t.id));
|
||||
setAssignedTasks(result.data);
|
||||
setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id)));
|
||||
setShowTaskSheet(false);
|
||||
setNewTask({ id: null, title: '', description: '', emotion_id: '' });
|
||||
setNewTask({ id: null, title: '', description: '', emotion_id: '', tenant_id: null });
|
||||
toast.success(t('events.tasks.created', 'Aufgabe gespeichert'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -185,7 +276,7 @@ export default function MobileEventTasksPage() {
|
||||
setBusyId(taskId);
|
||||
try {
|
||||
await detachTasksFromEvent(eventId, [taskId]);
|
||||
setTasks((prev) => prev.filter((task) => task.id !== taskId));
|
||||
setAssignedTasks((prev) => prev.filter((task) => task.id !== taskId));
|
||||
toast.success(t('events.tasks.removed', 'Aufgabe entfernt'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -203,11 +294,12 @@ export default function MobileEventTasksPage() {
|
||||
title: task.title,
|
||||
description: task.description ?? '',
|
||||
emotion_id: task.emotion?.id ? String(task.emotion.id) : '',
|
||||
tenant_id: (task as any).tenant_id ?? null,
|
||||
});
|
||||
setShowTaskSheet(true);
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter((task) => {
|
||||
const filteredTasks = assignedTasks.filter((task) => {
|
||||
const matchText =
|
||||
!searchTerm ||
|
||||
task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
@@ -229,7 +321,7 @@ export default function MobileEventTasksPage() {
|
||||
await assignTasksToEvent(eventId, [created.id]);
|
||||
}
|
||||
const result = await getEventTasks(eventId, 1);
|
||||
setTasks(result.data);
|
||||
setAssignedTasks(result.data);
|
||||
setBulkLines('');
|
||||
setShowBulkSheet(false);
|
||||
toast.success(t('events.tasks.created', 'Aufgabe gespeichert'));
|
||||
@@ -253,7 +345,7 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
setShowEmotionSheet(false);
|
||||
setEditingEmotion(null);
|
||||
setEmotionForm({ name: '', color: '#e5e7eb' });
|
||||
setEmotionForm({ name: '', color: border });
|
||||
toast.success(t('events.tasks.emotionSaved', 'Emotion gespeichert'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -284,17 +376,14 @@ export default function MobileEventTasksPage() {
|
||||
headerActions={
|
||||
<XStack space="$2">
|
||||
<Pressable onPress={() => load()}>
|
||||
<RefreshCcw size={18} color="#0f172a" />
|
||||
</Pressable>
|
||||
<Pressable onPress={() => setShowActionsSheet(true)}>
|
||||
<MoreHorizontal size={18} color="#0f172a" />
|
||||
<RefreshCcw size={18} color={text} />
|
||||
</Pressable>
|
||||
</XStack>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<MobileCard>
|
||||
<Text fontSize={13} fontWeight="600" color="#b91c1c">
|
||||
<Text fontSize={13} fontWeight="600" color={danger}>
|
||||
{error}
|
||||
</Text>
|
||||
</MobileCard>
|
||||
@@ -306,12 +395,76 @@ export default function MobileEventTasksPage() {
|
||||
<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>
|
||||
) : assignedTasks.length === 0 ? (
|
||||
<YStack space="$2">
|
||||
<MobileCard space="$2">
|
||||
<Text fontSize={13} fontWeight="700" color={text}>
|
||||
{t('events.tasks.empty', 'Noch keine Aufgaben zugewiesen.')}
|
||||
</Text>
|
||||
<Text fontSize={12} color={muted}>
|
||||
{t('events.tasks.emptyHint', 'Lege jetzt Tasks an oder importiere ein Paket.')}
|
||||
</Text>
|
||||
</MobileCard>
|
||||
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||
<Pressable onPress={() => setShowTaskSheet(true)}>
|
||||
<ListItem
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack
|
||||
width={28}
|
||||
height={28}
|
||||
borderRadius={14}
|
||||
backgroundColor={primary}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Plus size={14} color={surface} />
|
||||
</YStack>
|
||||
<Text fontSize={12.5} fontWeight="700" color={text}>
|
||||
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
|
||||
</Text>
|
||||
</XStack>
|
||||
}
|
||||
subTitle={
|
||||
<Text fontSize={11.5} color={muted}>
|
||||
{t('events.tasks.addTaskHint', 'Erstelle eine neue Aufgabe für dieses Event.')}
|
||||
</Text>
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
/>
|
||||
</Pressable>
|
||||
<InlineSeparator />
|
||||
<Pressable onPress={() => setShowCollectionSheet(true)}>
|
||||
<ListItem
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack
|
||||
width={28}
|
||||
height={28}
|
||||
borderRadius={14}
|
||||
backgroundColor={primary}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Plus size={14} color={surface} />
|
||||
</YStack>
|
||||
<Text fontSize={12.5} fontWeight="700" color={text}>
|
||||
{t('events.tasks.import', 'Aufgabenpaket importieren')}
|
||||
</Text>
|
||||
</XStack>
|
||||
}
|
||||
subTitle={
|
||||
<Text fontSize={11.5} color={muted}>
|
||||
{t('events.tasks.importHint', 'Nutze vordefinierte Pakete für deinen Event-Typ.')}
|
||||
</Text>
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
/>
|
||||
</Pressable>
|
||||
</YStack>
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack space="$2">
|
||||
<YStack space="$2">
|
||||
@@ -322,105 +475,102 @@ export default function MobileEventTasksPage() {
|
||||
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>
|
||||
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
|
||||
<MobileCard borderColor={border} backgroundColor={surface} space="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack>
|
||||
<Text fontSize={12} fontWeight="700" color={text}>
|
||||
{t('events.tasks.emotionFilter', 'Emotion filter')}
|
||||
</Text>
|
||||
<Text fontSize={11} color={muted}>
|
||||
{emotionFilter
|
||||
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
|
||||
: t('events.tasks.allEmotions', 'All')}
|
||||
</Text>
|
||||
</YStack>
|
||||
<ChevronDown size={16} color={muted} />
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
</Pressable>
|
||||
</YStack>
|
||||
|
||||
<Text fontSize="$sm" color="#4b5563">
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
|
||||
</Text>
|
||||
<YStack borderWidth={1} borderColor="#e5e7eb" borderRadius="$4" overflow="hidden">
|
||||
<YStack borderWidth={1} borderColor={border} 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}`}>
|
||||
<Pressable onPress={() => startEdit(task)}>
|
||||
<ListItem
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{task.title}
|
||||
</Text>
|
||||
}
|
||||
subTitle={
|
||||
task.description ? (
|
||||
<Text fontSize={11.5} fontWeight="400" color="#6b7280">
|
||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||
{task.description}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<XStack space="$2" alignItems="flex-start">
|
||||
{task.emotion ? (
|
||||
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
|
||||
) : null}
|
||||
<Pressable disabled={busyId === task.id} onPress={() => detachTask(task.id)}>
|
||||
<Trash2 size={14} color={danger} />
|
||||
</Pressable>
|
||||
</XStack>
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
/>
|
||||
</Pressable>
|
||||
{idx < assignedTasks.length - 1 ? <InlineSeparator /> : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</YStack>
|
||||
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.library', 'Weitere Aufgaben')}
|
||||
</Text>
|
||||
<Pressable onPress={() => setShowCollectionSheet(true)}>
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{t('events.tasks.import', 'Import Pack')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</XStack>
|
||||
<Pressable onPress={() => setExpandedLibrary((prev) => !prev)}>
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
{library.length === 0 ? (
|
||||
<Text fontSize={12} fontWeight="500" color={subtle}>
|
||||
{t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack borderWidth={1} borderColor={border} 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={text}>
|
||||
{task.title}
|
||||
</Text>
|
||||
}
|
||||
subTitle={
|
||||
task.description ? (
|
||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||
{task.description}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<Pressable onPress={() => quickAssign(task.id)}>
|
||||
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -443,35 +593,37 @@ export default function MobileEventTasksPage() {
|
||||
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 > 6 ? (
|
||||
<Pressable onPress={() => setExpandedCollections((prev) => !prev)}>
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{expandedCollections ? t('events.tasks.hideCollections', 'Hide collections') : t('events.tasks.showCollections', 'Show all')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{collections.length === 0 ? (
|
||||
<Text fontSize={13} fontWeight="500" color="#4b5563">
|
||||
<Text fontSize={13} fontWeight="500" color={muted}>
|
||||
{t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack borderWidth={1} borderColor="#e5e7eb" borderRadius="$4" overflow="hidden">
|
||||
<YStack borderWidth={1} borderColor={border} 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">
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{collection.name}
|
||||
</Text>
|
||||
}
|
||||
subTitle={
|
||||
collection.description ? (
|
||||
<Text fontSize={11.5} fontWeight="400" color="#6b7280">
|
||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||
{collection.description}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<Pressable onPress={() => importCollection(collection.id)}>
|
||||
<Text fontSize={12} fontWeight="600" color="#007AFF">
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{t('events.tasks.import', 'Import')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -496,7 +648,7 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<Field label={t('events.tasks.title', 'Titel')}>
|
||||
<Field label={t('events.tasks.titleLabel', 'Titel')} color={text}>
|
||||
<input
|
||||
type="text"
|
||||
value={newTask.title}
|
||||
@@ -505,7 +657,7 @@ export default function MobileEventTasksPage() {
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('events.tasks.description', 'Beschreibung')}>
|
||||
<Field label={t('events.tasks.description', 'Beschreibung')} color={text}>
|
||||
<textarea
|
||||
value={newTask.description}
|
||||
onChange={(e) => setNewTask((prev) => ({ ...prev, description: e.target.value }))}
|
||||
@@ -513,7 +665,7 @@ export default function MobileEventTasksPage() {
|
||||
style={{ ...inputStyle, minHeight: 80 }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('events.tasks.emotion', 'Emotion')}>
|
||||
<Field label={t('events.tasks.emotion', 'Emotion')} color={text}>
|
||||
<select
|
||||
value={newTask.emotion_id}
|
||||
onChange={(e) => setNewTask((prev) => ({ ...prev, emotion_id: e.target.value }))}
|
||||
@@ -530,47 +682,6 @@ export default function MobileEventTasksPage() {
|
||||
</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)}
|
||||
@@ -578,7 +689,7 @@ export default function MobileEventTasksPage() {
|
||||
footer={<CTAButton label={t('events.tasks.saveTask', 'Aufgabe speichern')} onPress={() => handleBulkAdd()} />}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<Text fontSize={12} color="#4b5563">
|
||||
<Text fontSize={12} color={muted}>
|
||||
{t('events.tasks.bulkHint', 'One task per line. These will be created and added to the event.')}
|
||||
</Text>
|
||||
<textarea
|
||||
@@ -595,7 +706,7 @@ export default function MobileEventTasksPage() {
|
||||
onClose={() => {
|
||||
setShowEmotionSheet(false);
|
||||
setEditingEmotion(null);
|
||||
setEmotionForm({ name: '', color: '#e5e7eb' });
|
||||
setEmotionForm({ name: '', color: border });
|
||||
}}
|
||||
title={t('events.tasks.manageEmotions', 'Manage emotions')}
|
||||
footer={
|
||||
@@ -606,7 +717,7 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<Field label={t('events.tasks.emotionName', 'Name')}>
|
||||
<Field label={t('events.tasks.emotionName', 'Name')} color={text}>
|
||||
<input
|
||||
type="text"
|
||||
value={emotionForm.name}
|
||||
@@ -615,12 +726,12 @@ export default function MobileEventTasksPage() {
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('events.tasks.emotionColor', 'Farbe')}>
|
||||
<Field label={t('events.tasks.emotionColor', 'Farbe')} color={text}>
|
||||
<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' }}
|
||||
style={{ width: '100%', height: 44, borderRadius: 10, border: `1px solid ${border}`, background: surface }}
|
||||
/>
|
||||
</Field>
|
||||
<YStack space="$2">
|
||||
@@ -629,7 +740,7 @@ export default function MobileEventTasksPage() {
|
||||
key={`emo-${em.id}`}
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Tag label={em.name ?? ''} color={em.color ?? '#e5e7eb'} />
|
||||
<Tag label={em.name ?? ''} color={em.color ?? border} />
|
||||
</XStack>
|
||||
}
|
||||
iconAfter={
|
||||
@@ -637,13 +748,13 @@ export default function MobileEventTasksPage() {
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setEditingEmotion(em);
|
||||
setEmotionForm({ name: em.name ?? '', color: em.color ?? '#e5e7eb' });
|
||||
setEmotionForm({ name: em.name ?? '', color: em.color ?? border });
|
||||
}}
|
||||
>
|
||||
<Pencil size={14} color="#007AFF" />
|
||||
<Pencil size={14} color={primary} />
|
||||
</Pressable>
|
||||
<Pressable onPress={() => removeEmotion(em.id)}>
|
||||
<Trash2 size={14} color="#ef4444" />
|
||||
<Trash2 size={14} color={danger} />
|
||||
</Pressable>
|
||||
</XStack>
|
||||
}
|
||||
@@ -654,119 +765,126 @@ export default function MobileEventTasksPage() {
|
||||
</MobileSheet>
|
||||
|
||||
<MobileSheet
|
||||
open={showEmotionSheet}
|
||||
onClose={() => {
|
||||
setShowEmotionSheet(false);
|
||||
setEditingEmotion(null);
|
||||
setEmotionForm({ name: '', color: '#e5e7eb' });
|
||||
}}
|
||||
title={t('events.tasks.manageEmotions', 'Manage emotions')}
|
||||
open={showEmotionFilterSheet}
|
||||
onClose={() => setShowEmotionFilterSheet(false)}
|
||||
title={t('events.tasks.emotionFilter', 'Emotion filter')}
|
||||
footer={
|
||||
<CTAButton
|
||||
label={t('events.tasks.saveEmotion', 'Emotion speichern')}
|
||||
onPress={() => {
|
||||
void saveEmotion();
|
||||
}}
|
||||
/>
|
||||
<CTAButton label={t('common.close', 'Close')} onPress={() => setShowEmotionFilterSheet(false)} />
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<RadioGroup
|
||||
value={emotionFilter}
|
||||
onValueChange={(val) => {
|
||||
setEmotionFilter(val);
|
||||
setShowEmotionFilterSheet(false);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<RadioGroup.Item value="">
|
||||
<RadioGroup.Indicator />
|
||||
</RadioGroup.Item>
|
||||
<Text fontSize={12.5} color={text}>
|
||||
{t('events.tasks.allEmotions', 'All')}
|
||||
</Text>
|
||||
</XStack>
|
||||
{emotions.map((emotion) => (
|
||||
<XStack key={`emo-filter-${emotion.id}`} alignItems="center" space="$2">
|
||||
<RadioGroup.Item value={String(emotion.id)}>
|
||||
<RadioGroup.Indicator />
|
||||
</RadioGroup.Item>
|
||||
<Text fontSize={12.5} color={emotion.color ?? text}>
|
||||
{emotion.name ?? ''}
|
||||
</Text>
|
||||
</XStack>
|
||||
))}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</RadioGroup>
|
||||
</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,
|
||||
}}
|
||||
<Pressable
|
||||
onPress={() => setShowFabMenu(true)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 20,
|
||||
bottom: 90,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
boxShadow: '0 10px 25px rgba(0,122,255,0.35)',
|
||||
zIndex: 60,
|
||||
}}
|
||||
>
|
||||
<Plus size={20} color={surface} />
|
||||
</Pressable>
|
||||
|
||||
<MobileSheet
|
||||
open={showFabMenu}
|
||||
onClose={() => setShowFabMenu(false)}
|
||||
title={t('events.tasks.actions', 'Aktionen')}
|
||||
footer={null}
|
||||
>
|
||||
<Plus size={20} color="#ffffff" />
|
||||
</Pressable>
|
||||
<YStack space="$1">
|
||||
<ListItem
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
|
||||
</Text>
|
||||
}
|
||||
onPress={() => {
|
||||
setShowFabMenu(false);
|
||||
setShowTaskSheet(true);
|
||||
}}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
/>
|
||||
<ListItem
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.bulkAdd', 'Bulk add')}
|
||||
</Text>
|
||||
}
|
||||
onPress={() => {
|
||||
setShowFabMenu(false);
|
||||
setShowBulkSheet(true);
|
||||
}}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
/>
|
||||
<ListItem
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.manageEmotions', 'Manage emotions')}
|
||||
</Text>
|
||||
}
|
||||
subTitle={
|
||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||
{t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')}
|
||||
</Text>
|
||||
}
|
||||
onPress={() => {
|
||||
setShowFabMenu(false);
|
||||
setShowEmotionSheet(true);
|
||||
}}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
/>
|
||||
</YStack>
|
||||
</MobileSheet>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
function Field({ label, color, children }: { label: string; color: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<YStack space="$1">
|
||||
<Text fontSize={12.5} fontWeight="600" color="#111827">
|
||||
<Text fontSize={12.5} fontWeight="600" color={color}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user