Files
Spanglish/frontend/src/lib/api.ts
Michilis bbfaa1172a Add unlisted event status: hidden from listings but accessible by URL
- Backend: add 'unlisted' to schema enum and Zod validation; allow booking for unlisted events
- Frontend: Event type and guards updated; unlisted events bookable, excluded from public listing/sitemap
- Admin: badge, status dropdown, Make Unlisted / Make Public / Unpublish actions; scanner/emails/tickets include unlisted

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 02:21:41 +00:00

1274 lines
35 KiB
TypeScript

const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
export interface ApiError {
error: string;
}
async function fetchApi<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = typeof window !== 'undefined'
? localStorage.getItem('spanglish-token')
: null;
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
};
if (token) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Request failed' }));
const errorMessage = typeof errorData.error === 'string'
? errorData.error
: (errorData.message || JSON.stringify(errorData) || 'Request failed');
throw new Error(errorMessage);
}
return res.json();
}
// Events API
export const eventsApi = {
getAll: (params?: { status?: string; upcoming?: boolean }) => {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.upcoming) query.set('upcoming', 'true');
return fetchApi<{ events: Event[] }>(`/api/events?${query}`);
},
getById: (id: string) => fetchApi<{ event: Event }>(`/api/events/${id}`),
getNextUpcoming: () => fetchApi<{ event: Event | null }>('/api/events/next/upcoming'),
create: (data: Partial<Event>) =>
fetchApi<{ event: Event }>('/api/events', {
method: 'POST',
body: JSON.stringify(data),
}),
update: (id: string, data: Partial<Event>) =>
fetchApi<{ event: Event }>(`/api/events/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: string) =>
fetchApi<{ message: string }>(`/api/events/${id}`, { method: 'DELETE' }),
duplicate: (id: string) =>
fetchApi<{ event: Event; message: string }>(`/api/events/${id}/duplicate`, { method: 'POST' }),
};
// Tickets API
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),
}),
manualCreate: (data: {
eventId: string;
firstName: string;
lastName?: string;
email: string;
phone?: string;
preferredLanguage?: 'en' | 'es';
adminNote?: string;
}) =>
fetchApi<{ ticket: Ticket; payment: Payment; message: string }>('/api/tickets/admin/manual', {
method: 'POST',
body: JSON.stringify(data),
}),
checkPaymentStatus: (ticketId: string) =>
fetchApi<{ ticketStatus: string; paymentStatus: string; lnbitsStatus?: string; isPaid: boolean }>(
`/api/lnbits/status/${ticketId}`
),
// Get PDF download URL (returns the URL, not the PDF itself)
getPdfUrl: (id: string) => `${API_BASE}/api/tickets/${id}/pdf`,
};
// Contacts API
export const contactsApi = {
submit: (data: { name: string; email: string; message: string }) =>
fetchApi<{ message: string }>('/api/contacts', {
method: 'POST',
body: JSON.stringify(data),
}),
subscribe: (email: string, name?: string) =>
fetchApi<{ message: string }>('/api/contacts/subscribe', {
method: 'POST',
body: JSON.stringify({ email, name }),
}),
getAll: (status?: string) => {
const query = status ? `?status=${status}` : '';
return fetchApi<{ contacts: Contact[] }>(`/api/contacts${query}`);
},
updateStatus: (id: string, status: string) =>
fetchApi<{ contact: Contact }>(`/api/contacts/${id}`, {
method: 'PUT',
body: JSON.stringify({ status }),
}),
};
// Users API
export const usersApi = {
getAll: (role?: string) => {
const query = role ? `?role=${role}` : '';
return fetchApi<{ users: User[] }>(`/api/users${query}`);
},
getById: (id: string) => fetchApi<{ user: User }>(`/api/users/${id}`),
update: (id: string, data: Partial<User>) =>
fetchApi<{ user: User }>(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: string) =>
fetchApi<{ message: string }>(`/api/users/${id}`, { method: 'DELETE' }),
};
// Payments API
export const paymentsApi = {
getAll: (params?: { status?: string; provider?: string; pendingApproval?: boolean }) => {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.provider) query.set('provider', params.provider);
if (params?.pendingApproval) query.set('pendingApproval', 'true');
return fetchApi<{ payments: PaymentWithDetails[] }>(`/api/payments?${query}`);
},
getPendingApproval: () =>
fetchApi<{ payments: PaymentWithDetails[] }>('/api/payments/pending-approval'),
update: (id: string, data: { status: string; reference?: string; adminNote?: string }) =>
fetchApi<{ payment: Payment }>(`/api/payments/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
approve: (id: string, adminNote?: string, sendEmail: boolean = true) =>
fetchApi<{ payment: Payment; message: string }>(`/api/payments/${id}/approve`, {
method: 'POST',
body: JSON.stringify({ adminNote, sendEmail }),
}),
reject: (id: string, adminNote?: string, sendEmail: boolean = true) =>
fetchApi<{ payment: Payment; message: string }>(`/api/payments/${id}/reject`, {
method: 'POST',
body: JSON.stringify({ adminNote, sendEmail }),
}),
sendReminder: (id: string) =>
fetchApi<{ message: string; reminderSentAt?: string }>(`/api/payments/${id}/send-reminder`, {
method: 'POST',
}),
updateNote: (id: string, adminNote: string) =>
fetchApi<{ payment: Payment; message: string }>(`/api/payments/${id}/note`, {
method: 'POST',
body: JSON.stringify({ adminNote }),
}),
refund: (id: string) =>
fetchApi<{ message: string }>(`/api/payments/${id}/refund`, { method: 'POST' }),
};
// Payment Options API
export const paymentOptionsApi = {
// Global payment options
getGlobal: () =>
fetchApi<{ paymentOptions: PaymentOptionsConfig }>('/api/payment-options'),
updateGlobal: (data: Partial<PaymentOptionsConfig>) =>
fetchApi<{ paymentOptions: PaymentOptionsConfig; message: string }>('/api/payment-options', {
method: 'PUT',
body: JSON.stringify(data),
}),
// Event-specific options (merged with global)
getForEvent: (eventId: string) =>
fetchApi<{ paymentOptions: PaymentOptionsConfig; hasOverrides: boolean }>(
`/api/payment-options/event/${eventId}`
),
// Event overrides (admin only)
getEventOverrides: (eventId: string) =>
fetchApi<{ overrides: Partial<PaymentOptionsConfig> | null }>(
`/api/payment-options/event/${eventId}/overrides`
),
updateEventOverrides: (eventId: string, data: Partial<PaymentOptionsConfig>) =>
fetchApi<{ overrides: Partial<PaymentOptionsConfig>; message: string }>(
`/api/payment-options/event/${eventId}/overrides`,
{
method: 'PUT',
body: JSON.stringify(data),
}
),
deleteEventOverrides: (eventId: string) =>
fetchApi<{ message: string }>(`/api/payment-options/event/${eventId}/overrides`, {
method: 'DELETE',
}),
};
// Media API
export const mediaApi = {
getAll: (relatedType?: string, relatedId?: string) => {
const params = new URLSearchParams();
if (relatedType) params.set('relatedType', relatedType);
if (relatedId) params.set('relatedId', relatedId);
const query = params.toString();
return fetchApi<{ media: Media[] }>(`/api/media${query ? `?${query}` : ''}`);
},
upload: async (file: File, relatedId?: string, relatedType?: string) => {
const token = typeof window !== 'undefined'
? localStorage.getItem('spanglish-token')
: null;
const formData = new FormData();
formData.append('file', file);
if (relatedId) formData.append('relatedId', relatedId);
if (relatedType) formData.append('relatedType', relatedType);
const res = await fetch(`${API_BASE}/api/media/upload`, {
method: 'POST',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
body: formData,
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Upload failed' }));
throw new Error(errorData.error || 'Upload failed');
}
return res.json() as Promise<{ media: Media; url: string }>;
},
delete: (id: string) =>
fetchApi<{ message: string }>(`/api/media/${id}`, { method: 'DELETE' }),
};
// Admin API
export const adminApi = {
getDashboard: () => fetchApi<{ dashboard: DashboardData }>('/api/admin/dashboard'),
getAnalytics: () => fetchApi<{ analytics: AnalyticsData }>('/api/admin/analytics'),
exportTickets: (eventId?: string) => {
const query = eventId ? `?eventId=${eventId}` : '';
return fetchApi<{ tickets: ExportedTicket[] }>(`/api/admin/export/tickets${query}`);
},
exportFinancial: (params?: { startDate?: string; endDate?: string; eventId?: string }) => {
const query = new URLSearchParams();
if (params?.startDate) query.set('startDate', params.startDate);
if (params?.endDate) query.set('endDate', params.endDate);
if (params?.eventId) query.set('eventId', params.eventId);
return fetchApi<{ payments: ExportedPayment[]; summary: FinancialSummary }>(`/api/admin/export/financial?${query}`);
},
/** Download attendee export as a file (CSV). Returns a Blob. */
exportAttendees: async (eventId: string, params?: { status?: string; format?: string; q?: string }) => {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.format) query.set('format', params.format);
if (params?.q) query.set('q', params.q);
const token = typeof window !== 'undefined'
? localStorage.getItem('spanglish-token')
: null;
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}/api/admin/events/${eventId}/attendees/export?${query}`, { headers });
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Export failed' }));
throw new Error(errorData.error || 'Export failed');
}
const disposition = res.headers.get('Content-Disposition') || '';
const filenameMatch = disposition.match(/filename="?([^"]+)"?/);
const filename = filenameMatch ? filenameMatch[1] : `attendees-${new Date().toISOString().split('T')[0]}.csv`;
const blob = await res.blob();
return { blob, filename };
},
/** Download tickets export as CSV. Returns a Blob. */
exportTicketsCSV: async (eventId: string, params?: { status?: string; q?: string }) => {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.q) query.set('q', params.q);
const token = typeof window !== 'undefined'
? localStorage.getItem('spanglish-token')
: null;
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}/api/admin/events/${eventId}/tickets/export?${query}`, { headers });
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Export failed' }));
throw new Error(errorData.error || 'Export failed');
}
const disposition = res.headers.get('Content-Disposition') || '';
const filenameMatch = disposition.match(/filename="?([^"]+)"?/);
const filename = filenameMatch ? filenameMatch[1] : `tickets-${new Date().toISOString().split('T')[0]}.csv`;
const blob = await res.blob();
return { blob, filename };
},
};
// Emails API
export const emailsApi = {
// Templates
getTemplates: () => fetchApi<{ templates: EmailTemplate[] }>('/api/emails/templates'),
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;
}) =>
fetchApi<{ subject: string; bodyHtml: string }>('/api/emails/preview', {
method: 'POST',
body: JSON.stringify(data),
}),
// Logs
getLogs: (params?: { eventId?: string; status?: 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?.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}`),
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' }),
};
// Types
export interface Event {
id: string;
title: string;
titleEs?: string;
description: string;
descriptionEs?: string;
shortDescription?: string;
shortDescriptionEs?: string;
startDatetime: string;
endDatetime?: string;
location: string;
locationUrl?: string;
price: number;
currency: string;
capacity: number;
status: 'draft' | 'published' | 'unlisted' | 'cancelled' | 'completed' | 'archived';
bannerUrl?: string;
externalBookingEnabled?: boolean;
externalBookingUrl?: string;
bookedCount?: number;
availableSeats?: number;
isFeatured?: boolean;
createdAt: string;
updatedAt: string;
}
export interface Ticket {
id: string;
bookingId?: string; // Groups multiple tickets from same booking
userId: string;
eventId: string;
attendeeFirstName: string;
attendeeLastName?: string;
attendeeEmail?: string;
attendeePhone?: string;
attendeeRuc?: string;
preferredLanguage?: string;
status: 'pending' | 'confirmed' | 'cancelled' | 'checked_in';
checkinAt?: string;
checkedInByAdminId?: string;
qrCode: string;
adminNote?: string;
createdAt: string;
event?: Event;
payment?: Payment;
user?: User;
}
export interface TicketValidationResult {
valid: boolean;
status: 'valid' | 'already_checked_in' | 'pending_payment' | 'cancelled' | 'invalid' | 'wrong_event';
canCheckIn: boolean;
ticket?: {
id: string;
qrCode: string;
attendeeName: string;
attendeeEmail?: string;
attendeePhone?: string;
status: string;
checkinAt?: string;
checkedInBy?: string;
};
event?: {
id: string;
title: string;
startDatetime: string;
location: string;
};
error?: string;
}
export interface TicketSearchResult {
id: string;
qrCode: string;
attendeeName: string;
attendeeEmail?: string;
attendeePhone?: string;
status: string;
checkinAt?: string;
event?: {
id: string;
title: string;
startDatetime: string;
location: string;
} | null;
}
export interface LiveSearchResult {
ticket_id: string;
name: string;
email?: string;
status: string;
checked_in: boolean;
checkinAt?: string;
event_id: string;
qrCode: string;
event?: {
id: string;
title: string;
startDatetime: string;
location: string;
} | null;
}
export interface Payment {
id: string;
ticketId: string;
provider: 'bancard' | 'lightning' | 'cash' | 'bank_transfer' | 'tpago';
amount: number;
currency: string;
status: 'pending' | 'pending_approval' | 'paid' | 'refunded' | 'failed';
reference?: string;
userMarkedPaidAt?: string;
payerName?: string; // Name of payer if different from attendee
paidAt?: string;
paidByAdminId?: string;
adminNote?: string;
reminderSentAt?: string; // When payment reminder email was sent
createdAt: string;
updatedAt: string;
}
export interface PaymentWithDetails extends Payment {
ticket: {
id: string;
bookingId?: string;
attendeeFirstName: string;
attendeeLastName?: string;
attendeeEmail?: string;
attendeePhone?: string;
status: string;
} | null;
event: {
id: string;
title: string;
startDatetime: string;
} | null;
}
export interface PaymentOptionsConfig {
tpagoEnabled: boolean;
tpagoLink?: string | null;
tpagoInstructions?: string | null;
tpagoInstructionsEs?: string | null;
bankTransferEnabled: boolean;
bankName?: string | null;
bankAccountHolder?: string | null;
bankAccountNumber?: string | null;
bankAlias?: string | null;
bankPhone?: string | null;
bankNotes?: string | null;
bankNotesEs?: string | null;
lightningEnabled: boolean;
cashEnabled: boolean;
cashInstructions?: string | null;
cashInstructionsEs?: string | null;
// Booking settings
allowDuplicateBookings?: boolean;
}
export interface User {
id: string;
email: string;
name: string;
phone?: string;
role: 'admin' | 'organizer' | 'staff' | 'marketing' | 'user';
languagePreference?: string;
isClaimed?: boolean;
rucNumber?: string;
accountStatus?: string;
createdAt: string;
}
export interface Contact {
id: string;
name: string;
email: string;
message: string;
status: 'new' | 'read' | 'replied';
createdAt: string;
}
export interface AttendeeData {
firstName: string;
lastName?: string;
}
export interface BookingData {
eventId: string;
firstName: string;
lastName: string;
email: string;
phone: string;
preferredLanguage?: 'en' | 'es';
paymentMethod: 'bancard' | 'lightning' | 'cash' | 'bank_transfer' | 'tpago';
ruc?: string;
// For multi-ticket bookings
attendees?: AttendeeData[];
}
export interface DashboardData {
stats: {
totalUsers: number;
totalEvents: number;
totalTickets: number;
confirmedTickets: number;
pendingPayments: number;
totalRevenue: number;
newContacts: number;
totalSubscribers: number;
};
upcomingEvents: Event[];
recentTickets: Ticket[];
}
export interface AnalyticsData {
events: {
id: string;
title: string;
date: string;
capacity: number;
totalBookings: number;
confirmedBookings: number;
checkedIn: number;
revenue: number;
}[];
}
export interface Media {
id: string;
fileUrl: string;
type: 'image' | 'video' | 'document';
relatedId?: string;
relatedType?: string;
createdAt: string;
}
export interface ExportedTicket {
ticketId: string;
ticketStatus: string;
qrCode: string;
checkinAt?: string;
userName: string;
userEmail: string;
userPhone?: string;
eventTitle: string;
eventDate: string;
paymentStatus: string;
paymentAmount: number;
createdAt: string;
}
export interface EmailTemplate {
id: string;
name: string;
slug: string;
subject: string;
subjectEs?: string;
bodyHtml: string;
bodyHtmlEs?: string;
bodyText?: string;
bodyTextEs?: string;
description?: string;
variables: EmailVariable[];
isSystem: boolean;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface EmailVariable {
name: string;
description: string;
example: string;
}
export interface EmailLog {
id: string;
templateId?: string;
eventId?: string;
recipientEmail: string;
recipientName?: string;
subject: string;
bodyHtml?: string;
status: 'pending' | 'sent' | 'failed' | 'bounced';
errorMessage?: string;
sentAt?: string;
sentBy?: string;
createdAt: string;
}
export interface EmailStats {
total: number;
sent: number;
failed: number;
pending: number;
}
export interface Pagination {
total: number;
limit: number;
offset: number;
hasMore: boolean;
}
export interface ExportedPayment {
paymentId: string;
amount: number;
currency: string;
provider: string;
status: string;
reference?: string;
paidAt?: string;
createdAt: string;
ticketId: string;
attendeeFirstName: string;
attendeeLastName?: string;
attendeeEmail?: string;
eventId: string;
eventTitle: string;
eventDate: string;
}
export interface FinancialSummary {
totalPayments: number;
totalPaid: number;
totalPending: number;
totalRefunded: number;
byProvider: {
bancard: number;
lightning: number;
cash: number;
bank_transfer: number;
tpago: number;
};
paidCount: number;
pendingCount: number;
pendingApprovalCount: number;
refundedCount: number;
failedCount: number;
}
// ==================== User Dashboard Types ====================
export interface UserProfile {
id: string;
email: string;
name: string;
phone?: string;
languagePreference?: string;
rucNumber?: string;
isClaimed: boolean;
accountStatus: string;
hasPassword: boolean;
hasGoogleLinked: boolean;
memberSince: string;
membershipDays: number;
createdAt: string;
}
export interface UserTicket extends Ticket {
invoice?: {
id: string;
invoiceNumber: string;
pdfUrl?: string;
createdAt: string;
} | null;
}
export interface UserPayment extends Payment {
ticket: {
id: string;
attendeeFirstName: string;
attendeeLastName?: string;
status: string;
} | null;
event: {
id: string;
title: string;
titleEs?: string;
startDatetime: string;
} | null;
invoice?: {
id: string;
invoiceNumber: string;
pdfUrl?: string;
} | null;
}
export interface UserInvoice {
id: string;
paymentId: string;
invoiceNumber: string;
rucNumber?: string;
legalName?: string;
amount: number;
currency: string;
pdfUrl?: string;
status: string;
createdAt: string;
event?: {
id: string;
title: string;
titleEs?: string;
startDatetime: string;
} | null;
}
export interface UserSession {
id: string;
userAgent?: string;
ipAddress?: string;
lastActiveAt: string;
createdAt: string;
}
export interface DashboardSummary {
user: {
name: string;
email: string;
accountStatus: string;
memberSince: string;
membershipDays: number;
};
stats: {
totalTickets: number;
confirmedTickets: number;
upcomingEvents: number;
pendingPayments: number;
};
}
export interface NextEventInfo {
event: Event;
ticket: Ticket;
payment: Payment | null;
}
// ==================== Auth API (new methods) ====================
export const authApi = {
// Magic link
requestMagicLink: (email: string) =>
fetchApi<{ message: string }>('/api/auth/magic-link/request', {
method: 'POST',
body: JSON.stringify({ email }),
}),
verifyMagicLink: (token: string) =>
fetchApi<{ user: User; token: string; refreshToken: string }>('/api/auth/magic-link/verify', {
method: 'POST',
body: JSON.stringify({ token }),
}),
// Password reset
requestPasswordReset: (email: string) =>
fetchApi<{ message: string }>('/api/auth/password-reset/request', {
method: 'POST',
body: JSON.stringify({ email }),
}),
confirmPasswordReset: (token: string, password: string) =>
fetchApi<{ message: string }>('/api/auth/password-reset/confirm', {
method: 'POST',
body: JSON.stringify({ token, password }),
}),
// Account claiming
requestClaimAccount: (email: string) =>
fetchApi<{ message: string }>('/api/auth/claim-account/request', {
method: 'POST',
body: JSON.stringify({ email }),
}),
confirmClaimAccount: (token: string, data: { password?: string; googleId?: string }) =>
fetchApi<{ user: User; token: string; refreshToken: string; message: string }>(
'/api/auth/claim-account/confirm',
{
method: 'POST',
body: JSON.stringify({ token, ...data }),
}
),
// Google OAuth
googleAuth: (credential: string) =>
fetchApi<{ user: User; token: string; refreshToken: string }>('/api/auth/google', {
method: 'POST',
body: JSON.stringify({ credential }),
}),
// Change password
changePassword: (currentPassword: string, newPassword: string) =>
fetchApi<{ message: string }>('/api/auth/change-password', {
method: 'POST',
body: JSON.stringify({ currentPassword, newPassword }),
}),
// Get current user
me: () => fetchApi<{ user: User }>('/api/auth/me'),
};
// ==================== User Dashboard API ====================
export const dashboardApi = {
// Summary
getSummary: () =>
fetchApi<{ summary: DashboardSummary }>('/api/dashboard/summary'),
// Profile
getProfile: () =>
fetchApi<{ profile: UserProfile }>('/api/dashboard/profile'),
updateProfile: (data: { name?: string; phone?: string; languagePreference?: string; rucNumber?: string }) =>
fetchApi<{ profile: UserProfile; message: string }>('/api/dashboard/profile', {
method: 'PUT',
body: JSON.stringify(data),
}),
// Tickets
getTickets: () =>
fetchApi<{ tickets: UserTicket[] }>('/api/dashboard/tickets'),
getTicket: (id: string) =>
fetchApi<{ ticket: UserTicket }>(`/api/dashboard/tickets/${id}`),
// Next event
getNextEvent: () =>
fetchApi<{ nextEvent: NextEventInfo | null }>('/api/dashboard/next-event'),
// Payments
getPayments: () =>
fetchApi<{ payments: UserPayment[] }>('/api/dashboard/payments'),
// Invoices
getInvoices: () =>
fetchApi<{ invoices: UserInvoice[] }>('/api/dashboard/invoices'),
// Sessions
getSessions: () =>
fetchApi<{ sessions: UserSession[] }>('/api/dashboard/sessions'),
revokeSession: (id: string) =>
fetchApi<{ message: string }>(`/api/dashboard/sessions/${id}`, { method: 'DELETE' }),
revokeAllSessions: () =>
fetchApi<{ message: string }>('/api/dashboard/sessions/revoke-all', { method: 'POST' }),
// Security
setPassword: (password: string) =>
fetchApi<{ message: string }>('/api/dashboard/set-password', {
method: 'POST',
body: JSON.stringify({ password }),
}),
unlinkGoogle: () =>
fetchApi<{ message: string }>('/api/dashboard/unlink-google', { method: 'POST' }),
};
// ==================== Site Settings API ====================
export interface SiteSettings {
id?: string;
timezone: string;
siteName: string;
siteDescription?: string | null;
siteDescriptionEs?: string | null;
contactEmail?: string | null;
contactPhone?: string | null;
facebookUrl?: string | null;
instagramUrl?: string | null;
twitterUrl?: string | null;
linkedinUrl?: string | null;
featuredEventId?: string | null;
maintenanceMode: boolean;
maintenanceMessage?: string | null;
maintenanceMessageEs?: string | null;
updatedAt?: string;
updatedBy?: string;
}
export interface TimezoneOption {
value: string;
label: string;
}
export const siteSettingsApi = {
get: () => fetchApi<{ settings: SiteSettings }>('/api/site-settings'),
update: (data: Partial<SiteSettings>) =>
fetchApi<{ settings: SiteSettings; message: string }>('/api/site-settings', {
method: 'PUT',
body: JSON.stringify(data),
}),
getTimezones: () =>
fetchApi<{ timezones: TimezoneOption[] }>('/api/site-settings/timezones'),
setFeaturedEvent: (eventId: string | null) =>
fetchApi<{ featuredEventId: string | null; message: string }>('/api/site-settings/featured-event', {
method: 'PUT',
body: JSON.stringify({ eventId }),
}),
};
// ==================== Legal Settings API ====================
export interface LegalSettingsData {
id?: string;
companyName?: string | null;
legalEntityName?: string | null;
rucNumber?: string | null;
companyAddress?: string | null;
companyCity?: string | null;
companyCountry?: string | null;
supportEmail?: string | null;
legalEmail?: string | null;
governingLaw?: string | null;
jurisdictionCity?: string | null;
updatedAt?: string;
updatedBy?: string;
}
export const legalSettingsApi = {
get: () => fetchApi<{ settings: LegalSettingsData }>('/api/legal-settings'),
update: (data: Partial<LegalSettingsData>) =>
fetchApi<{ settings: LegalSettingsData; message: string }>('/api/legal-settings', {
method: 'PUT',
body: JSON.stringify(data),
}),
};
// ==================== Legal Pages Types ====================
export interface LegalPage {
id: string;
slug: string;
title: string;
titleEs?: string | null;
contentText: string;
contentTextEs?: string | null;
contentMarkdown: string;
contentMarkdownEs?: string | null;
updatedAt: string;
updatedBy?: string | null;
createdAt: string;
source?: 'database' | 'filesystem';
hasEnglish?: boolean;
hasSpanish?: boolean;
}
export interface LegalPagePublic {
id?: string;
slug: string;
title: string;
contentMarkdown: string;
updatedAt?: string;
source?: 'database' | 'filesystem';
}
export interface LegalPageListItem {
id: string;
slug: string;
title: string;
updatedAt: string;
hasEnglish?: boolean;
hasSpanish?: boolean;
}
// ==================== Legal Pages API ====================
export const legalPagesApi = {
// Public endpoints
getAll: (locale?: string) =>
fetchApi<{ pages: LegalPageListItem[] }>(`/api/legal-pages${locale ? `?locale=${locale}` : ''}`),
getBySlug: (slug: string, locale?: string) =>
fetchApi<{ page: LegalPagePublic }>(`/api/legal-pages/${slug}${locale ? `?locale=${locale}` : ''}`),
// Admin endpoints
getAdminList: () =>
fetchApi<{ pages: LegalPage[] }>('/api/legal-pages/admin/list'),
getAdminPage: (slug: string) =>
fetchApi<{ page: LegalPage }>(`/api/legal-pages/admin/${slug}`),
update: (slug: string, data: {
contentMarkdown?: string;
contentMarkdownEs?: string;
title?: string;
titleEs?: string;
}) =>
fetchApi<{ page: LegalPage; message: string }>(`/api/legal-pages/admin/${slug}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
seed: () =>
fetchApi<{ message: string; seeded: number; pages?: string[] }>('/api/legal-pages/admin/seed', {
method: 'POST',
}),
};
// ==================== FAQ Types ====================
export interface FaqItem {
id: string;
question: string;
questionEs?: string | null;
answer: string;
answerEs?: string | null;
rank?: number;
}
export interface FaqItemAdmin extends FaqItem {
enabled: boolean;
showOnHomepage: boolean;
createdAt: string;
updatedAt: string;
}
// ==================== FAQ API ====================
export const faqApi = {
// Public
getList: (homepageOnly?: boolean) =>
fetchApi<{ faqs: FaqItem[] }>(`/api/faq${homepageOnly ? '?homepage=true' : ''}`),
// Admin
getAdminList: () =>
fetchApi<{ faqs: FaqItemAdmin[] }>('/api/faq/admin/list'),
getById: (id: string) =>
fetchApi<{ faq: FaqItemAdmin }>(`/api/faq/admin/${id}`),
create: (data: {
question: string;
questionEs?: string;
answer: string;
answerEs?: string;
enabled?: boolean;
showOnHomepage?: boolean;
}) =>
fetchApi<{ faq: FaqItemAdmin }>('/api/faq/admin', {
method: 'POST',
body: JSON.stringify(data),
}),
update: (id: string, data: {
question?: string;
questionEs?: string | null;
answer?: string;
answerEs?: string | null;
enabled?: boolean;
showOnHomepage?: boolean;
}) =>
fetchApi<{ faq: FaqItemAdmin }>(`/api/faq/admin/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: string) =>
fetchApi<{ message: string }>(`/api/faq/admin/${id}`, { method: 'DELETE' }),
reorder: (ids: string[]) =>
fetchApi<{ faqs: FaqItemAdmin[] }>('/api/faq/admin/reorder', {
method: 'POST',
body: JSON.stringify({ ids }),
}),
};