Enforce task limits and update event form
This commit is contained in:
@@ -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);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user