Enforce task limits and update event form
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-21 09:49:30 +01:00
parent 0b1430e64d
commit 1c5412e82c
15 changed files with 491 additions and 52 deletions

View File

@@ -201,6 +201,25 @@ export default function MobileEventTasksPage() {
);
const tasksEnabled = resolveEngagementMode(permissionSource ?? null) !== 'photo_only';
const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]);
const maxTasks = React.useMemo(() => {
const limit = eventRecord?.limits?.tasks?.limit;
return typeof limit === 'number' && Number.isFinite(limit) ? limit : null;
}, [eventRecord?.limits?.tasks?.limit]);
const remainingTasks = React.useMemo(() => {
if (maxTasks === null) {
return null;
}
return Math.max(0, maxTasks - assignedTasks.length);
}, [assignedTasks.length, maxTasks]);
const canAddTasks = maxTasks === null || (remainingTasks ?? 0) > 0;
const limitReachedMessage = t('events.tasks.limitReached', 'Photo task limit reached.');
const limitReachedHint =
maxTasks === null
? null
: t('events.tasks.limitReachedHint', {
count: maxTasks,
defaultValue: 'This event allows up to {{count}} photo tasks.',
});
React.useEffect(() => {
if (slugParam && activeEvent?.slug !== slugParam) {
selectEvent(slugParam);
@@ -310,6 +329,10 @@ export default function MobileEventTasksPage() {
async function quickAssign(taskId: number) {
if (!eventId) return;
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
setAssigningId(taskId);
try {
await assignTasksToEvent(eventId, [taskId]);
@@ -329,6 +352,10 @@ export default function MobileEventTasksPage() {
async function importCollection(collectionId: number) {
if (!slug || !eventId) return;
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
try {
await importTaskCollection(collectionId, slug);
const result = await getEventTasks(eventId, 1);
@@ -346,6 +373,10 @@ export default function MobileEventTasksPage() {
async function createNewTask() {
if (!eventId || !newTask.title.trim()) return;
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
try {
if (newTask.id) {
if (!Number.isFinite(Number(newTask.id))) {
@@ -536,13 +567,32 @@ export default function MobileEventTasksPage() {
async function handleBulkAdd() {
if (!eventId || !bulkLines.trim()) return;
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
const lines = bulkLines
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
if (!lines.length) return;
const capacity = remainingTasks === null ? lines.length : Math.max(0, remainingTasks);
const slicedLines = lines.slice(0, capacity);
if (!slicedLines.length) {
toast.error(limitReachedMessage);
return;
}
try {
for (const line of lines) {
if (slicedLines.length < lines.length) {
toast.error(
t('events.tasks.limitSkipped', {
count: lines.length - slicedLines.length,
defaultValue: 'Skipped {{count}} tasks due to limit.',
}),
);
}
for (const line of slicedLines) {
const created = await createTask({ title: line } as any);
await assignTasksToEvent(eventId, [created.id]);
}
@@ -627,16 +677,24 @@ export default function MobileEventTasksPage() {
<Text fontSize={12} color={muted}>
{t('events.tasks.emptyBody', 'Create photo tasks or import a pack for your event.')}
</Text>
{!canAddTasks ? (
<Text fontSize={12} fontWeight="600" color={dangerText}>
{limitReachedMessage}
{limitReachedHint ? ` ${limitReachedHint}` : ''}
</Text>
) : null}
<XStack space="$2">
<CTAButton
label={t('events.tasks.emptyActionTask', 'Add photo task')}
onPress={() => setShowTaskSheet(true)}
disabled={!canAddTasks}
fullWidth={false}
/>
<CTAButton
label={t('events.tasks.emptyActionPack', 'Import photo task pack')}
tone="ghost"
onPress={() => setShowCollectionSheet(true)}
disabled={!canAddTasks}
fullWidth={false}
/>
</XStack>
@@ -646,18 +704,24 @@ export default function MobileEventTasksPage() {
<ListItem
hoverTheme
pressTheme
onPress={() => setShowTaskSheet(true)}
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
setShowTaskSheet(true);
}}
title={
<XStack alignItems="center" space="$2">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor={primary}
backgroundColor={canAddTasks ? primary : withAlpha(border, 0.4)}
alignItems="center"
justifyContent="center"
>
<Plus size={14} color={surface} />
<Plus size={14} color={canAddTasks ? surface : muted} />
</YStack>
<Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
@@ -678,18 +742,24 @@ export default function MobileEventTasksPage() {
<ListItem
hoverTheme
pressTheme
onPress={() => setShowCollectionSheet(true)}
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
setShowCollectionSheet(true);
}}
title={
<XStack alignItems="center" space="$2">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor={primary}
backgroundColor={canAddTasks ? primary : withAlpha(border, 0.4)}
alignItems="center"
justifyContent="center"
>
<Plus size={14} color={surface} />
<Plus size={14} color={canAddTasks ? surface : muted} />
</YStack>
<Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.import', 'Fotoaufgabenpaket importieren')}
@@ -845,10 +915,18 @@ export default function MobileEventTasksPage() {
}
iconAfter={
<XStack space="$1.5" alignItems="center">
<Pressable onPress={() => quickAssign(task.id)}>
<Pressable
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
quickAssign(task.id);
}}
>
<XStack alignItems="center" space="$1">
<Plus size={14} color={primary} />
<Text fontSize={12} fontWeight="600" color={primary}>
<Plus size={14} color={canAddTasks ? primary : muted} />
<Text fontSize={12} fontWeight="600" color={canAddTasks ? primary : muted}>
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
</Text>
</XStack>
@@ -1087,6 +1165,12 @@ export default function MobileEventTasksPage() {
footer={null}
>
<YStack space="$2">
{!canAddTasks ? (
<Text fontSize={12} fontWeight="600" color={dangerText}>
{limitReachedMessage}
{limitReachedHint ? ` ${limitReachedHint}` : ''}
</Text>
) : null}
{collections.length > 6 ? (
<Pressable onPress={() => setExpandedCollections((prev) => !prev)}>
<Text fontSize={12} fontWeight="600" color={primary}>
@@ -1119,8 +1203,16 @@ export default function MobileEventTasksPage() {
}
iconAfter={
<XStack space="$1.5" alignItems="center">
<Pressable onPress={() => importCollection(collection.id)}>
<Text fontSize={12} fontWeight="600" color={primary}>
<Pressable
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
importCollection(collection.id);
}}
>
<Text fontSize={12} fontWeight="600" color={canAddTasks ? primary : muted}>
{t('events.tasks.import', 'Import')}
</Text>
</Pressable>
@@ -1142,10 +1234,20 @@ export default function MobileEventTasksPage() {
onClose={() => setShowTaskSheet(false)}
title={t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
footer={
<CTAButton label={t('events.tasks.saveTask', 'Fotoaufgabe speichern')} onPress={() => createNewTask()} />
<CTAButton
label={t('events.tasks.saveTask', 'Fotoaufgabe speichern')}
onPress={() => createNewTask()}
disabled={!canAddTasks}
/>
}
>
<YStack space="$2">
{!canAddTasks ? (
<Text fontSize={12} fontWeight="600" color={dangerText}>
{limitReachedMessage}
{limitReachedHint ? ` ${limitReachedHint}` : ''}
</Text>
) : null}
<MobileField label={t('events.tasks.titleLabel', 'Titel')}>
<MobileInput
type="text"
@@ -1183,9 +1285,30 @@ export default function MobileEventTasksPage() {
open={showBulkSheet}
onClose={() => setShowBulkSheet(false)}
title={t('events.tasks.bulkAdd', 'Bulk add')}
footer={<CTAButton label={t('events.tasks.saveTask', 'Fotoaufgabe speichern')} onPress={() => handleBulkAdd()} />}
footer={
<CTAButton
label={t('events.tasks.saveTask', 'Fotoaufgabe speichern')}
onPress={() => handleBulkAdd()}
disabled={!canAddTasks}
/>
}
>
<YStack space="$2">
{!canAddTasks ? (
<Text fontSize={12} fontWeight="600" color={dangerText}>
{limitReachedMessage}
{limitReachedHint ? ` ${limitReachedHint}` : ''}
</Text>
) : null}
{maxTasks !== null ? (
<Text fontSize={11} color={muted}>
{t('events.tasks.limitRemaining', {
count: remainingTasks ?? 0,
total: maxTasks,
defaultValue: '{{count}} of {{total}} photo tasks remaining.',
})}
</Text>
) : null}
<Text fontSize={12} color={muted}>
{t('events.tasks.bulkHint', 'One photo task per line. These will be created and added to the event.')}
</Text>
@@ -1414,7 +1537,13 @@ export default function MobileEventTasksPage() {
</AlertDialog>
<FloatingActionButton
onPress={() => setShowFabMenu(true)}
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
setShowFabMenu(true);
}}
label={t('events.tasks.add', 'Add')}
icon={Plus}
/>
@@ -1436,6 +1565,10 @@ export default function MobileEventTasksPage() {
</Text>
}
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
setShowFabMenu(false);
setShowTaskSheet(true);
}}
@@ -1454,6 +1587,10 @@ export default function MobileEventTasksPage() {
</Text>
}
onPress={() => {
if (!canAddTasks) {
toast.error(limitReachedMessage);
return;
}
setShowFabMenu(false);
setShowBulkSheet(true);
}}