feat: add guest notification center

This commit is contained in:
Codex Agent
2025-11-12 16:56:50 +01:00
parent 062932ce38
commit 4495ac1895
27 changed files with 2042 additions and 64 deletions

View File

@@ -85,6 +85,31 @@ export type TenantEvent = {
[key: string]: unknown;
};
export type GuestNotificationSummary = {
id: number;
type: string;
title: string;
body: string | null;
status: 'draft' | 'active' | 'archived';
audience_scope: 'all' | 'guest';
target_identifier?: string | null;
payload?: Record<string, unknown> | null;
priority: number;
created_at: string | null;
expires_at: string | null;
};
export type SendGuestNotificationPayload = {
title: string;
message: string;
type?: string;
audience?: 'all' | 'guest';
guest_identifier?: string | null;
cta?: { label: string; url: string } | null;
expires_in_minutes?: number | null;
priority?: number | null;
};
export type TenantPhoto = {
id: number;
filename: string;
@@ -968,10 +993,36 @@ function normalizeQrInvite(raw: JsonValue): EventQrInvite {
};
}
function normalizeGuestNotification(raw: JsonValue): GuestNotificationSummary | null {
if (!raw || typeof raw !== 'object') {
return null;
}
const record = raw as Record<string, JsonValue>;
return {
id: Number(record.id ?? 0),
type: typeof record.type === 'string' ? record.type : 'broadcast',
title: typeof record.title === 'string' ? record.title : '',
body: typeof record.body === 'string' ? record.body : null,
status: (record.status as GuestNotificationSummary['status']) ?? 'active',
audience_scope: (record.audience_scope as GuestNotificationSummary['audience_scope']) ?? 'all',
target_identifier: typeof record.target_identifier === 'string' ? record.target_identifier : null,
payload: (record.payload as Record<string, unknown>) ?? null,
priority: Number(record.priority ?? 0),
created_at: typeof record.created_at === 'string' ? record.created_at : null,
expires_at: typeof record.expires_at === 'string' ? record.expires_at : null,
};
}
function eventEndpoint(slug: string): string {
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
}
function guestNotificationsEndpoint(slug: string): string {
return `${eventEndpoint(slug)}/guest-notifications`;
}
function photoboothEndpoint(slug: string): string {
return `${eventEndpoint(slug)}/photobooth`;
}
@@ -1239,6 +1290,41 @@ export async function getEventToolkit(slug: string): Promise<EventToolkit> {
return toolkit;
}
export async function listGuestNotifications(slug: string): Promise<GuestNotificationSummary[]> {
const response = await authorizedFetch(guestNotificationsEndpoint(slug));
const data = await jsonOrThrow<{ data?: JsonValue[] }>(response, 'Failed to load guest notifications');
const rows = Array.isArray(data.data) ? data.data : [];
return rows
.map((row) => normalizeGuestNotification(row))
.filter((row): row is GuestNotificationSummary => Boolean(row));
}
export async function sendGuestNotification(
slug: string,
payload: SendGuestNotificationPayload
): Promise<GuestNotificationSummary> {
const response = await authorizedFetch(guestNotificationsEndpoint(slug), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<{ data?: JsonValue }>(response, 'Failed to send guest notification');
return normalizeGuestNotification(data.data ?? {}) ?? normalizeGuestNotification({
id: 0,
type: payload.type ?? 'broadcast',
title: payload.title,
body: payload.message,
status: 'active',
audience_scope: payload.audience ?? 'all',
target_identifier: payload.guest_identifier ?? null,
payload: payload.cta ? { cta: payload.cta } : null,
priority: payload.priority ?? 0,
created_at: new Date().toISOString(),
expires_at: null,
});
}
export async function getEventPhotoboothStatus(slug: string): Promise<PhotoboothStatus> {
return requestPhotoboothStatus(slug, '', {}, 'Failed to load photobooth status');
}