feat: Modularize event management system - 98.7% reduction in main file size
BREAKING CHANGES: - Refactored monolithic manage.astro (7,623 lines) into modular architecture - Original file backed up as manage-old.astro NEW ARCHITECTURE: ✅ 5 Utility Libraries: - event-management.ts: Event data operations & formatting - ticket-management.ts: Ticket CRUD operations & sales data - seating-management.ts: Seating map management & layout generation - sales-analytics.ts: Sales metrics, reporting & data export - marketing-kit.ts: Marketing asset generation & social media ✅ 5 Shared Components: - TicketTypeModal.tsx: Reusable ticket type creation/editing - SeatingMapModal.tsx: Advanced seating map editor with drag-and-drop - EmbedCodeModal.tsx: Widget embedding with customization - OrdersTable.tsx: Comprehensive orders table with sorting/pagination - AttendeesTable.tsx: Attendee management with export capabilities ✅ 11 Tab Components: - TicketsTab.tsx: Ticket management with card/list views - VenueTab.tsx: Seating map management & venue configuration - OrdersTab.tsx: Sales data & order management - AttendeesTab.tsx: Attendee check-in & management - PresaleTab.tsx: Presale code generation & tracking - DiscountTab.tsx: Discount code management - AddonsTab.tsx: Add-on product management - PrintedTab.tsx: Printed ticket barcode management - SettingsTab.tsx: Event configuration & custom fields - MarketingTab.tsx: Marketing kit with social media templates - PromotionsTab.tsx: Campaign & promotion management ✅ 4 Infrastructure Components: - TabNavigation.tsx: Responsive tab navigation system - EventManagement.tsx: Main orchestration component - EventHeader.astro: Event information header - QuickStats.astro: Statistics dashboard BENEFITS: - 98.7% reduction in main file size (7,623 → ~100 lines) - Dramatic improvement in maintainability and team collaboration - Component-level testing now possible - Reusable components across multiple features - Lazy loading support for better performance - Full TypeScript support with proper interfaces - Separation of concerns: business logic separated from UI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
264
src/lib/ticket-management.ts
Normal file
264
src/lib/ticket-management.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { Database } from './database.types';
|
||||
|
||||
const supabase = createClient<Database>(
|
||||
import.meta.env.PUBLIC_SUPABASE_URL,
|
||||
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
|
||||
);
|
||||
|
||||
export interface TicketType {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
price_cents: number;
|
||||
quantity: number;
|
||||
is_active: boolean;
|
||||
event_id: string;
|
||||
sort_order: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface TicketTypeFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
price_cents: number;
|
||||
quantity: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface TicketSale {
|
||||
id: string;
|
||||
event_id: string;
|
||||
ticket_type_id: string;
|
||||
price_paid: number;
|
||||
status: string;
|
||||
checked_in: boolean;
|
||||
customer_email: string;
|
||||
customer_name: string;
|
||||
created_at: string;
|
||||
ticket_uuid: string;
|
||||
ticket_types: {
|
||||
name: string;
|
||||
price_cents: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadTicketTypes(eventId: string): Promise<TicketType[]> {
|
||||
try {
|
||||
const { data: ticketTypes, error } = await supabase
|
||||
.from('ticket_types')
|
||||
.select('*')
|
||||
.eq('event_id', eventId)
|
||||
.order('sort_order', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading ticket types:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return ticketTypes || [];
|
||||
} catch (error) {
|
||||
console.error('Error loading ticket types:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTicketType(eventId: string, ticketTypeData: TicketTypeFormData): Promise<TicketType | null> {
|
||||
try {
|
||||
// Get the next sort order
|
||||
const { data: existingTypes } = await supabase
|
||||
.from('ticket_types')
|
||||
.select('sort_order')
|
||||
.eq('event_id', eventId)
|
||||
.order('sort_order', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
const nextSortOrder = existingTypes?.[0]?.sort_order ? existingTypes[0].sort_order + 1 : 1;
|
||||
|
||||
const { data: ticketType, error } = await supabase
|
||||
.from('ticket_types')
|
||||
.insert({
|
||||
...ticketTypeData,
|
||||
event_id: eventId,
|
||||
sort_order: nextSortOrder
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating ticket type:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ticketType;
|
||||
} catch (error) {
|
||||
console.error('Error creating ticket type:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTicketType(ticketTypeId: string, updates: Partial<TicketTypeFormData>): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('ticket_types')
|
||||
.update(updates)
|
||||
.eq('id', ticketTypeId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating ticket type:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating ticket type:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTicketType(ticketTypeId: string): Promise<boolean> {
|
||||
try {
|
||||
// Check if there are any tickets sold for this type
|
||||
const { data: tickets } = await supabase
|
||||
.from('tickets')
|
||||
.select('id')
|
||||
.eq('ticket_type_id', ticketTypeId)
|
||||
.limit(1);
|
||||
|
||||
if (tickets && tickets.length > 0) {
|
||||
throw new Error('Cannot delete ticket type with existing sales');
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('ticket_types')
|
||||
.delete()
|
||||
.eq('id', ticketTypeId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting ticket type:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deleting ticket type:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleTicketTypeStatus(ticketTypeId: string, isActive: boolean): Promise<boolean> {
|
||||
return updateTicketType(ticketTypeId, { is_active: isActive });
|
||||
}
|
||||
|
||||
export async function loadTicketSales(eventId: string, filters?: {
|
||||
ticketTypeId?: string;
|
||||
searchTerm?: string;
|
||||
status?: string;
|
||||
}): Promise<TicketSale[]> {
|
||||
try {
|
||||
let query = supabase
|
||||
.from('tickets')
|
||||
.select(`
|
||||
id,
|
||||
event_id,
|
||||
ticket_type_id,
|
||||
price_paid,
|
||||
status,
|
||||
checked_in,
|
||||
customer_email,
|
||||
customer_name,
|
||||
created_at,
|
||||
ticket_uuid,
|
||||
ticket_types (
|
||||
name,
|
||||
price_cents
|
||||
)
|
||||
`)
|
||||
.eq('event_id', eventId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
// Apply filters
|
||||
if (filters?.ticketTypeId) {
|
||||
query = query.eq('ticket_type_id', filters.ticketTypeId);
|
||||
}
|
||||
|
||||
if (filters?.status) {
|
||||
query = query.eq('status', filters.status);
|
||||
}
|
||||
|
||||
if (filters?.searchTerm) {
|
||||
query = query.or(`customer_email.ilike.%${filters.searchTerm}%,customer_name.ilike.%${filters.searchTerm}%`);
|
||||
}
|
||||
|
||||
const { data: tickets, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading ticket sales:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return tickets || [];
|
||||
} catch (error) {
|
||||
console.error('Error loading ticket sales:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkInTicket(ticketId: string): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('tickets')
|
||||
.update({ checked_in: true })
|
||||
.eq('id', ticketId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error checking in ticket:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking in ticket:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function refundTicket(ticketId: string): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('tickets')
|
||||
.update({ status: 'refunded' })
|
||||
.eq('id', ticketId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error refunding ticket:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error refunding ticket:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTicketPrice(cents: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(cents / 100);
|
||||
}
|
||||
|
||||
export function calculateTicketTypeStats(ticketType: TicketType, sales: TicketSale[]): {
|
||||
sold: number;
|
||||
available: number;
|
||||
revenue: number;
|
||||
} {
|
||||
const typeSales = sales.filter(sale => sale.ticket_type_id === ticketType.id && sale.status === 'confirmed');
|
||||
const sold = typeSales.length;
|
||||
const available = ticketType.quantity - sold;
|
||||
const revenue = typeSales.reduce((sum, sale) => sum + sale.price_paid, 0);
|
||||
|
||||
return { sold, available, revenue };
|
||||
}
|
||||
Reference in New Issue
Block a user