Event finance - Finance tab on the event page: P&L summary with a revenue-to-result waterfall, costs and other income in one ledger, and the partner split with payouts. Lifecycle stepper (Selling, Adding costs, Ready to close, Finalized) with what is left to do; closing the books goes through a checklist dialog that freezes the numbers. - Expense modal shows only the fields each calculation type needs, a live preview with the event's real counts, and a category suggested from the description. Date inputs follow the UI language. - Global Finance page: profit per event with outliers clipped and labelled, and a "Ready to close" list of past events whose books are still open. - Calculation service (integer PYG, basis points) with pinned regression scenarios; PYG formatting centralised in lib/money with locale-aware separators. Event team permissions - Per-event members with role presets and requireEventPermission; the header stat "Confirmed" is relabelled "Not checked in yet", which is what it counts. Public sales state - One sales state (online, door, sold out, ended, external, cancelled) for the event page, listings and JSON-LD, with the door price and tenders shown only while people can still pay at the door. Frontend unit tests run with vitest (npm test in frontend/). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
import { fetchApi } from './client';
|
|
import type { EmailTemplate, EmailVariable, EmailLog, EmailStats, Pagination } from './types';
|
|
|
|
export const emailsApi = {
|
|
// Templates
|
|
// Team members (non-admin) pass the event they email for, so the server can check their permission there.
|
|
getTemplates: (eventId?: string) =>
|
|
fetchApi<{ templates: EmailTemplate[] }>(`/api/emails/templates${eventId ? `?eventId=${encodeURIComponent(eventId)}` : ''}`),
|
|
|
|
getTemplate: (id: string) => fetchApi<{ template: EmailTemplate }>(`/api/emails/templates/${id}`),
|
|
|
|
createTemplate: (data: Partial<EmailTemplate>) =>
|
|
fetchApi<{ template: EmailTemplate; message: string }>('/api/emails/templates', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
updateTemplate: (id: string, data: Partial<EmailTemplate>) =>
|
|
fetchApi<{ template: EmailTemplate; message: string }>(`/api/emails/templates/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
deleteTemplate: (id: string) =>
|
|
fetchApi<{ message: string }>(`/api/emails/templates/${id}`, { method: 'DELETE' }),
|
|
|
|
getTemplateVariables: (slug: string) =>
|
|
fetchApi<{ variables: EmailVariable[] }>(`/api/emails/templates/${slug}/variables`),
|
|
|
|
// Sending
|
|
sendToEvent: (eventId: string, data: {
|
|
templateSlug: string;
|
|
customVariables?: Record<string, any>;
|
|
recipientFilter?: 'all' | 'confirmed' | 'pending' | 'checked_in';
|
|
}) =>
|
|
fetchApi<{ success: boolean; queuedCount: number; error?: string }>(
|
|
`/api/emails/send/event/${eventId}`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}
|
|
),
|
|
|
|
sendCustom: (data: {
|
|
to: string;
|
|
toName?: string;
|
|
subject: string;
|
|
bodyHtml: string;
|
|
bodyText?: string;
|
|
eventId?: string;
|
|
}) =>
|
|
fetchApi<{ success: boolean; logId?: string; error?: string }>('/api/emails/send/custom', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
preview: (data: {
|
|
templateSlug: string;
|
|
variables?: Record<string, any>;
|
|
locale?: string;
|
|
}, eventId?: string) =>
|
|
fetchApi<{ subject: string; bodyHtml: string }>(`/api/emails/preview${eventId ? `?eventId=${encodeURIComponent(eventId)}` : ''}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
// Logs
|
|
getLogs: (params?: { eventId?: string; status?: string; search?: string; limit?: number; offset?: number }) => {
|
|
const query = new URLSearchParams();
|
|
if (params?.eventId) query.set('eventId', params.eventId);
|
|
if (params?.status) query.set('status', params.status);
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.limit) query.set('limit', params.limit.toString());
|
|
if (params?.offset) query.set('offset', params.offset.toString());
|
|
return fetchApi<{ logs: EmailLog[]; pagination: Pagination }>(`/api/emails/logs?${query}`);
|
|
},
|
|
|
|
getLog: (id: string) => fetchApi<{ log: EmailLog }>(`/api/emails/logs/${id}`),
|
|
|
|
resendLog: (id: string) =>
|
|
fetchApi<{ success: boolean; error?: string }>(`/api/emails/logs/${id}/resend`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
getStats: (eventId?: string) => {
|
|
const query = eventId ? `?eventId=${eventId}` : '';
|
|
return fetchApi<{ stats: EmailStats }>(`/api/emails/stats${query}`);
|
|
},
|
|
|
|
seedTemplates: () =>
|
|
fetchApi<{ message: string }>('/api/emails/seed-templates', { method: 'POST' }),
|
|
};
|