Major additions: - Territory manager system with application workflow - Custom pricing and page builder with Craft.js - Enhanced Stripe Connect onboarding - CodeReadr QR scanning integration - Kiosk mode for venue sales - Super admin dashboard and analytics - MCP integration for AI-powered operations Infrastructure improvements: - Centralized API client and routing system - Enhanced authentication with organization context - Comprehensive theme management system - Advanced event management with custom tabs - Performance monitoring and accessibility features Database schema updates: - Territory management tables - Custom pages and pricing structures - Kiosk PIN system - Enhanced organization profiles - CodeReadr integration tables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
389 lines
9.9 KiB
TypeScript
389 lines
9.9 KiB
TypeScript
import { supabase } from './supabase';
|
|
import type { Database } from './database.types';
|
|
|
|
export interface ApiResponse<T> {
|
|
data: T | null;
|
|
error: string | null;
|
|
success: boolean;
|
|
}
|
|
|
|
export interface EventStats {
|
|
ticketsSold: number;
|
|
ticketsAvailable: number;
|
|
checkedIn: number;
|
|
totalRevenue: number;
|
|
netRevenue: number;
|
|
}
|
|
|
|
export interface EventDetails {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
venue: string;
|
|
start_time: string;
|
|
slug: string;
|
|
organization_id: string;
|
|
}
|
|
|
|
class ApiClient {
|
|
private authenticated = false;
|
|
private session: any = null;
|
|
private userOrganizationId: string | null = null;
|
|
|
|
async initialize(): Promise<boolean> {
|
|
try {
|
|
|
|
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
|
|
|
|
if (sessionError) {
|
|
|
|
this.authenticated = false;
|
|
return false;
|
|
}
|
|
|
|
if (!session) {
|
|
|
|
this.authenticated = false;
|
|
return false;
|
|
}
|
|
|
|
this.session = session;
|
|
this.authenticated = true;
|
|
|
|
// Get user's organization
|
|
const { data: userRecord, error: userError } = await supabase
|
|
.from('users')
|
|
.select('organization_id')
|
|
.eq('id', session.user.id)
|
|
.single();
|
|
|
|
if (userError) {
|
|
|
|
}
|
|
|
|
this.userOrganizationId = userRecord?.organization_id || null;
|
|
|
|
return true;
|
|
} catch (error) {
|
|
|
|
this.authenticated = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async ensureAuthenticated(): Promise<boolean> {
|
|
if (!this.authenticated) {
|
|
const initialized = await this.initialize();
|
|
if (!initialized) {
|
|
|
|
// Don't automatically redirect here - let the component handle it
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async getEventDetails(eventId: string): Promise<ApiResponse<EventDetails>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const { data: event, error } = await supabase
|
|
.from('events')
|
|
.select('*')
|
|
.eq('id', eventId)
|
|
.single();
|
|
|
|
if (error) {
|
|
|
|
return { data: null, error: error.message, success: false };
|
|
}
|
|
|
|
return { data: event, error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load event details', success: false };
|
|
}
|
|
}
|
|
|
|
async getEventStats(eventId: string): Promise<ApiResponse<EventStats>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
// Load ticket sales data
|
|
const { data: tickets, error: ticketsError } = await supabase
|
|
.from('tickets')
|
|
.select(`
|
|
id,
|
|
price,
|
|
checked_in,
|
|
refund_status,
|
|
ticket_types (
|
|
id,
|
|
quantity_available
|
|
)
|
|
`)
|
|
.eq('event_id', eventId);
|
|
|
|
if (ticketsError) {
|
|
|
|
return { data: null, error: ticketsError.message, success: false };
|
|
}
|
|
|
|
// Load ticket types for capacity calculation
|
|
const { data: ticketTypes, error: ticketTypesError } = await supabase
|
|
.from('ticket_types')
|
|
.select('id, quantity_available')
|
|
.eq('event_id', eventId)
|
|
.eq('is_active', true);
|
|
|
|
if (ticketTypesError) {
|
|
|
|
return { data: null, error: ticketTypesError.message, success: false };
|
|
}
|
|
|
|
// Calculate stats
|
|
const ticketsSold = tickets?.length || 0;
|
|
// Only count non-refunded tickets for revenue
|
|
const activeTickets = tickets?.filter(ticket =>
|
|
!ticket.refund_status || ticket.refund_status === null
|
|
) || [];
|
|
const totalRevenue = activeTickets.reduce((sum, ticket) => sum + ticket.price, 0) || 0;
|
|
const netRevenue = totalRevenue * 0.97; // Assuming 3% platform fee
|
|
const checkedIn = tickets?.filter(ticket => ticket.checked_in).length || 0;
|
|
const totalCapacity = ticketTypes?.reduce((sum, type) => sum + type.quantity_available, 0) || 0;
|
|
const ticketsAvailable = totalCapacity - ticketsSold;
|
|
|
|
const stats: EventStats = {
|
|
ticketsSold,
|
|
ticketsAvailable,
|
|
checkedIn,
|
|
totalRevenue,
|
|
netRevenue
|
|
};
|
|
|
|
return { data: stats, error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load event statistics', success: false };
|
|
}
|
|
}
|
|
|
|
async getTicketTypes(eventId: string): Promise<ApiResponse<any[]>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from('ticket_types')
|
|
.select('*')
|
|
.eq('event_id', eventId)
|
|
.eq('is_active', true)
|
|
.order('display_order', { ascending: true });
|
|
|
|
if (error) {
|
|
|
|
return { data: null, error: error.message, success: false };
|
|
}
|
|
|
|
return { data: data || [], error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load ticket types', success: false };
|
|
}
|
|
}
|
|
|
|
async getOrders(eventId: string): Promise<ApiResponse<any[]>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from('tickets')
|
|
.select(`
|
|
*,
|
|
ticket_types (
|
|
name,
|
|
price
|
|
)
|
|
`)
|
|
.eq('event_id', eventId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
|
|
return { data: null, error: error.message, success: false };
|
|
}
|
|
|
|
return { data: data || [], error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load orders', success: false };
|
|
}
|
|
}
|
|
|
|
async getPresaleCodes(eventId: string): Promise<ApiResponse<any[]>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from('presale_codes')
|
|
.select('*')
|
|
.eq('event_id', eventId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
|
|
return { data: null, error: error.message, success: false };
|
|
}
|
|
|
|
return { data: data || [], error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load presale codes', success: false };
|
|
}
|
|
}
|
|
|
|
async getDiscountCodes(eventId: string): Promise<ApiResponse<any[]>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from('discount_codes')
|
|
.select('*')
|
|
.eq('event_id', eventId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
|
|
return { data: null, error: error.message, success: false };
|
|
}
|
|
|
|
return { data: data || [], error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load discount codes', success: false };
|
|
}
|
|
}
|
|
|
|
async getAttendees(eventId: string): Promise<ApiResponse<any[]>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from('tickets')
|
|
.select(`
|
|
*,
|
|
ticket_types (
|
|
name
|
|
)
|
|
`)
|
|
.eq('event_id', eventId)
|
|
.is('refund_status', null)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
|
|
return { data: null, error: error.message, success: false };
|
|
}
|
|
|
|
return { data: data || [], error: null, success: true };
|
|
} catch (error) {
|
|
|
|
return { data: null, error: 'Failed to load attendees', success: false };
|
|
}
|
|
}
|
|
|
|
// Utility methods
|
|
getUserOrganizationId(): string | null {
|
|
return this.userOrganizationId;
|
|
}
|
|
|
|
getUser(): any {
|
|
return this.session?.user || null;
|
|
}
|
|
|
|
getSession(): any {
|
|
return this.session;
|
|
}
|
|
|
|
isAuthenticated(): boolean {
|
|
return this.authenticated;
|
|
}
|
|
|
|
async logout(): Promise<void> {
|
|
await supabase.auth.signOut();
|
|
this.authenticated = false;
|
|
this.session = null;
|
|
this.userOrganizationId = null;
|
|
}
|
|
|
|
// Generic authenticated request helper
|
|
async makeAuthenticatedRequest<T>(
|
|
url: string,
|
|
options: RequestInit = {}
|
|
): Promise<ApiResponse<T>> {
|
|
try {
|
|
if (!(await this.ensureAuthenticated())) {
|
|
return { data: null, error: 'Authentication required', success: false };
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.session?.access_token}`,
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
return {
|
|
data: null,
|
|
error: errorText || `HTTP ${response.status}`,
|
|
success: false
|
|
};
|
|
}
|
|
|
|
const data = await response.json();
|
|
return { data, error: null, success: true };
|
|
} catch (error) {
|
|
return {
|
|
data: null,
|
|
error: error instanceof Error ? error.message : 'Request failed',
|
|
success: false
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export a singleton instance
|
|
export const apiClient = new ApiClient();
|
|
|
|
// Export a hook-like function for easier use in components
|
|
export async function useApi() {
|
|
if (!apiClient.isAuthenticated()) {
|
|
await apiClient.initialize();
|
|
}
|
|
return apiClient;
|
|
}
|
|
|
|
// Export the makeAuthenticatedRequest function for direct use
|
|
export async function makeAuthenticatedRequest<T>(
|
|
url: string,
|
|
options: RequestInit = {}
|
|
): Promise<ApiResponse<T>> {
|
|
return apiClient.makeAuthenticatedRequest<T>(url, options);
|
|
} |