Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { fetchApi, fetchBlob } from './client';
|
||||
import type {
|
||||
DashboardData,
|
||||
AnalyticsData,
|
||||
ExportedTicket,
|
||||
ExportedPayment,
|
||||
FinancialSummary,
|
||||
} from './types';
|
||||
|
||||
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: (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);
|
||||
return fetchBlob(
|
||||
`/api/admin/events/${eventId}/attendees/export?${query}`,
|
||||
`attendees-${new Date().toISOString().split('T')[0]}.csv`
|
||||
);
|
||||
},
|
||||
/** Download tickets export as CSV. Returns a Blob. */
|
||||
exportTicketsCSV: (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);
|
||||
return fetchBlob(
|
||||
`/api/admin/events/${eventId}/tickets/export?${query}`,
|
||||
`tickets-${new Date().toISOString().split('T')[0]}.csv`
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { User } from './types';
|
||||
|
||||
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'),
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** Read the stored auth token (browser only). */
|
||||
export function getToken(): string | null {
|
||||
return typeof window !== 'undefined' ? localStorage.getItem('spanglish-token') : null;
|
||||
}
|
||||
|
||||
export async function fetchApi<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a file download (CSV/blob) with auth, returning the blob and the
|
||||
* filename parsed from the Content-Disposition header.
|
||||
*/
|
||||
export async function fetchBlob(
|
||||
endpoint: string,
|
||||
fallbackFilename: string
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${API_BASE}${endpoint}`, { 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] : fallbackFilename;
|
||||
const blob = await res.blob();
|
||||
return { blob, filename };
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { Contact } from './types';
|
||||
|
||||
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 }),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { fetchApi } from './client';
|
||||
import type {
|
||||
DashboardSummary,
|
||||
UserProfile,
|
||||
UserTicket,
|
||||
UserPayment,
|
||||
UserInvoice,
|
||||
UserSession,
|
||||
NextEventInfo,
|
||||
} from './types';
|
||||
|
||||
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' }),
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { EmailTemplate, EmailVariable, EmailLog, EmailStats, Pagination } from './types';
|
||||
|
||||
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; 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' }),
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { Event } from './types';
|
||||
|
||||
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' }),
|
||||
|
||||
getSlugAliases: (id: string) =>
|
||||
fetchApi<{ aliases: { slug: string; createdAt: string }[] }>(`/api/events/${id}/slug-aliases`),
|
||||
|
||||
deleteSlugAlias: (id: string, slug: string) =>
|
||||
fetchApi<{ message: string }>(`/api/events/${id}/slug-aliases/${encodeURIComponent(slug)}`, { method: 'DELETE' }),
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { FaqItem, FaqItemAdmin } from './types';
|
||||
|
||||
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 }),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// Barrel for the frontend API client. Consumers import from `@/lib/api`.
|
||||
export type { ApiError } from './client';
|
||||
export * from './types';
|
||||
|
||||
export { eventsApi } from './events';
|
||||
export { ticketsApi } from './tickets';
|
||||
export { contactsApi } from './contacts';
|
||||
export { usersApi } from './users';
|
||||
export { paymentsApi } from './payments';
|
||||
export { paymentOptionsApi } from './paymentOptions';
|
||||
export { mediaApi } from './media';
|
||||
export { adminApi } from './admin';
|
||||
export { emailsApi } from './emails';
|
||||
export { authApi } from './auth';
|
||||
export { dashboardApi } from './dashboard';
|
||||
export { siteSettingsApi } from './siteSettings';
|
||||
export { legalSettingsApi } from './legalSettings';
|
||||
export { legalPagesApi } from './legalPages';
|
||||
export { faqApi } from './faq';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { LegalPage, LegalPagePublic, LegalPageListItem } from './types';
|
||||
|
||||
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',
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { LegalSettingsData } from './types';
|
||||
|
||||
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),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { fetchApi, API_BASE, getToken } from './client';
|
||||
import type { Media } from './types';
|
||||
|
||||
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 = getToken();
|
||||
|
||||
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' }),
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { PaymentOptionsConfig } from './types';
|
||||
|
||||
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). Pass ticketId after booking to
|
||||
// retrieve bank/TPago credentials gated behind the booking capability token.
|
||||
getForEvent: (eventId: string, ticketId?: string) =>
|
||||
fetchApi<{ paymentOptions: PaymentOptionsConfig; hasOverrides: boolean }>(
|
||||
`/api/payment-options/event/${eventId}${ticketId ? `?ticketId=${encodeURIComponent(ticketId)}` : ''}`
|
||||
),
|
||||
|
||||
// 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',
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { Payment, PaymentWithDetails } from './types';
|
||||
|
||||
export const paymentsApi = {
|
||||
getAll: (params?: { status?: string; provider?: string; pendingApproval?: boolean; eventId?: string; eventIds?: string[] }) => {
|
||||
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');
|
||||
if (params?.eventId) query.set('eventId', params.eventId);
|
||||
if (params?.eventIds && params.eventIds.length > 0) query.set('eventIds', params.eventIds.join(','));
|
||||
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' }),
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { SiteSettings, TimezoneOption } from './types';
|
||||
|
||||
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 }),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { fetchApi, API_BASE } from './client';
|
||||
import type {
|
||||
Ticket,
|
||||
Payment,
|
||||
BookingData,
|
||||
TicketValidationResult,
|
||||
TicketSearchResult,
|
||||
LiveSearchResult,
|
||||
} 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),
|
||||
}),
|
||||
|
||||
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),
|
||||
}),
|
||||
|
||||
guestCreate: (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/guest', {
|
||||
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`,
|
||||
};
|
||||
@@ -0,0 +1,546 @@
|
||||
export interface Event {
|
||||
id: string;
|
||||
slug: 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
|
||||
bookingTicketCount?: number; // Total tickets in the booking (for per-quantity payment links)
|
||||
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;
|
||||
isGuest?: boolean;
|
||||
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;
|
||||
tpagoLink2?: string | null;
|
||||
tpagoLink3?: string | null;
|
||||
tpagoLink4?: string | null;
|
||||
tpagoLink5?: 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;
|
||||
resendAttempts?: number;
|
||||
lastResentAt?: 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;
|
||||
}
|
||||
|
||||
// ==================== Site Settings Types ====================
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ==================== Legal Settings Types ====================
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ==================== 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;
|
||||
}
|
||||
|
||||
// ==================== 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;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { fetchApi } from './client';
|
||||
import type { User } from './types';
|
||||
|
||||
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' }),
|
||||
};
|
||||
Reference in New Issue
Block a user