fixed event join token handling in the event admin. created new seeders with new tenants and package purchases. added new playwright test scenarios.

This commit is contained in:
Codex Agent
2025-10-26 14:44:47 +01:00
parent 6290a3a448
commit ecf5a23b28
59 changed files with 3900 additions and 691 deletions

View File

@@ -18,16 +18,37 @@ export type EventJoinTokenLayout = {
download_urls: Record<string, string>;
};
export type TenantEventType = {
id: number;
slug: string;
name: string;
name_translations: Record<string, string>;
icon: string | null;
settings: Record<string, unknown>;
created_at?: string | null;
updated_at?: string | null;
};
export type TenantEvent = {
id: number;
name: string | Record<string, string>;
slug: string;
event_date: string | null;
event_type_id: number | null;
event_type: TenantEventType | null;
status: 'draft' | 'published' | 'archived';
is_active?: boolean;
description?: string | null;
photo_count?: number;
like_count?: number;
package?: {
id: number | string | null;
name: string | null;
price: number | null;
purchased_at: string | null;
expires_at: string | null;
} | null;
[key: string]: unknown;
};
export type TenantPhoto = {
@@ -208,8 +229,8 @@ export type EventMember = {
avatar_url?: string | null;
};
type EventListResponse = { data?: TenantEvent[] };
type EventResponse = { data: TenantEvent };
type EventListResponse = { data?: JsonValue[] };
type EventResponse = { data: JsonValue };
export type EventJoinToken = {
id: number;
@@ -226,13 +247,14 @@ export type EventJoinToken = {
layouts: EventJoinTokenLayout[];
layouts_url: string | null;
};
type CreatedEventResponse = { message: string; data: TenantEvent; balance: number };
type CreatedEventResponse = { message: string; data: JsonValue; balance: number };
type PhotoResponse = { message: string; data: TenantPhoto };
type EventSavePayload = {
name: string;
slug: string;
date?: string;
event_type_id: number;
event_date?: string;
status?: 'draft' | 'published' | 'archived';
is_active?: boolean;
package_id?: number;
@@ -322,13 +344,51 @@ function pickTranslatedText(translations: Record<string, string>, fallback: stri
return fallback;
}
function normalizeEvent(event: TenantEvent): TenantEvent {
function normalizeEventType(raw: JsonValue | TenantEventType | null): TenantEventType | null {
if (!raw) {
return null;
}
const translations = normalizeTranslationMap((raw as JsonValue).name ?? {}, undefined, true);
const fallback = typeof (raw as JsonValue).name === 'string' ? (raw as JsonValue).name : 'Event';
return {
...event,
is_active: typeof event.is_active === 'boolean' ? event.is_active : undefined,
id: Number((raw as JsonValue).id ?? 0),
slug: String((raw as JsonValue).slug ?? ''),
name: pickTranslatedText(translations, fallback ?? 'Event'),
name_translations: translations,
icon: ((raw as JsonValue).icon ?? null) as string | null,
settings: ((raw as JsonValue).settings ?? {}) as Record<string, unknown>,
created_at: (raw as JsonValue).created_at ?? null,
updated_at: (raw as JsonValue).updated_at ?? null,
};
}
function normalizeEvent(event: JsonValue): TenantEvent {
const normalizedType = normalizeEventType(event.event_type ?? event.eventType ?? null);
const normalized: TenantEvent = {
...(event as Record<string, unknown>),
id: Number(event.id ?? 0),
name: event.name ?? '',
slug: String(event.slug ?? ''),
event_date: typeof event.event_date === 'string'
? event.event_date
: (typeof event.date === 'string' ? event.date : null),
event_type_id: event.event_type_id !== undefined && event.event_type_id !== null
? Number(event.event_type_id)
: null,
event_type: normalizedType,
status: (event.status ?? 'draft') as TenantEvent['status'],
is_active: typeof event.is_active === 'boolean' ? event.is_active : undefined,
description: event.description ?? null,
photo_count: event.photo_count !== undefined ? Number(event.photo_count ?? 0) : undefined,
like_count: event.like_count !== undefined ? Number(event.like_count ?? 0) : undefined,
package: event.package ?? null,
};
return normalized;
}
function normalizePhoto(photo: TenantPhoto): TenantPhoto {
return {
id: photo.id,
@@ -574,6 +634,15 @@ export async function getEvent(slug: string): Promise<TenantEvent> {
return normalizeEvent(data.data);
}
export async function getEventTypes(): Promise<TenantEventType[]> {
const response = await authorizedFetch('/api/v1/tenant/event-types');
const data = await jsonOrThrow<{ data?: JsonValue[] }>(response, 'Failed to load event types');
const rows = Array.isArray(data.data) ? data.data : [];
return rows
.map((row) => normalizeEventType(row))
.filter((row): row is TenantEventType => Boolean(row));
}
export async function getEventPhotos(slug: string): Promise<TenantPhoto[]> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos`);
const data = await jsonOrThrow<{ data?: TenantPhoto[] }>(response, 'Failed to load photos');
@@ -602,7 +671,7 @@ export async function deletePhoto(slug: string, id: number): Promise<void> {
export async function toggleEvent(slug: string): Promise<TenantEvent> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/toggle`, { method: 'POST' });
const data = await jsonOrThrow<{ message: string; data: TenantEvent }>(response, 'Failed to toggle event');
const data = await jsonOrThrow<{ message: string; data: JsonValue }>(response, 'Failed to toggle event');
return normalizeEvent(data.data);
}
@@ -621,7 +690,7 @@ export async function getEventStats(slug: string): Promise<EventStats> {
export async function getEventJoinTokens(slug: string): Promise<EventJoinToken[]> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens`);
const payload = await jsonOrThrow<{ data: JsonValue[] }>(response, 'Failed to load join tokens');
const payload = await jsonOrThrow<{ data: JsonValue[] }>(response, 'Failed to load invitations');
const list = Array.isArray(payload.data) ? payload.data : [];
return list.map(normalizeJoinToken);
}
@@ -636,7 +705,7 @@ export async function createInviteLink(
headers: { 'Content-Type': 'application/json' },
body,
});
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to create invite');
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to create invitation');
return normalizeJoinToken(data.data ?? {});
}
@@ -651,7 +720,7 @@ export async function revokeEventJoinToken(
options.body = JSON.stringify({ reason });
}
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens/${tokenId}`, options);
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to revoke join token');
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to revoke invitation');
return normalizeJoinToken(data.data ?? {});
}