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

@@ -2,14 +2,14 @@ import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { CalendarDays, ChevronDown, MapPin, Save, Check } from 'lucide-react';
import { CalendarDays, MapPin, Save, Check } from 'lucide-react';
import { isPast, isSameDay, parseISO, startOfDay } from 'date-fns';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Switch } from '@tamagui/switch';
import { MobileShell } from './components/MobileShell';
import { MobileCard, CTAButton, FloatingActionButton } from './components/Primitives';
import { MobileDateTimeInput, MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import { MobileDateInput, MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import { LegalConsentSheet } from './components/LegalConsentSheet';
import {
createEvent,
@@ -31,7 +31,6 @@ import { getApiErrorMessage, getApiValidationMessage, isApiError } from '../lib/
import toast from 'react-hot-toast';
import { useBackNavigation } from './hooks/useBackNavigation';
import { useAdminTheme } from './theme';
import { withAlpha } from './components/colors';
import { useAuth } from '../auth/context';
import { useEventContext } from '../context/EventContext';
@@ -138,7 +137,7 @@ export default function MobileEventFormPage() {
const handleDateChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (isEventCompleted) return;
setForm((prev) => ({ ...prev, date: event.target.value }));
setForm((prev) => ({ ...prev, date: normalizeEventDateTime(event.target.value, prev.date) }));
},
[isEventCompleted],
);
@@ -258,11 +257,12 @@ export default function MobileEventFormPage() {
async function handleSubmit() {
setSaving(true);
setError(null);
const normalizedEventDate = normalizeEventDateTime(form.date);
try {
if (isEdit && slug) {
const updated = await updateEvent(slug, {
name: form.name,
event_date: form.date || undefined,
event_date: normalizedEventDate || undefined,
event_type_id: form.eventTypeId ?? undefined,
status: form.published ? 'published' : 'draft',
settings: {
@@ -280,7 +280,7 @@ export default function MobileEventFormPage() {
name: form.name || t('eventForm.fields.name.fallback', 'Event'),
slug: `${Date.now()}`,
event_type_id: form.eventTypeId ?? undefined,
event_date: form.date || undefined,
event_date: normalizedEventDate || undefined,
status: form.published ? 'published' : 'draft',
package_id: isSuperAdmin ? form.packageId ?? undefined : undefined,
service_package_slug: form.servicePackageSlug ?? undefined,
@@ -314,7 +314,7 @@ export default function MobileEventFormPage() {
name: form.name || t('eventForm.fields.name.fallback', 'Event'),
slug: `${Date.now()}`,
event_type_id: form.eventTypeId ?? undefined,
event_date: form.date || undefined,
event_date: normalizedEventDate || undefined,
status: form.published ? 'published' : 'draft',
package_id: isSuperAdmin ? form.packageId ?? undefined : undefined,
service_package_slug: form.servicePackageSlug ?? undefined,
@@ -374,6 +374,20 @@ export default function MobileEventFormPage() {
}
}
const requiredLabel = React.useCallback(
(label: string) => (
<XStack alignItems="center" space="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{label}
</Text>
<Text fontSize="$sm" fontWeight="800" color={danger}>
*
</Text>
</XStack>
),
[danger, text],
);
return (
<MobileShell
activeTab="home"
@@ -389,7 +403,7 @@ export default function MobileEventFormPage() {
) : null}
<MobileCard space="$3">
<MobileField label={t('eventForm.fields.name.label', 'Event name')}>
<MobileField label={requiredLabel(t('eventForm.fields.name.label', 'Event name'))}>
<MobileInput
type="text"
value={form.name}
@@ -451,10 +465,10 @@ export default function MobileEventFormPage() {
</MobileField>
) : null}
<MobileField label={t('eventForm.fields.date.label', 'Date & time')}>
<MobileField label={requiredLabel(t('eventForm.fields.date.label', 'Date & time'))}>
<XStack alignItems="center" space="$2">
<MobileDateTimeInput
value={form.date}
<MobileDateInput
value={extractDateValue(form.date)}
onChange={handleDateChange}
style={{ flex: 1 }}
disabled={isEventCompleted}
@@ -463,7 +477,7 @@ export default function MobileEventFormPage() {
</XStack>
</MobileField>
<MobileField label={t('eventForm.fields.type.label', 'Event type')}>
<MobileField label={requiredLabel(t('eventForm.fields.type.label', 'Event type'))}>
{typesLoading ? (
<Text fontSize="$sm" color={muted}>{t('eventForm.fields.type.loading', 'Loading event types…')}</Text>
) : eventTypes.length === 0 ? (
@@ -589,9 +603,9 @@ export default function MobileEventFormPage() {
<YStack space="$2" paddingBottom="$10">
{!isEdit ? (
<CTAButton
label={t('eventForm.actions.saveDraft', 'Save as draft')}
label={t('eventForm.actions.create', 'Create event')}
tone="ghost"
onPress={back}
onPress={() => handleSubmit()}
/>
) : null}
</YStack>
@@ -663,6 +677,30 @@ function isWaiverRequiredError(error: unknown): boolean {
return 'accepted_waiver' in metaErrors;
}
const DEFAULT_EVENT_TIME = '12:00';
function normalizeEventDateTime(value: string, previousValue?: string): string {
if (!value) {
return '';
}
const [datePart, timePartRaw] = value.split('T');
if (!datePart) {
return value;
}
const nextTime = timePartRaw?.slice(0, 5);
if (!nextTime || nextTime === '00:00') {
const previousTime = previousValue?.split('T')[1]?.slice(0, 5);
if (previousTime && previousTime !== '00:00') {
return `${datePart}T${previousTime}`;
}
return `${datePart}T${DEFAULT_EVENT_TIME}`;
}
return `${datePart}T${nextTime}`;
}
function toDateTimeLocal(value?: string | null): string {
if (!value) return '';
@@ -674,6 +712,14 @@ function toDateTimeLocal(value?: string | null): string {
return fallback.length >= 16 ? fallback.slice(0, 16) : '';
}
function extractDateValue(value?: string | null): string {
if (!value) {
return '';
}
return value.split('T')[0] ?? '';
}
function resolveLocation(event: TenantEvent): string {
const settings = (event.settings ?? {}) as Record<string, unknown>;
const candidate =