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>
This commit is contained in:
389
src/lib/api-client.ts
Normal file
389
src/lib/api-client.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user