Update partner packages, copy, and demo switcher
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-15 17:33:36 +01:00
parent 2f93271d94
commit ad829ae509
50 changed files with 1335 additions and 411 deletions

View File

@@ -9,7 +9,19 @@ import { MobileShell } from './components/MobileShell';
import { MobileCard, CTAButton } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import { LegalConsentSheet } from './components/LegalConsentSheet';
import { createEvent, getEvent, updateEvent, getEventTypes, getPackages, Package, TenantEvent, TenantEventType, trackOnboarding } from '../api';
import {
createEvent,
getEvent,
updateEvent,
getEventTypes,
getPackages,
getTenantPackagesOverview,
Package,
TenantEvent,
TenantEventType,
TenantPackageSummary,
trackOnboarding,
} from '../api';
import { resolveEventSlugAfterUpdate } from './eventFormNavigation';
import { adminPath } from '../constants';
import { isAuthError } from '../auth/tokens';
@@ -30,6 +42,7 @@ type FormState = {
autoApproveUploads: boolean;
tasksEnabled: boolean;
packageId: number | null;
servicePackageSlug: string | null;
};
export default function MobileEventFormPage() {
@@ -52,11 +65,14 @@ export default function MobileEventFormPage() {
autoApproveUploads: true,
tasksEnabled: true,
packageId: null,
servicePackageSlug: null,
});
const [eventTypes, setEventTypes] = React.useState<TenantEventType[]>([]);
const [typesLoading, setTypesLoading] = React.useState(false);
const [packages, setPackages] = React.useState<Package[]>([]);
const [packagesLoading, setPackagesLoading] = React.useState(false);
const [kontingentOptions, setKontingentOptions] = React.useState<Array<{ slug: string; remaining: number }>>([]);
const [kontingentLoading, setKontingentLoading] = React.useState(false);
const [loading, setLoading] = React.useState(isEdit);
const [saving, setSaving] = React.useState(false);
const [consentOpen, setConsentOpen] = React.useState(false);
@@ -84,6 +100,7 @@ export default function MobileEventFormPage() {
(data.settings?.engagement_mode as string | undefined) !== 'photo_only' &&
(data.engagement_mode as string | undefined) !== 'photo_only',
packageId: null,
servicePackageSlug: null,
});
setError(null);
} catch (err) {
@@ -139,6 +156,75 @@ export default function MobileEventFormPage() {
})();
}, [isSuperAdmin, isEdit]);
React.useEffect(() => {
if (isEdit) {
return;
}
(async () => {
setKontingentLoading(true);
try {
const overview = await getTenantPackagesOverview();
const packages = overview.packages ?? [];
const active = packages.filter((pkg) => pkg.active && pkg.package_type === 'reseller');
const totals = new Map<string, number>();
active.forEach((pkg: TenantPackageSummary) => {
const slugValue = pkg.included_package_slug ?? 'standard';
if (!slugValue) {
return;
}
const remaining = Number.isFinite(pkg.remaining_events as number) ? Number(pkg.remaining_events) : 0;
if (remaining <= 0) {
return;
}
totals.set(slugValue, (totals.get(slugValue) ?? 0) + remaining);
});
const options = Array.from(totals.entries())
.map(([slugValue, remaining]) => ({ slug: slugValue, remaining }))
.sort((a, b) => a.slug.localeCompare(b.slug));
setKontingentOptions(options);
setForm((prev) => {
if (prev.servicePackageSlug || options.length === 0) {
return prev;
}
if (options.length === 1) {
return { ...prev, servicePackageSlug: options[0].slug };
}
const standard = options.find((row) => row.slug === 'standard');
return { ...prev, servicePackageSlug: standard?.slug ?? options[0].slug };
});
} catch {
setKontingentOptions([]);
} finally {
setKontingentLoading(false);
}
})();
}, [isEdit]);
const resolveServiceTierLabel = React.useCallback((slugValue: string) => {
if (slugValue === 'starter') {
return 'Starter';
}
if (slugValue === 'standard') {
return 'Standard';
}
if (slugValue === 'pro') {
return 'Premium';
}
return slugValue;
}, []);
async function handleSubmit() {
setSaving(true);
setError(null);
@@ -165,6 +251,7 @@ export default function MobileEventFormPage() {
event_date: form.date || undefined,
status: form.published ? 'published' : 'draft',
package_id: isSuperAdmin ? form.packageId ?? undefined : undefined,
service_package_slug: form.servicePackageSlug ?? undefined,
settings: {
location: form.location,
guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review',
@@ -188,6 +275,7 @@ export default function MobileEventFormPage() {
event_date: form.date || undefined,
status: form.published ? 'published' : 'draft',
package_id: isSuperAdmin ? form.packageId ?? undefined : undefined,
service_package_slug: form.servicePackageSlug ?? undefined,
settings: {
location: form.location,
guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review',
@@ -283,6 +371,34 @@ export default function MobileEventFormPage() {
</MobileField>
) : null}
{!isEdit && (kontingentLoading || kontingentOptions.length > 0) ? (
<MobileField label={t('eventForm.fields.servicePackage.label', 'Event-Level (Event-Kontingent)')}>
{kontingentLoading ? (
<Text fontSize="$sm" color={muted}>
{t('eventForm.fields.servicePackage.loading', 'Loading Event-Kontingente…')}
</Text>
) : (
<MobileSelect
value={form.servicePackageSlug ?? ''}
onChange={(e) => setForm((prev) => ({ ...prev, servicePackageSlug: String(e.target.value) }))}
>
<option value="">{t('eventForm.fields.servicePackage.placeholder', 'Select tier')}</option>
{kontingentOptions.map((opt) => (
<option key={opt.slug} value={opt.slug}>
{resolveServiceTierLabel(opt.slug)} · {opt.remaining} {t('eventForm.fields.servicePackage.events', 'Events')}
</option>
))}
</MobileSelect>
)}
<Text fontSize="$xs" color={muted}>
{t(
'eventForm.fields.servicePackage.help',
'Wählt das Event-Level. Pro Event wird 1 aus dem passenden Event-Kontingent verbraucht.',
)}
</Text>
</MobileField>
) : null}
<MobileField label={t('eventForm.fields.date.label', 'Date & time')}>
<XStack alignItems="center" space="$2">
<NativeDateTimeInput