Files
blackcanyontickets/src/lib/api-client.ts
dzinesco 26a87d0d00 feat: Complete platform enhancement with multi-tenant architecture
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>
2025-07-12 18:21:40 -06:00

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);
}