Update photo task labels and filters

This commit is contained in:
Codex Agent
2026-01-20 08:30:40 +01:00
parent d365536b7d
commit acd19ccfa0
4 changed files with 195 additions and 320 deletions

View File

@@ -519,34 +519,34 @@
"manage": "Layouts & QR-Codes verwalten" "manage": "Layouts & QR-Codes verwalten"
}, },
"tasks": { "tasks": {
"badge": "Aufgaben", "badge": "Fotoaufgaben",
"title": "Tasks & Checklisten", "title": "Fotoaufgaben & Checklisten",
"subtitle": "Motiviere Gäste mit klaren Aufgaben & Highlights.", "subtitle": "Motiviere Gäste mit klaren Fotoaufgaben & Highlights.",
"summary": { "summary": {
"assigned": "Zugewiesen", "assigned": "Zugewiesen",
"library": "Bibliothek", "library": "Bibliothek",
"collections": "Sammlungen", "collections": "Sammlungen",
"emotions": "Emotionen" "emotions": "Emotionen"
}, },
"empty": "Noch keine Aufgaben zugewiesen.", "empty": "Noch keine Fotoaufgaben zugewiesen.",
"manage": "Aufgabenbereich öffnen", "manage": "Fotoaufgabenbereich öffnen",
"status": { "status": {
"completed": "Erledigt", "completed": "Erledigt",
"open": "Offen" "open": "Offen"
}, },
"disabledTitle": "Task-Modus ist für dieses Event aus", "disabledTitle": "Fotoaufgaben-Modus ist für dieses Event aus",
"disabledBody": "Gäste sehen nur den Fotofeed. Aktiviere Tasks in den Event-Einstellungen, um sie wieder anzuzeigen.", "disabledBody": "Gäste sehen nur den Fotofeed. Aktiviere Fotoaufgaben in den Event-Einstellungen, um sie wieder anzuzeigen.",
"toggle": { "toggle": {
"title": "Aufgaben für dieses Event", "title": "Fotoaufgaben für dieses Event",
"description": "Gib Gästen optionale Foto-Ideen und kleine Challenges.", "description": "Gib Gästen optionale Fotoaufgaben und kleine Foto-Ideen.",
"active": "AKTIV", "active": "AKTIV",
"inactive": "INAKTIV", "inactive": "INAKTIV",
"onLabel": "Gäste sehen Aufgaben-Prompts", "onLabel": "Gäste sehen Fotoaufgaben",
"offLabel": "Gäste sehen nur Fotos", "offLabel": "Gäste sehen nur Fotos",
"switchLabel": "Aufgaben aktiv", "switchLabel": "Fotoaufgaben aktiv",
"enabled": "Aufgaben aktiviert", "enabled": "Fotoaufgaben aktiviert",
"disabled": "Aufgaben deaktiviert", "disabled": "Fotoaufgaben deaktiviert",
"permissionHint": "Du hast keine Berechtigung, Aufgaben zu ändern." "permissionHint": "Du hast keine Berechtigung, Fotoaufgaben zu ändern."
}, },
"quickNav": "Schnellzugriff", "quickNav": "Schnellzugriff",
"sections": { "sections": {
@@ -556,31 +556,31 @@
"emotions": "Emotionen" "emotions": "Emotionen"
}, },
"actions": "Aktionen", "actions": "Aktionen",
"assigned": "Task hinzugefügt", "assigned": "Fotoaufgabe hinzugefügt",
"updateFailed": "Task konnte nicht gespeichert werden.", "updateFailed": "Fotoaufgabe konnte nicht gespeichert werden.",
"created": "Aufgabe gespeichert", "created": "Fotoaufgabe gespeichert",
"removed": "Aufgabe entfernt", "removed": "Fotoaufgabe entfernt",
"imported": "Aufgabenpaket importiert", "imported": "Fotoaufgabenpaket importiert",
"saveTask": "Aufgabe speichern", "saveTask": "Fotoaufgabe speichern",
"add": "Hinzufügen", "add": "Hinzufügen",
"emptyHint": "Lege jetzt Tasks an oder importiere ein Paket.", "emptyHint": "Lege jetzt Fotoaufgaben an oder importiere ein Paket.",
"emptyTitle": "Noch keine Tasks", "emptyTitle": "Noch keine Fotoaufgaben",
"emptyBody": "Lege Tasks an oder importiere ein Paket für dein Event.", "emptyBody": "Lege Fotoaufgaben an oder importiere ein Paket für dein Event.",
"emptyActionTask": "Task hinzufügen", "emptyActionTask": "Fotoaufgabe hinzufügen",
"emptyActionPack": "Paket importieren", "emptyActionPack": "Fotoaufgabenpaket importieren",
"addTask": "Aufgabe hinzufügen", "addTask": "Fotoaufgabe hinzufügen",
"addTaskHint": "Erstelle eine neue Aufgabe für dieses Event.", "addTaskHint": "Erstelle eine neue Fotoaufgabe für dieses Event.",
"import": "Aufgabenpaket importieren", "import": "Fotoaufgabenpaket importieren",
"importHint": "Nutze vordefinierte Pakete für deinen Event-Typ.", "importHint": "Nutze vordefinierte Pakete für deinen Event-Typ.",
"search": "Tasks durchsuchen", "search": "Fotoaufgaben durchsuchen",
"emotionFilter": "Emotion filtern", "emotionFilter": "Emotion filtern",
"customEmotion": "Eigene Emotion", "customEmotion": "Eigene Emotion",
"allEmotions": "Alle", "allEmotions": "Alle",
"count": "{{count}} Tasks", "count": "{{count}} Fotoaufgaben",
"library": "Weitere Aufgaben", "library": "Weitere Fotoaufgaben",
"hideLibrary": "Bibliothek ausblenden", "hideLibrary": "Bibliothek ausblenden",
"viewAllLibrary": "Alle anzeigen", "viewAllLibrary": "Alle anzeigen",
"libraryEmpty": "Keine weiteren Aufgaben verfügbar.", "libraryEmpty": "Keine weiteren Fotoaufgaben verfügbar.",
"hideCollections": "Pakete ausblenden", "hideCollections": "Pakete ausblenden",
"showCollections": "Alle Pakete anzeigen", "showCollections": "Alle Pakete anzeigen",
"collectionsEmpty": "Keine Pakete vorhanden.", "collectionsEmpty": "Keine Pakete vorhanden.",
@@ -589,8 +589,8 @@
"bulkRemove": "Auswahl löschen", "bulkRemove": "Auswahl löschen",
"bulkCancel": "Auswahl beenden", "bulkCancel": "Auswahl beenden",
"bulkRemoveTitle": "Auswahl löschen", "bulkRemoveTitle": "Auswahl löschen",
"bulkRemoveBody": "Dies entfernt die ausgewählten Aufgaben aus dem Event.", "bulkRemoveBody": "Dies entfernt die ausgewählten Fotoaufgaben aus dem Event.",
"select": "Aufgabe auswählen", "select": "Fotoaufgabe auswählen",
"manageEmotions": "Emotionen verwalten", "manageEmotions": "Emotionen verwalten",
"manageEmotionsHint": "Filtere und halte deine Taxonomie gepflegt.", "manageEmotionsHint": "Filtere und halte deine Taxonomie gepflegt.",
"saveEmotion": "Emotion speichern", "saveEmotion": "Emotion speichern",
@@ -605,7 +605,7 @@
"descriptionPlaceholder": "Optionale Hinweise", "descriptionPlaceholder": "Optionale Hinweise",
"titleLabel": "Titel", "titleLabel": "Titel",
"titlePlaceholder": "z. B. Erstes Gruppenfoto", "titlePlaceholder": "z. B. Erstes Gruppenfoto",
"bulkHint": "Eine Aufgabe pro Zeile. Sie werden erstellt und dem Event hinzugefügt.", "bulkHint": "Eine Fotoaufgabe pro Zeile. Sie werden erstellt und dem Event hinzugefügt.",
"bulkPlaceholder": "z. B.\nBraut & Bräutigam Porträt\nGruppenfoto mit Hauptgästen" "bulkPlaceholder": "z. B.\nBraut & Bräutigam Porträt\nGruppenfoto mit Hauptgästen"
}, },
"recap": { "recap": {

View File

@@ -515,34 +515,34 @@
"manage": "Manage layouts & invites" "manage": "Manage layouts & invites"
}, },
"tasks": { "tasks": {
"badge": "Tasks", "badge": "Photo tasks",
"title": "Tasks & checklists", "title": "Photo tasks & checklists",
"subtitle": "Motivate guests with clear prompts & highlights.", "subtitle": "Motivate guests with clear photo tasks & highlights.",
"summary": { "summary": {
"assigned": "Assigned", "assigned": "Assigned",
"library": "Library", "library": "Library",
"collections": "Collections", "collections": "Collections",
"emotions": "Emotions" "emotions": "Emotions"
}, },
"empty": "No tasks assigned yet.", "empty": "No photo tasks assigned yet.",
"manage": "Open task workspace", "manage": "Open photo task workspace",
"status": { "status": {
"completed": "Done", "completed": "Done",
"open": "Open" "open": "Open"
}, },
"disabledTitle": "Task mode is off for this event", "disabledTitle": "Photo task mode is off for this event",
"disabledBody": "Guests only see the photo feed. Enable tasks in the event settings to show them again.", "disabledBody": "Guests only see the photo feed. Enable photo tasks in the event settings to show them again.",
"toggle": { "toggle": {
"title": "Tasks for this event", "title": "Photo tasks for this event",
"description": "Give guests optional prompts and photo ideas.", "description": "Give guests optional photo tasks and prompts.",
"active": "ACTIVE", "active": "ACTIVE",
"inactive": "INACTIVE", "inactive": "INACTIVE",
"onLabel": "Guests see task prompts", "onLabel": "Guests see photo tasks",
"offLabel": "Guest app shows photos only", "offLabel": "Guest app shows photos only",
"switchLabel": "Tasks enabled", "switchLabel": "Photo tasks enabled",
"enabled": "Tasks activated", "enabled": "Photo tasks activated",
"disabled": "Tasks disabled", "disabled": "Photo tasks disabled",
"permissionHint": "You do not have permission to change tasks." "permissionHint": "You do not have permission to change photo tasks."
}, },
"quickNav": "Quick jump", "quickNav": "Quick jump",
"sections": { "sections": {
@@ -552,31 +552,31 @@
"emotions": "Emotions" "emotions": "Emotions"
}, },
"actions": "Actions", "actions": "Actions",
"assigned": "Task added", "assigned": "Photo task added",
"updateFailed": "Task could not be saved.", "updateFailed": "Photo task could not be saved.",
"created": "Task saved", "created": "Photo task saved",
"removed": "Task removed", "removed": "Photo task removed",
"imported": "Task pack imported", "imported": "Photo task pack imported",
"saveTask": "Save task", "saveTask": "Save photo task",
"add": "Add", "add": "Add",
"emptyHint": "Add tasks or import a pack.", "emptyHint": "Add photo tasks or import a pack.",
"emptyTitle": "No tasks yet", "emptyTitle": "No photo tasks yet",
"emptyBody": "Create tasks or import a pack for your event.", "emptyBody": "Create photo tasks or import a pack for your event.",
"emptyActionTask": "Add task", "emptyActionTask": "Add photo task",
"emptyActionPack": "Import pack", "emptyActionPack": "Import photo task pack",
"addTask": "Add task", "addTask": "Add photo task",
"addTaskHint": "Create a new task for this event.", "addTaskHint": "Create a new photo task for this event.",
"import": "Import pack", "import": "Import photo task pack",
"importHint": "Use predefined packs for your event type.", "importHint": "Use predefined packs for your event type.",
"search": "Search tasks", "search": "Search photo tasks",
"emotionFilter": "Emotion filter", "emotionFilter": "Emotion filter",
"customEmotion": "Custom emotion", "customEmotion": "Custom emotion",
"allEmotions": "All", "allEmotions": "All",
"count": "{{count}} tasks", "count": "{{count}} photo tasks",
"library": "More tasks", "library": "More photo tasks",
"hideLibrary": "Hide library", "hideLibrary": "Hide library",
"viewAllLibrary": "View all", "viewAllLibrary": "View all",
"libraryEmpty": "No more tasks available.", "libraryEmpty": "No more photo tasks available.",
"hideCollections": "Hide collections", "hideCollections": "Hide collections",
"showCollections": "Show all", "showCollections": "Show all",
"collectionsEmpty": "No collections available.", "collectionsEmpty": "No collections available.",
@@ -585,8 +585,8 @@
"bulkRemove": "Delete selection", "bulkRemove": "Delete selection",
"bulkCancel": "End selection", "bulkCancel": "End selection",
"bulkRemoveTitle": "Delete selection", "bulkRemoveTitle": "Delete selection",
"bulkRemoveBody": "This will remove the selected tasks from the event.", "bulkRemoveBody": "This will remove the selected photo tasks from the event.",
"select": "Select task", "select": "Select photo task",
"manageEmotions": "Manage emotions", "manageEmotions": "Manage emotions",
"manageEmotionsHint": "Filter and keep your taxonomy tidy.", "manageEmotionsHint": "Filter and keep your taxonomy tidy.",
"saveEmotion": "Save emotion", "saveEmotion": "Save emotion",
@@ -601,7 +601,7 @@
"descriptionPlaceholder": "Optional notes", "descriptionPlaceholder": "Optional notes",
"titleLabel": "Title", "titleLabel": "Title",
"titlePlaceholder": "e.g. First group photo", "titlePlaceholder": "e.g. First group photo",
"bulkHint": "One task per line. These will be created and added to the event.", "bulkHint": "One photo task per line. These will be created and added to the event.",
"bulkPlaceholder": "e.g.\nBride & groom portrait\nGroup photo main guests" "bulkPlaceholder": "e.g.\nBride & groom portrait\nGroup photo main guests"
}, },
"recap": { "recap": {

View File

@@ -50,7 +50,7 @@ import { useBackNavigation } from './hooks/useBackNavigation';
import { buildTaskSummary } from './lib/taskSummary'; import { buildTaskSummary } from './lib/taskSummary';
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts'; import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
import { withAlpha } from './components/colors'; import { withAlpha } from './components/colors';
import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme'; import { useAdminTheme } from './theme';
import { resolveEngagementMode } from '../lib/events'; import { resolveEngagementMode } from '../lib/events';
import { useAuth } from '../auth/context'; import { useAuth } from '../auth/context';
@@ -65,140 +65,6 @@ function allowPermission(permissions: string[], permission: string): boolean {
return false; return false;
} }
function TaskSummaryCard({ summary }: { summary: ReturnType<typeof buildTaskSummary> }) {
const { t } = useTranslation('management');
const { textStrong, muted, border, surface, surfaceMuted, shadow } = useAdminTheme();
const total = summary.assigned + summary.library + summary.collections + summary.emotions;
const segments = [
{
key: 'assigned',
label: t('events.tasks.summary.assigned', 'Assigned'),
value: summary.assigned,
color: ADMIN_ACTION_COLORS.tasks,
},
{
key: 'library',
label: t('events.tasks.summary.library', 'Library'),
value: summary.library,
color: ADMIN_ACTION_COLORS.qr,
},
{
key: 'collections',
label: t('events.tasks.summary.collections', 'Collections'),
value: summary.collections,
color: ADMIN_ACTION_COLORS.settings,
},
{
key: 'emotions',
label: t('events.tasks.summary.emotions', 'Emotions'),
value: summary.emotions,
color: ADMIN_ACTION_COLORS.branding,
},
];
return (
<Card
borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.14}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
{t('events.tasks.summary.title', 'Task overview')}
</Text>
</XStack>
</XStack>
<XStack alignItems="baseline" space="$2">
<Text fontSize="$xl" fontWeight="900" color={textStrong}>
{total}
</Text>
<Text fontSize="$xs" color={muted}>
{t('events.tasks.summary.total', 'Tasks total')}
</Text>
</XStack>
<XStack
height={10}
borderRadius={999}
overflow="hidden"
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
{total > 0 ? (
segments.map((segment) =>
segment.value > 0 ? (
<XStack
key={segment.key}
flex={segment.value}
backgroundColor={withAlpha(segment.color, 0.55)}
/>
) : null,
)
) : (
<XStack flex={1} backgroundColor={withAlpha(border, 0.4)} />
)}
</XStack>
<XStack flexWrap="wrap" space="$2">
{segments.map((segment) => (
<SummaryLegendItem key={segment.key} label={segment.label} value={segment.value} color={segment.color} />
))}
</XStack>
</YStack>
</Card>
);
}
function SummaryLegendItem({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
const { textStrong } = useAdminTheme();
return (
<XStack
alignItems="center"
space="$1.5"
paddingHorizontal="$2.5"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={withAlpha(color, 0.35)}
backgroundColor={withAlpha(color, 0.12)}
>
<XStack width={8} height={8} borderRadius={999} backgroundColor={color} />
<Text fontSize={11} fontWeight="700" color={textStrong}>
{label}
</Text>
<Text fontSize={11} fontWeight="700" color={textStrong}>
{value}
</Text>
</XStack>
);
}
function QuickNavChip({ function QuickNavChip({
value, value,
label, label,
@@ -398,7 +264,7 @@ export default function MobileEventTasksPage() {
setSelectedTaskIds(new Set()); setSelectedTaskIds(new Set());
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
const message = getApiErrorMessage(err, t('events.errors.loadFailed', 'Tasks konnten nicht geladen werden.')); const message = getApiErrorMessage(err, t('events.errors.loadFailed', 'Fotoaufgaben konnten nicht geladen werden.'));
setError(message); setError(message);
toast.error(message); toast.error(message);
// If the current slug is invalid, attempt to recover to a valid event to avoid empty lists. // If the current slug is invalid, attempt to recover to a valid event to avoid empty lists.
@@ -430,11 +296,11 @@ export default function MobileEventTasksPage() {
const result = await getEventTasks(eventId, 1); const result = await getEventTasks(eventId, 1);
setAssignedTasks(result.data); setAssignedTasks(result.data);
setLibrary((prev) => prev.filter((t) => t.id !== taskId)); setLibrary((prev) => prev.filter((t) => t.id !== taskId));
toast.success(t('events.tasks.assigned', 'Task hinzugefügt')); toast.success(t('events.tasks.assigned', 'Fotoaufgabe hinzugefügt'));
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Task konnte nicht zugewiesen werden.'))); setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht zugewiesen werden.')));
toast.error(t('events.tasks.updateFailed', 'Task konnte nicht zugewiesen werden.')); toast.error(t('events.tasks.updateFailed', 'Fotoaufgabe konnte nicht zugewiesen werden.'));
} }
} finally { } finally {
setAssigningId(null); setAssigningId(null);
@@ -449,7 +315,7 @@ export default function MobileEventTasksPage() {
const assignedIds = new Set(result.data.map((t) => t.id)); const assignedIds = new Set(result.data.map((t) => t.id));
setAssignedTasks(result.data); setAssignedTasks(result.data);
setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id))); setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id)));
toast.success(t('events.tasks.imported', 'Aufgabenpaket importiert')); toast.success(t('events.tasks.imported', 'Fotoaufgabenpaket importiert'));
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Paket konnte nicht importiert werden.'))); setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Paket konnte nicht importiert werden.')));
@@ -463,7 +329,7 @@ export default function MobileEventTasksPage() {
try { try {
if (newTask.id) { if (newTask.id) {
if (!Number.isFinite(Number(newTask.id))) { if (!Number.isFinite(Number(newTask.id))) {
toast.error(t('events.tasks.updateFailed', 'Task konnte nicht gespeichert werden (ID fehlt).')); toast.error(t('events.tasks.updateFailed', 'Fotoaufgabe konnte nicht gespeichert werden (ID fehlt).'));
return; return;
} }
@@ -501,11 +367,11 @@ export default function MobileEventTasksPage() {
setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id))); setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id)));
setShowTaskSheet(false); setShowTaskSheet(false);
setNewTask({ id: null, title: '', description: '', emotion_id: '', tenant_id: null }); setNewTask({ id: null, title: '', description: '', emotion_id: '', tenant_id: null });
toast.success(t('events.tasks.created', 'Aufgabe gespeichert')); toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert'));
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Aufgabe konnte nicht erstellt werden.'))); setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht erstellt werden.')));
toast.error(t('events.errors.saveFailed', 'Aufgabe konnte nicht erstellt werden.')); toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht erstellt werden.'));
} }
} }
} }
@@ -525,11 +391,11 @@ export default function MobileEventTasksPage() {
} }
return next; return next;
}); });
toast.success(t('events.tasks.removed', 'Aufgabe entfernt')); toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt'));
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Aufgabe konnte nicht entfernt werden.'))); setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht entfernt werden.')));
toast.error(t('events.errors.saveFailed', 'Aufgabe konnte nicht entfernt werden.')); toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht entfernt werden.'));
} }
} finally { } finally {
setBusyId(null); setBusyId(null);
@@ -556,10 +422,10 @@ export default function MobileEventTasksPage() {
setAssignedTasks((prev) => prev.filter((task) => !selectedTaskIds.has(task.id))); setAssignedTasks((prev) => prev.filter((task) => !selectedTaskIds.has(task.id)));
setSelectedTaskIds(new Set()); setSelectedTaskIds(new Set());
setSelectionMode(false); setSelectionMode(false);
toast.success(t('events.tasks.removed', 'Aufgabe entfernt')); toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt'));
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
toast.error(t('events.errors.saveFailed', 'Aufgabe konnte nicht entfernt werden.')); toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht entfernt werden.'));
} }
} finally { } finally {
setBulkDeleteBusy(false); setBulkDeleteBusy(false);
@@ -664,10 +530,10 @@ export default function MobileEventTasksPage() {
setAssignedTasks(result.data); setAssignedTasks(result.data);
setBulkLines(''); setBulkLines('');
setShowBulkSheet(false); setShowBulkSheet(false);
toast.success(t('events.tasks.created', 'Aufgabe gespeichert')); toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert'));
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
toast.error(t('events.errors.saveFailed', 'Aufgabe konnte nicht erstellt werden.')); toast.error(t('events.errors.saveFailed', 'Fotoaufgabe konnte nicht erstellt werden.'));
} }
} }
} }
@@ -720,8 +586,8 @@ export default function MobileEventTasksPage() {
setEventRecord(updated); setEventRecord(updated);
toast.success( toast.success(
nextEnabled nextEnabled
? t('events.tasks.toggle.enabled', 'Tasks activated') ? t('events.tasks.toggle.enabled', 'Photo tasks activated')
: t('events.tasks.toggle.disabled', 'Tasks disabled') : t('events.tasks.toggle.disabled', 'Photo tasks disabled')
); );
} catch (err) { } catch (err) {
if (!isAuthError(err)) { if (!isAuthError(err)) {
@@ -735,7 +601,7 @@ export default function MobileEventTasksPage() {
return ( return (
<MobileShell <MobileShell
activeTab="tasks" activeTab="tasks"
title={t('events.tasks.title', 'Tasks & Checklists')} title={t('events.tasks.title', 'Photo tasks & checklists')}
onBack={back} onBack={back}
headerActions={ headerActions={
<XStack space="$2"> <XStack space="$2">
@@ -763,12 +629,12 @@ export default function MobileEventTasksPage() {
<MobileCard space="$3"> <MobileCard space="$3">
<YStack space="$1"> <YStack space="$1">
<Text fontSize="$sm" fontWeight="800" color={text}> <Text fontSize="$sm" fontWeight="800" color={text}>
{t('events.tasks.toggle.title', 'Tasks for this event')} {t('events.tasks.toggle.title', 'Photo tasks for this event')}
</Text> </Text>
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{t( {t(
'events.tasks.toggle.description', 'events.tasks.toggle.description',
'Give guests optional prompts and photo ideas.' 'Give guests optional photo tasks and prompts.'
)} )}
</Text> </Text>
</YStack> </YStack>
@@ -780,19 +646,19 @@ export default function MobileEventTasksPage() {
</PillBadge> </PillBadge>
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{tasksEnabled {tasksEnabled
? t('events.tasks.toggle.onLabel', 'Guests see task prompts') ? t('events.tasks.toggle.onLabel', 'Guests see photo tasks')
: t('events.tasks.toggle.offLabel', 'Guest app shows photos only')} : t('events.tasks.toggle.offLabel', 'Guest app shows photos only')}
</Text> </Text>
</XStack> </XStack>
<XStack alignItems="center" justifyContent="space-between" marginTop="$2"> <XStack alignItems="center" justifyContent="space-between" marginTop="$2">
<Text fontSize="$xs" color={text} fontWeight="600"> <Text fontSize="$xs" color={text} fontWeight="600">
{t('events.tasks.toggle.switchLabel', 'Tasks enabled')} {t('events.tasks.toggle.switchLabel', 'Photo tasks enabled')}
</Text> </Text>
<Switch <Switch
size="$4" size="$4"
checked={tasksEnabled} checked={tasksEnabled}
onCheckedChange={handleTasksToggle} onCheckedChange={handleTasksToggle}
aria-label={t('events.tasks.toggle.switchLabel', 'Tasks enabled')} aria-label={t('events.tasks.toggle.switchLabel', 'Photo tasks enabled')}
disabled={!canManageTasks || tasksToggleBusy} disabled={!canManageTasks || tasksToggleBusy}
> >
<Switch.Thumb /> <Switch.Thumb />
@@ -800,79 +666,87 @@ export default function MobileEventTasksPage() {
</XStack> </XStack>
{isMember && !canManageTasks ? ( {isMember && !canManageTasks ? (
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{t('events.tasks.toggle.permissionHint', 'You do not have permission to change tasks.')} {t('events.tasks.toggle.permissionHint', 'You do not have permission to change photo tasks.')}
</Text> </Text>
) : null} ) : null}
</MobileCard> </MobileCard>
) : null} ) : null}
{!loading ? ( {!loading ? (
<TaskSummaryCard summary={summary} /> <YStack space="$2">
) : null} <Card
borderRadius={22}
{!loading ? ( borderWidth={2}
<Card borderColor={border}
borderRadius={22} backgroundColor={surface}
borderWidth={2} padding="$3"
borderColor={border} >
backgroundColor={surface} <YStack space="$2.5">
padding="$3" <XStack alignItems="center" justifyContent="space-between">
> <XStack
<YStack space="$2.5"> alignItems="center"
<XStack alignItems="center" justifyContent="space-between"> paddingHorizontal="$3"
<XStack paddingVertical="$1.5"
alignItems="center" borderRadius={999}
paddingHorizontal="$3" borderWidth={1}
paddingVertical="$1.5" borderColor={border}
borderRadius={999} backgroundColor={surfaceMuted}
borderWidth={1} >
borderColor={border} <Text fontSize="$xs" fontWeight="800" color={text}>
backgroundColor={surfaceMuted} {t('events.tasks.quickNav', 'Quick jump')}
> </Text>
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('events.tasks.quickNav', 'Quick jump')}
</Text>
</XStack>
</XStack>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<ToggleGroup
type="single"
value={quickNavSelection}
onValueChange={(next) => {
const key = next as TaskSectionKey | '';
if (!key) {
return;
}
setQuickNavSelection(key);
handleQuickNav(key);
}}
>
<XStack space="$2" paddingVertical="$1">
{sectionCounts.map((section) => (
<QuickNavChip
key={section.key}
value={section.key}
label={t(`events.tasks.sections.${section.key}`, section.key)}
count={section.count}
onPress={() => {
setQuickNavSelection(section.key);
handleQuickNav(section.key);
}}
isActive={quickNavSelection === section.key}
/>
))}
</XStack> </XStack>
</ToggleGroup> </XStack>
</ScrollView>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<ToggleGroup
type="single"
value={quickNavSelection}
onValueChange={(next) => {
const key = next as TaskSectionKey | '';
if (!key) {
return;
}
setQuickNavSelection(key);
handleQuickNav(key);
}}
>
<XStack space="$2" paddingVertical="$1">
{sectionCounts.map((section) => (
<QuickNavChip
key={section.key}
value={section.key}
label={t(`events.tasks.sections.${section.key}`, section.key)}
count={section.count}
onPress={() => {
setQuickNavSelection(section.key);
handleQuickNav(section.key);
}}
isActive={quickNavSelection === section.key}
/>
))}
</XStack>
</ToggleGroup>
</ScrollView>
</YStack>
</Card>
<MobileCard
padding="$3"
space="$0"
style={{
position: 'sticky',
top: 'calc(env(safe-area-inset-top, 0px) + 76px)',
zIndex: 45,
}}
>
<XStack alignItems="center" space="$2"> <XStack alignItems="center" space="$2">
<XStack flex={1}> <XStack flex={1}>
<MobileInput <MobileInput
type="search" type="search"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('events.tasks.search', 'Search tasks')} placeholder={t('events.tasks.search', 'Search photo tasks')}
compact compact
/> />
</XStack> </XStack>
@@ -899,8 +773,8 @@ export default function MobileEventTasksPage() {
</XStack> </XStack>
</Pressable> </Pressable>
</XStack> </XStack>
</YStack> </MobileCard>
</Card> </YStack>
) : null} ) : null}
{loading ? ( {loading ? (
@@ -913,19 +787,19 @@ export default function MobileEventTasksPage() {
<YStack space="$2"> <YStack space="$2">
<MobileCard space="$2"> <MobileCard space="$2">
<Text fontSize={13} fontWeight="700" color={text}> <Text fontSize={13} fontWeight="700" color={text}>
{t('events.tasks.emptyTitle', 'No tasks yet')} {t('events.tasks.emptyTitle', 'No photo tasks yet')}
</Text> </Text>
<Text fontSize={12} color={muted}> <Text fontSize={12} color={muted}>
{t('events.tasks.emptyBody', 'Create tasks or import a pack for your event.')} {t('events.tasks.emptyBody', 'Create photo tasks or import a pack for your event.')}
</Text> </Text>
<XStack space="$2"> <XStack space="$2">
<CTAButton <CTAButton
label={t('events.tasks.emptyActionTask', 'Add task')} label={t('events.tasks.emptyActionTask', 'Add photo task')}
onPress={() => setShowTaskSheet(true)} onPress={() => setShowTaskSheet(true)}
fullWidth={false} fullWidth={false}
/> />
<CTAButton <CTAButton
label={t('events.tasks.emptyActionPack', 'Import pack')} label={t('events.tasks.emptyActionPack', 'Import photo task pack')}
tone="ghost" tone="ghost"
onPress={() => setShowCollectionSheet(true)} onPress={() => setShowCollectionSheet(true)}
fullWidth={false} fullWidth={false}
@@ -951,13 +825,13 @@ export default function MobileEventTasksPage() {
<Plus size={14} color={surface} /> <Plus size={14} color={surface} />
</YStack> </YStack>
<Text fontSize={12.5} fontWeight="700" color={text}> <Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.addTask', 'Aufgabe hinzufügen')} {t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
</Text> </Text>
</XStack> </XStack>
} }
subTitle={ subTitle={
<Text fontSize={11.5} color={muted}> <Text fontSize={11.5} color={muted}>
{t('events.tasks.addTaskHint', 'Erstelle eine neue Aufgabe für dieses Event.')} {t('events.tasks.addTaskHint', 'Erstelle eine neue Fotoaufgabe für dieses Event.')}
</Text> </Text>
} }
paddingVertical="$2" paddingVertical="$2"
@@ -983,7 +857,7 @@ export default function MobileEventTasksPage() {
<Plus size={14} color={surface} /> <Plus size={14} color={surface} />
</YStack> </YStack>
<Text fontSize={12.5} fontWeight="700" color={text}> <Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.import', 'Aufgabenpaket importieren')} {t('events.tasks.import', 'Fotoaufgabenpaket importieren')}
</Text> </Text>
</XStack> </XStack>
} }
@@ -1003,7 +877,7 @@ export default function MobileEventTasksPage() {
<YStack space="$2"> <YStack space="$2">
<YStack ref={assignedRef} /> <YStack ref={assignedRef} />
<Text fontSize="$sm" color={muted}> <Text fontSize="$sm" color={muted}>
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })} {t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })}
</Text> </Text>
{selectionMode ? ( {selectionMode ? (
<MobileCard padding="$2.5" space="$2"> <MobileCard padding="$2.5" space="$2">
@@ -1046,7 +920,7 @@ export default function MobileEventTasksPage() {
checked={selectedTaskIds.has(task.id)} checked={selectedTaskIds.has(task.id)}
onCheckedChange={() => toggleSelectedTask(task.id)} onCheckedChange={() => toggleSelectedTask(task.id)}
onPress={(event: any) => event?.stopPropagation?.()} onPress={(event: any) => event?.stopPropagation?.()}
aria-label={t('events.tasks.select', 'Select task')} aria-label={t('events.tasks.select', 'Select photo task')}
> >
<Checkbox.Indicator> <Checkbox.Indicator>
<Check size={14} color={text} /> <Check size={14} color={text} />
@@ -1078,7 +952,7 @@ export default function MobileEventTasksPage() {
borderWidth={1} borderWidth={1}
borderColor={`${danger}33`} borderColor={`${danger}33`}
icon={<Trash2 size={14} color={dangerText} />} icon={<Trash2 size={14} color={dangerText} />}
aria-label={t('events.tasks.remove', 'Remove task')} aria-label={t('events.tasks.remove', 'Remove photo task')}
disabled={busyId === task.id} disabled={busyId === task.id}
onPress={(event: any) => { onPress={(event: any) => {
event?.stopPropagation?.(); event?.stopPropagation?.();
@@ -1098,11 +972,11 @@ export default function MobileEventTasksPage() {
<XStack justifyContent="space-between" alignItems="center" marginTop="$2"> <XStack justifyContent="space-between" alignItems="center" marginTop="$2">
<YStack ref={libraryRef} /> <YStack ref={libraryRef} />
<Text fontSize={12.5} fontWeight="600" color={text}> <Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.library', 'Weitere Aufgaben')} {t('events.tasks.library', 'Weitere Fotoaufgaben')}
</Text> </Text>
<Pressable onPress={() => setShowCollectionSheet(true)}> <Pressable onPress={() => setShowCollectionSheet(true)}>
<Text fontSize={12} fontWeight="600" color={primary}> <Text fontSize={12} fontWeight="600" color={primary}>
{t('events.tasks.import', 'Import Pack')} {t('events.tasks.import', 'Import photo task pack')}
</Text> </Text>
</Pressable> </Pressable>
</XStack> </XStack>
@@ -1113,7 +987,7 @@ export default function MobileEventTasksPage() {
</Pressable> </Pressable>
{library.length === 0 ? ( {library.length === 0 ? (
<Text fontSize={12} fontWeight="500" color={subtle}> <Text fontSize={12} fontWeight="500" color={subtle}>
{t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')} {t('events.tasks.libraryEmpty', 'Keine weiteren Fotoaufgaben verfügbar.')}
</Text> </Text>
) : ( ) : (
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}> <YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
@@ -1137,9 +1011,12 @@ export default function MobileEventTasksPage() {
iconAfter={ iconAfter={
<XStack space="$1.5" alignItems="center"> <XStack space="$1.5" alignItems="center">
<Pressable onPress={() => quickAssign(task.id)}> <Pressable onPress={() => quickAssign(task.id)}>
<Text fontSize={12} fontWeight="600" color={primary}> <XStack alignItems="center" space="$1">
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')} <Plus size={14} color={primary} />
</Text> <Text fontSize={12} fontWeight="600" color={primary}>
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
</Text>
</XStack>
</Pressable> </Pressable>
<ChevronRight size={14} color={subtle} /> <ChevronRight size={14} color={subtle} />
</XStack> </XStack>
@@ -1157,7 +1034,7 @@ export default function MobileEventTasksPage() {
<MobileSheet <MobileSheet
open={showCollectionSheet} open={showCollectionSheet}
onClose={() => setShowCollectionSheet(false)} onClose={() => setShowCollectionSheet(false)}
title={t('events.tasks.import', 'Aufgabenpaket importieren')} title={t('events.tasks.import', 'Fotoaufgabenpaket importieren')}
footer={null} footer={null}
> >
<YStack space="$2"> <YStack space="$2">
@@ -1214,9 +1091,9 @@ export default function MobileEventTasksPage() {
<MobileSheet <MobileSheet
open={showTaskSheet} open={showTaskSheet}
onClose={() => setShowTaskSheet(false)} onClose={() => setShowTaskSheet(false)}
title={t('events.tasks.addTask', 'Aufgabe hinzufügen')} title={t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
footer={ footer={
<CTAButton label={t('events.tasks.saveTask', 'Aufgabe speichern')} onPress={() => createNewTask()} /> <CTAButton label={t('events.tasks.saveTask', 'Fotoaufgabe speichern')} onPress={() => createNewTask()} />
} }
> >
<YStack space="$2"> <YStack space="$2">
@@ -1257,11 +1134,11 @@ export default function MobileEventTasksPage() {
open={showBulkSheet} open={showBulkSheet}
onClose={() => setShowBulkSheet(false)} onClose={() => setShowBulkSheet(false)}
title={t('events.tasks.bulkAdd', 'Bulk add')} title={t('events.tasks.bulkAdd', 'Bulk add')}
footer={<CTAButton label={t('events.tasks.saveTask', 'Aufgabe speichern')} onPress={() => handleBulkAdd()} />} footer={<CTAButton label={t('events.tasks.saveTask', 'Fotoaufgabe speichern')} onPress={() => handleBulkAdd()} />}
> >
<YStack space="$2"> <YStack space="$2">
<Text fontSize={12} color={muted}> <Text fontSize={12} color={muted}>
{t('events.tasks.bulkHint', 'One task per line. These will be created and added to the event.')} {t('events.tasks.bulkHint', 'One photo task per line. These will be created and added to the event.')}
</Text> </Text>
<MobileTextArea <MobileTextArea
value={bulkLines} value={bulkLines}
@@ -1400,14 +1277,14 @@ export default function MobileEventTasksPage() {
<YStack space="$3"> <YStack space="$3">
<AlertDialog.Title> <AlertDialog.Title>
<Text fontSize="$md" fontWeight="800" color={text}> <Text fontSize="$md" fontWeight="800" color={text}>
{t('events.tasks.removeTitle', 'Remove task?')} {t('events.tasks.removeTitle', 'Remove photo task?')}
</Text> </Text>
</AlertDialog.Title> </AlertDialog.Title>
<AlertDialog.Description> <AlertDialog.Description>
<Text fontSize="$sm" color={muted}> <Text fontSize="$sm" color={muted}>
{deleteCandidate {deleteCandidate
? t('events.tasks.removeBody', 'This will remove "{{title}}" from the event.', { title: deleteCandidate.title }) ? t('events.tasks.removeBody', 'This will remove "{{title}}" from the event.', { title: deleteCandidate.title })
: t('events.tasks.removeBodyFallback', 'This will remove the task from the event.')} : t('events.tasks.removeBodyFallback', 'This will remove the photo task from the event.')}
</Text> </Text>
</AlertDialog.Description> </AlertDialog.Description>
<XStack space="$2" justifyContent="flex-end"> <XStack space="$2" justifyContent="flex-end">
@@ -1460,7 +1337,7 @@ export default function MobileEventTasksPage() {
</AlertDialog.Title> </AlertDialog.Title>
<AlertDialog.Description> <AlertDialog.Description>
<Text fontSize="$sm" color={muted}> <Text fontSize="$sm" color={muted}>
{t('events.tasks.bulkRemoveBody', 'This will remove the selected tasks from the event.')} {t('events.tasks.bulkRemoveBody', 'This will remove the selected photo tasks from the event.')}
</Text> </Text>
</AlertDialog.Description> </AlertDialog.Description>
<XStack space="$2" justifyContent="flex-end"> <XStack space="$2" justifyContent="flex-end">
@@ -1506,7 +1383,7 @@ export default function MobileEventTasksPage() {
pressTheme pressTheme
title={ title={
<Text fontSize={12.5} fontWeight="600" color={text}> <Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.addTask', 'Aufgabe hinzufügen')} {t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
</Text> </Text>
} }
onPress={() => { onPress={() => {

View File

@@ -264,14 +264,12 @@ vi.mock('../theme', () => ({
import MobileEventTasksPage from '../EventTasksPage'; import MobileEventTasksPage from '../EventTasksPage';
describe('MobileEventTasksPage', () => { describe('MobileEventTasksPage', () => {
it('renders the task overview summary and quick jump chips', async () => { it('renders the quick jump chips and photo task header', async () => {
render(<MobileEventTasksPage />); render(<MobileEventTasksPage />);
expect(await screen.findByText('Tasks for this event')).toBeInTheDocument(); expect(await screen.findByText('Photo tasks for this event')).toBeInTheDocument();
expect(await screen.findByText('Task overview')).toBeInTheDocument();
expect(screen.getByText('Tasks total')).toBeInTheDocument();
expect(screen.getByText('Quick jump')).toBeInTheDocument(); expect(screen.getByText('Quick jump')).toBeInTheDocument();
expect(screen.getByText('Assigned')).toBeInTheDocument(); expect(screen.getByText('assigned')).toBeInTheDocument();
expect(api.getTaskCollections).toHaveBeenCalledWith( expect(api.getTaskCollections).toHaveBeenCalledWith(
expect.objectContaining({ event_type: 'wedding' }), expect.objectContaining({ event_type: 'wedding' }),