Door walk-ins were only expressible as an unpaid ticket, which left the cash out of revenue. The new type records the cash payment as paid and makes every field optional, since a walk-in often gives no details: a blank name is logged as "Walk-in", and a confirmation email only goes out when an email is entered. Door tickets reuse paymentStatus 'paid' (the column enum is capped at paid/unpaid/comp) so badges and revenue totals pick them up with no migration; the cash payment row is referenced "Paid at door" to keep them distinguishable from emailed manual tickets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import { fetchApi, API_BASE } from './client';
|
|
import type {
|
|
Ticket,
|
|
Payment,
|
|
BookingData,
|
|
TicketValidationResult,
|
|
TicketSearchResult,
|
|
LiveSearchResult,
|
|
LightningInvoice,
|
|
} from './types';
|
|
|
|
export const ticketsApi = {
|
|
book: (data: BookingData) =>
|
|
fetchApi<{ ticket: Ticket; payment: Payment; message: string }>('/api/tickets', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
getById: (id: string) => fetchApi<{ ticket: Ticket }>(`/api/tickets/${id}`),
|
|
|
|
getAll: (params?: { eventId?: string; status?: string }) => {
|
|
const query = new URLSearchParams();
|
|
if (params?.eventId) query.set('eventId', params.eventId);
|
|
if (params?.status) query.set('status', params.status);
|
|
return fetchApi<{ tickets: Ticket[] }>(`/api/tickets?${query}`);
|
|
},
|
|
|
|
// Validate ticket by QR code (for scanner)
|
|
validate: (code: string, eventId?: string) =>
|
|
fetchApi<TicketValidationResult>('/api/tickets/validate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ code, eventId }),
|
|
}),
|
|
|
|
// Search tickets by name/email (for scanner manual search)
|
|
search: (query: string, eventId?: string) =>
|
|
fetchApi<{ tickets: TicketSearchResult[] }>('/api/tickets/search', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query, eventId }),
|
|
}),
|
|
|
|
// Get event check-in stats (for scanner header counter)
|
|
getCheckinStats: (eventId: string) =>
|
|
fetchApi<{ eventId: string; capacity: number; checkedIn: number; totalActive: number }>(
|
|
`/api/tickets/stats/checkin?eventId=${eventId}`
|
|
),
|
|
|
|
// Live search tickets (GET - for scanner live search with debounce)
|
|
searchLive: (q: string, eventId?: string) => {
|
|
const params = new URLSearchParams();
|
|
params.set('q', q);
|
|
if (eventId) params.set('eventId', eventId);
|
|
return fetchApi<{ tickets: LiveSearchResult[] }>(`/api/tickets/search?${params}`);
|
|
},
|
|
|
|
checkin: (id: string) =>
|
|
fetchApi<{ ticket: Ticket & { attendeeName?: string }; event?: { id: string; title: string }; message: string }>(`/api/tickets/${id}/checkin`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
removeCheckin: (id: string) =>
|
|
fetchApi<{ ticket: Ticket; message: string }>(`/api/tickets/${id}/remove-checkin`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
cancel: (id: string) =>
|
|
fetchApi<{ message: string }>(`/api/tickets/${id}/cancel`, { method: 'POST' }),
|
|
|
|
updateStatus: (id: string, status: string) =>
|
|
fetchApi<{ ticket: Ticket }>(`/api/tickets/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ status }),
|
|
}),
|
|
|
|
updateNote: (id: string, note: string) =>
|
|
fetchApi<{ ticket: Ticket; message: string }>(`/api/tickets/${id}/note`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ note }),
|
|
}),
|
|
|
|
markPaid: (id: string) =>
|
|
fetchApi<{ ticket: Ticket; message: string }>(`/api/tickets/${id}/mark-paid`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
// For manual payment methods (bank_transfer, tpago) - user marks payment as sent
|
|
markPaymentSent: (id: string, payerName?: string) =>
|
|
fetchApi<{ payment: Payment; message: string }>(`/api/tickets/${id}/mark-payment-sent`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ payerName }),
|
|
}),
|
|
|
|
adminCreate: (data: {
|
|
eventId: string;
|
|
firstName: string;
|
|
lastName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
preferredLanguage?: 'en' | 'es';
|
|
autoCheckin?: boolean;
|
|
adminNote?: string;
|
|
}) =>
|
|
fetchApi<{ ticket: Ticket; payment: Payment; message: string }>('/api/tickets/admin/create', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
// Unified add-attendee endpoint behind the single Add Ticket modal
|
|
// (paid = confirmation + QR, door = cash taken at the door, unpaid = pay link +
|
|
// door collection, guest = free comp)
|
|
adminAdd: (data: {
|
|
eventId: string;
|
|
type: 'paid' | 'door' | 'unpaid' | 'guest';
|
|
firstName?: string;
|
|
lastName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
preferredLanguage?: 'en' | 'es';
|
|
checkinNow?: boolean;
|
|
adminNote?: string;
|
|
}) =>
|
|
fetchApi<{ ticket: Ticket; payment: Payment; message: string }>('/api/tickets/admin/add', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
checkPaymentStatus: (ticketId: string) =>
|
|
fetchApi<{ ticketStatus: string; paymentStatus: string; lnbitsStatus?: string; isPaid: boolean }>(
|
|
`/api/lnbits/status/${ticketId}`
|
|
),
|
|
|
|
// Get a Lightning invoice to pay/re-pay a ticket - reuses the stored invoice
|
|
// if it's still valid, otherwise generates a fresh one.
|
|
getLightningInvoice: (ticketId: string) =>
|
|
fetchApi<{ invoice?: LightningInvoice; reused?: boolean; alreadyPaid?: boolean }>(
|
|
`/api/lnbits/invoice/${ticketId}`,
|
|
{ method: 'POST' }
|
|
),
|
|
|
|
// Get PDF download URL (returns the URL, not the PDF itself)
|
|
getPdfUrl: (id: string) => `${API_BASE}/api/tickets/${id}/pdf`,
|
|
};
|