Files
blackcanyontickets/src/pages/admin/super-dashboard.astro
dzinesco e8b95231b7 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>
2025-07-08 18:30:26 -06:00

1761 lines
78 KiB
Plaintext

---
import Layout from '../../layouts/Layout.astro';
---
<Layout title="Super Admin Dashboard - Portal">
<div class="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-slate-900 relative overflow-hidden">
<!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-br from-blue-500/20 to-transparent rounded-full animate-pulse"></div>
<div class="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-to-tl from-purple-500/20 to-transparent rounded-full animate-pulse" style="animation-delay: 2s;"></div>
</div>
<!-- Sticky Navigation -->
<nav class="sticky top-0 z-50 bg-white/10 backdrop-blur-xl shadow-xl border-b border-white/20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-20">
<div class="flex items-center space-x-8">
<a href="/admin/super-dashboard" class="flex items-center">
<span class="text-xl font-light text-white">
<span class="font-bold">P</span>ortal
</span>
<span class="ml-2 px-2 py-1 bg-gradient-to-r from-red-500 to-orange-500 text-white rounded-md text-xs font-medium">Super Admin</span>
</a>
</div>
<div class="flex items-center space-x-4">
<a href="/admin/dashboard" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200">
Admin View
</a>
<a href="/dashboard" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200">
Organizer View
</a>
<span id="user-name" class="text-sm text-white font-medium"></span>
<button id="logout-btn" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200">
Sign Out
</button>
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8 relative z-10">
<div class="px-4 py-6 sm:px-0">
<!-- Page Header -->
<div class="mb-8">
<h2 class="text-4xl md:text-5xl font-light text-white tracking-wide">Business Intelligence</h2>
<p class="text-white/80 mt-2 text-lg font-light">Platform-wide analytics and performance insights</p>
</div>
<!-- Key Metrics Dashboard -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div id="revenue-card" class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl shadow-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-gradient-to-br from-green-500/20 to-emerald-500/20 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
</div>
<div class="text-right">
<p class="text-sm font-medium text-white/80">Platform Revenue</p>
<p id="total-revenue" class="text-2xl font-light text-white">$0.00</p>
<p id="revenue-change" class="text-sm text-green-400">+0% vs last month</p>
</div>
</div>
</div>
<div id="fees-card" class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl shadow-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
</div>
<div class="text-right">
<p class="text-sm font-medium text-white/80">Platform Fees</p>
<p id="total-fees" class="text-2xl font-light text-white">$0.00</p>
<p id="fees-change" class="text-sm text-purple-400">+0% vs last month</p>
</div>
</div>
</div>
<div id="organizers-card" class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl shadow-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
</div>
<div class="text-right">
<p class="text-sm font-medium text-white/80">Active Organizers</p>
<p id="active-organizers" class="text-2xl font-light text-white">0</p>
<p id="organizers-change" class="text-sm text-blue-400">+0 this month</p>
</div>
</div>
</div>
<div id="tickets-card" class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl shadow-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-gradient-to-br from-yellow-500/20 to-orange-500/20 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
</svg>
</div>
<div class="text-right">
<p class="text-sm font-medium text-white/80">Tickets Sold</p>
<p id="total-tickets" class="text-2xl font-light text-white">0</p>
<p id="tickets-change" class="text-sm text-yellow-400">+0 this month</p>
</div>
</div>
</div>
</div>
<!-- Navigation Tabs -->
<div class="mb-6">
<nav class="flex space-x-8 border-b border-white/20">
<button id="tab-overview" class="tab-button border-b-2 border-indigo-400 text-indigo-400 py-2 px-1 text-sm font-medium">
Overview
</button>
<button id="tab-revenue" class="tab-button border-b-2 border-transparent text-white/60 hover:text-white py-2 px-1 text-sm font-medium">
Revenue Analysis
</button>
<button id="tab-events" class="tab-button border-b-2 border-transparent text-white/60 hover:text-white py-2 px-1 text-sm font-medium">
Global Events
</button>
<button id="tab-performance" class="tab-button border-b-2 border-transparent text-white/60 hover:text-white py-2 px-1 text-sm font-medium">
Performance
</button>
<button id="tab-organizers" class="tab-button border-b-2 border-transparent text-white/60 hover:text-white py-2 px-1 text-sm font-medium">
Organizers
</button>
<button id="tab-tickets" class="tab-button border-b-2 border-transparent text-white/60 hover:text-white py-2 px-1 text-sm font-medium">
Tickets
</button>
<button id="tab-exports" class="tab-button border-b-2 border-transparent text-white/60 hover:text-white py-2 px-1 text-sm font-medium">
Data Export
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="bg-white/10 backdrop-blur-xl border border-white/20 shadow-lg rounded-2xl">
<!-- Overview Tab -->
<div id="content-overview" class="tab-content p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Revenue Trends Chart -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Revenue Trends (Last 6 Months)</h3>
<div id="revenue-chart" class="h-64">
<canvas id="revenueCanvas" width="400" height="200"></canvas>
</div>
</div>
<!-- Top Organizers -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Top Performing Organizers</h3>
<div id="top-organizers" class="space-y-3">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="mt-6 bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Recent Platform Activity</h3>
<div id="recent-activity" class="space-y-3">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Global Events Tab -->
<div id="content-events" class="tab-content p-6 hidden">
<div class="mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-white">Global Event Management</h3>
<button id="create-event-btn" class="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-4 py-2 rounded-lg font-medium transition-all">
Create Event
</button>
</div>
<!-- Event Filters -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-4 mb-6">
<h4 class="text-white font-medium mb-3">Filters</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-white mb-1">Date Range</label>
<div class="grid grid-cols-2 gap-1">
<input type="date" id="event-start-date" class="px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<input type="date" id="event-end-date" class="px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Category</label>
<select id="event-category-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Categories</option>
<option value="concert">Concert</option>
<option value="festival">Festival</option>
<option value="conference">Conference</option>
<option value="gala">Gala</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Organizer</label>
<select id="event-organizer-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Organizers</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Status</label>
<select id="event-status-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Events</option>
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="past">Past</option>
</select>
</div>
</div>
<div class="mt-4">
<button id="apply-event-filters" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all">
Apply Filters
</button>
<button id="reset-event-filters" class="ml-2 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all">
Reset
</button>
</div>
</div>
<!-- Events Table -->
<div id="global-events-table" class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl overflow-hidden">
<div class="p-4 text-center text-white/60">
<div class="animate-spin w-8 h-8 border-2 border-white/30 border-t-white rounded-full mx-auto mb-2"></div>
Loading events...
</div>
</div>
</div>
</div>
<!-- Revenue Analysis Tab -->
<div id="content-revenue" class="tab-content p-6 hidden">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<!-- Revenue Breakdown -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Revenue Breakdown</h3>
<div id="revenue-breakdown" class="space-y-4">
<!-- Will be populated by JavaScript -->
</div>
</div>
<!-- Monthly Comparison -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Monthly Comparison</h3>
<div id="monthly-comparison" class="h-64">
<canvas id="comparisonCanvas" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
<!-- Performance Tab -->
<div id="content-performance" class="tab-content p-6 hidden">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<!-- Event Performance -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Event Performance Metrics</h3>
<div id="event-performance" class="space-y-4">
<!-- Will be populated by JavaScript -->
</div>
</div>
<!-- Sales Velocity -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Sales Velocity</h3>
<div id="sales-velocity" class="h-64">
<canvas id="velocityCanvas" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
<!-- Organizers Tab -->
<div id="content-organizers" class="tab-content p-6 hidden">
<div class="mb-6">
<h3 class="text-lg font-medium text-white mb-4">Organizer Performance Dashboard</h3>
<div id="organizer-table" class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl overflow-hidden">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Tickets Tab -->
<div id="content-tickets" class="tab-content p-6 hidden">
<div class="mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-white">Global Ticket Management</h3>
<div class="flex space-x-2">
<button id="export-tickets-btn" class="bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white px-4 py-2 rounded-xl font-medium transition-all backdrop-blur-xl shadow-lg border border-white/20">
Export Tickets
</button>
<button id="refresh-tickets-btn" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl font-medium transition-all">
Refresh
</button>
</div>
</div>
<!-- Advanced Filters -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-4 mb-6">
<h4 class="text-white font-medium mb-3">Advanced Filters</h4>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-white mb-1">Date Range</label>
<div class="grid grid-cols-2 gap-1">
<input type="date" id="ticket-start-date" class="px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<input type="date" id="ticket-end-date" class="px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Event</label>
<select id="ticket-event-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Events</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Organizer</label>
<select id="ticket-organizer-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Organizers</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Price Range</label>
<div class="grid grid-cols-2 gap-1">
<input type="number" id="ticket-min-price" placeholder="Min" class="px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<input type="number" id="ticket-max-price" placeholder="Max" class="px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Ticket Type</label>
<select id="ticket-type-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Types</option>
<option value="general">General Admission</option>
<option value="vip">VIP</option>
<option value="reserved">Reserved Seating</option>
<option value="student">Student</option>
<option value="senior">Senior</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Status</label>
<select id="ticket-status-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="used">Used</option>
<option value="refunded">Refunded</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Purchase Method</label>
<select id="ticket-purchase-method-filter" class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
<option value="">All Methods</option>
<option value="online">Online</option>
<option value="door">At Door</option>
<option value="phone">Phone</option>
<option value="admin">Admin Created</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-1">Search</label>
<input type="text" id="ticket-search" placeholder="Ticket ID, email, name..." class="w-full px-2 py-1 bg-white/10 border border-white/20 rounded text-white text-sm">
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button id="apply-ticket-filters" class="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all backdrop-blur-xl shadow-lg border border-white/20">
Apply Filters
</button>
<button id="reset-ticket-filters" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all">
Reset
</button>
<button id="save-ticket-filter-preset" class="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all backdrop-blur-xl shadow-lg border border-white/20">
Save Preset
</button>
<select id="ticket-filter-presets" class="px-3 py-2 bg-white/10 backdrop-blur-xl border border-white/20 rounded-xl text-white text-sm">
<option value="">Load Preset...</option>
<option value="high-value">High Value Tickets ($100+)</option>
<option value="recent">Recent Purchases (7 days)</option>
<option value="unused">Unused Tickets</option>
<option value="refunded">Refunded Tickets</option>
</select>
</div>
</div>
<!-- Ticket Statistics -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-4">
<div class="text-center">
<div class="text-2xl font-bold text-white" id="total-tickets-count">0</div>
<div class="text-sm text-white/60">Total Tickets</div>
</div>
</div>
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-4">
<div class="text-center">
<div class="text-2xl font-bold text-green-400" id="used-tickets-count">0</div>
<div class="text-sm text-white/60">Used Tickets</div>
</div>
</div>
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-4">
<div class="text-center">
<div class="text-2xl font-bold text-yellow-400" id="active-tickets-count">0</div>
<div class="text-sm text-white/60">Active Tickets</div>
</div>
</div>
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-4">
<div class="text-center">
<div class="text-2xl font-bold text-red-400" id="refunded-tickets-count">0</div>
<div class="text-sm text-white/60">Refunded Tickets</div>
</div>
</div>
</div>
<!-- Tickets Table -->
<div id="tickets-table" class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl overflow-hidden">
<div class="p-4 text-center text-white/60">
<div class="animate-spin w-8 h-8 border-2 border-white/30 border-t-white rounded-full mx-auto mb-2"></div>
Loading tickets...
</div>
</div>
<!-- Pagination -->
<div class="flex justify-between items-center mt-4">
<div class="text-sm text-white/60">
Showing <span id="tickets-showing-start">0</span> to <span id="tickets-showing-end">0</span> of <span id="tickets-total">0</span> tickets
</div>
<div class="flex space-x-2">
<button id="tickets-prev-page" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-3 py-1 rounded-xl text-sm disabled:opacity-50 transition-all" disabled>
Previous
</button>
<span id="tickets-page-info" class="text-white/60 text-sm py-1 px-2">Page 1 of 1</span>
<button id="tickets-next-page" class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-3 py-1 rounded-xl text-sm disabled:opacity-50 transition-all" disabled>
Next
</button>
</div>
</div>
</div>
</div>
<!-- Data Export Tab -->
<div id="content-exports" class="tab-content p-6 hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Export Options -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Export Options</h3>
<div class="space-y-4">
<button id="export-revenue" class="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-6 py-3 rounded-xl font-medium transition-all">
Export Revenue Report
</button>
<button id="export-organizers" class="w-full bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white px-6 py-3 rounded-xl font-medium transition-all">
Export Organizer Data
</button>
<button id="export-events" class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white px-6 py-3 rounded-xl font-medium transition-all">
Export Event Analytics
</button>
<button id="export-tickets" class="w-full bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-700 hover:to-orange-700 text-white px-6 py-3 rounded-xl font-medium transition-all">
Export Ticket Sales
</button>
</div>
</div>
<!-- Custom Export -->
<div class="bg-white/5 backdrop-blur-xl border border-white/10 rounded-xl p-6">
<h3 class="text-lg font-medium text-white mb-4">Custom Export</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-white mb-2">Date Range</label>
<div class="grid grid-cols-2 gap-2">
<input type="date" id="export-start-date" class="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/60">
<input type="date" id="export-end-date" class="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/60">
</div>
</div>
<div>
<label class="block text-sm font-medium text-white mb-2">Data Type</label>
<select id="export-type" class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white">
<option value="all">All Data</option>
<option value="revenue">Revenue Only</option>
<option value="events">Events Only</option>
<option value="tickets">Tickets Only</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white mb-2">Filter By</label>
<select id="export-filter-type" class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white">
<option value="all">All Data</option>
<option value="organizer">Specific Organizer</option>
<option value="event">Specific Event</option>
<option value="category">Event Category</option>
</select>
</div>
<div id="export-filter-value" class="hidden">
<label class="block text-sm font-medium text-white mb-2">Select Filter Value</label>
<select id="export-filter-select" class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white">
<option value="">Loading...</option>
</select>
</div>
<button id="export-custom" class="w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-6 py-3 rounded-xl font-medium transition-all">
Generate Custom Export
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</Layout>
<script>
import { supabase } from '../../lib/supabase';
let currentTab = 'overview';
let revenueChart, comparisonChart, velocityChart;
// Helper function to make authenticated API calls
async function makeAuthenticatedRequest(url) {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('No authentication session');
}
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${session.access_token}`
}
});
return await response.json();
}
// Check authentication and super admin role
async function checkSuperAdminAuth() {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
window.location.href = '/';
return null;
}
const { data: user } = await supabase
.from('users')
.select('role, name, email')
.eq('id', session.user.id)
.single();
if (!user || user.role !== 'admin') {
window.location.href = '/dashboard';
return null;
}
document.getElementById('user-name').textContent = user.name || user.email;
return session;
}
// Load platform metrics
async function loadPlatformMetrics() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=platform_overview');
if (!result.success) {
throw new Error(result.error || 'Failed to load platform metrics');
}
const { summary } = result.data;
// Update UI
document.getElementById('total-revenue').textContent = `$${summary.totalRevenue.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
document.getElementById('total-fees').textContent = `$${summary.totalPlatformFees.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
document.getElementById('active-organizers').textContent = summary.activeOrganizers;
document.getElementById('total-tickets').textContent = summary.totalTickets;
// Update change indicators
document.getElementById('revenue-change').textContent = `${summary.revenueGrowth > 0 ? '+' : ''}${summary.revenueGrowth.toFixed(1)}% vs last month`;
const feesGrowth = summary.lastMonthRevenue > 0 ? ((summary.thisMonthRevenue - summary.lastMonthRevenue) / summary.lastMonthRevenue * 100) : 0;
document.getElementById('fees-change').textContent = `${feesGrowth > 0 ? '+' : ''}${feesGrowth.toFixed(1)}% vs last month`;
// Calculate organizer and ticket growth (simplified)
document.getElementById('organizers-change').textContent = `${summary.activeOrganizers} total`;
document.getElementById('tickets-change').textContent = `${summary.totalTickets} total`;
return result.data;
} catch (error) {
console.error('Error loading platform metrics:', error);
// Set error state
document.getElementById('total-revenue').textContent = 'Error';
document.getElementById('total-fees').textContent = 'Error';
document.getElementById('active-organizers').textContent = 'Error';
document.getElementById('total-tickets').textContent = 'Error';
}
}
// Load revenue chart
async function loadRevenueChart() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=platform_overview');
if (!result.success) {
throw new Error(result.error || 'Failed to load revenue chart');
}
const { monthlyTrends } = result.data;
const canvas = document.getElementById('revenueCanvas');
const ctx = canvas.getContext('2d');
// Simple chart drawing
const maxRevenue = Math.max(...monthlyTrends.map(d => d.revenue));
const chartHeight = 180;
const chartWidth = canvas.width - 80;
const barWidth = chartWidth / monthlyTrends.length;
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(34, 197, 94, 0.8)';
ctx.strokeStyle = 'rgba(34, 197, 94, 1)';
ctx.lineWidth = 2;
monthlyTrends.forEach((data, index) => {
const barHeight = maxRevenue > 0 ? (data.revenue / maxRevenue) * chartHeight : 0;
const x = 40 + index * barWidth;
const y = canvas.height - 40 - barHeight;
ctx.fillRect(x, y, barWidth - 10, barHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(data.month, x + barWidth / 2, canvas.height - 10);
ctx.fillText(`$${data.revenue.toFixed(0)}`, x + barWidth / 2, y - 5);
ctx.fillStyle = 'rgba(34, 197, 94, 0.8)';
});
} catch (error) {
console.error('Error loading revenue chart:', error);
}
}
// Load top organizers
async function loadTopOrganizers() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=platform_overview');
if (!result.success) {
throw new Error(result.error || 'Failed to load top organizers');
}
const { topOrganizers } = result.data;
const container = document.getElementById('top-organizers');
container.innerHTML = topOrganizers.map((org, index) => `
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
${index + 1}
</div>
<div>
<p class="text-white font-medium">${org.name}</p>
<p class="text-white/60 text-sm">Platform fees: $${(org.fees / 100).toFixed(2)}</p>
</div>
</div>
<div class="text-right">
<p class="text-white font-bold">$${(org.revenue / 100).toLocaleString()}</p>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading top organizers:', error);
}
}
// Load recent activity
async function loadRecentActivity() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=platform_overview');
if (!result.success) {
throw new Error(result.error || 'Failed to load recent activity');
}
const { recentActivity } = result.data;
const container = document.getElementById('recent-activity');
container.innerHTML = recentActivity.map(activity => {
let icon = '📊';
let title = '';
let subtitle = '';
switch (activity.type) {
case 'purchase':
icon = '💳';
title = `Purchase: $${(activity.amount / 100).toFixed(2)}`;
subtitle = `Transaction completed`;
break;
case 'event':
icon = '🎪';
title = `New event: ${activity.title}`;
subtitle = `Event created`;
break;
case 'organization':
icon = '🏢';
title = `New organizer: ${activity.name}`;
subtitle = `Organization registered`;
break;
}
return `
<div class="flex items-start space-x-3 p-3 bg-white/5 rounded-lg">
<div class="text-lg">${icon}</div>
<div class="flex-1">
<p class="text-white font-medium text-sm">${title}</p>
<p class="text-white/60 text-xs">${subtitle}</p>
<p class="text-white/40 text-xs mt-1">${new Date(activity.date).toLocaleDateString()}</p>
</div>
</div>
`;
}).join('');
} catch (error) {
console.error('Error loading recent activity:', error);
}
}
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('border-indigo-400', 'text-indigo-400');
btn.classList.add('border-transparent', 'text-white/60');
});
document.getElementById(`tab-${tabName}`).classList.remove('border-transparent', 'text-white/60');
document.getElementById(`tab-${tabName}`).classList.add('border-indigo-400', 'text-indigo-400');
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`content-${tabName}`).classList.remove('hidden');
currentTab = tabName;
loadTabContent(tabName);
}
// Load tab-specific content
async function loadTabContent(tabName) {
switch (tabName) {
case 'overview':
await loadRevenueChart();
await loadTopOrganizers();
await loadRecentActivity();
break;
case 'revenue':
await loadRevenueBreakdown();
await loadMonthlyComparison();
break;
case 'performance':
await loadEventPerformance();
await loadSalesVelocity();
break;
case 'events':
await loadGlobalEvents();
break;
case 'organizers':
await loadOrganizerTable();
break;
case 'tickets':
await loadTicketManagement();
break;
case 'exports':
setupExportHandlers();
break;
}
}
// Export functionality
function setupExportHandlers() {
const exportButtons = [
{ id: 'export-revenue', type: 'revenue' },
{ id: 'export-organizers', type: 'organizers' },
{ id: 'export-events', type: 'events' },
{ id: 'export-tickets', type: 'tickets' },
{ id: 'export-custom', type: 'custom' }
];
exportButtons.forEach(({ id, type }) => {
const button = document.getElementById(id);
if (button) {
button.addEventListener('click', () => exportData(type));
}
});
// Set up filter change handlers
const filterTypeSelect = document.getElementById('export-filter-type');
if (filterTypeSelect) {
filterTypeSelect.addEventListener('change', updateFilterOptions);
}
}
// Update filter options based on selected filter type
async function updateFilterOptions() {
const filterType = document.getElementById('export-filter-type').value;
const filterValueDiv = document.getElementById('export-filter-value');
const filterSelect = document.getElementById('export-filter-select');
if (filterType === 'all') {
filterValueDiv.classList.add('hidden');
return;
}
filterValueDiv.classList.remove('hidden');
filterSelect.innerHTML = '<option value="">Loading...</option>';
try {
let options = [];
switch (filterType) {
case 'organizer':
const organizerResponse = await fetch('/api/admin/super-analytics?metric=organizer_performance');
const organizerResult = await organizerResponse.json();
if (organizerResult.success) {
options = organizerResult.data.organizers.map(org => ({
value: org.id,
label: org.name
}));
}
break;
case 'event':
const eventResponse = await fetch('/api/admin/super-analytics?metric=event_analytics');
const eventResult = await eventResponse.json();
if (eventResult.success) {
options = eventResult.data.events.map(event => ({
value: event.id,
label: event.title
}));
}
break;
case 'category':
const categoryResponse = await fetch('/api/admin/super-analytics?metric=event_analytics');
const categoryResult = await categoryResponse.json();
if (categoryResult.success) {
const categories = [...new Set(categoryResult.data.events.map(event => event.category).filter(Boolean))];
options = categories.map(cat => ({
value: cat,
label: cat
}));
}
break;
}
filterSelect.innerHTML = options.length > 0
? options.map(opt => `<option value="${opt.value}">${opt.label}</option>`).join('')
: '<option value="">No options available</option>';
} catch (error) {
console.error('Error loading filter options:', error);
filterSelect.innerHTML = '<option value="">Error loading options</option>';
}
}
async function exportData(type) {
try {
const button = document.getElementById(`export-${type}`);
const originalText = button.textContent;
button.textContent = 'Exporting...';
button.disabled = true;
let data, filename;
let queryParams = new URLSearchParams();
// Handle custom export with filters
if (type === 'custom') {
const startDate = document.getElementById('export-start-date').value;
const endDate = document.getElementById('export-end-date').value;
const dataType = document.getElementById('export-type').value;
const filterType = document.getElementById('export-filter-type').value;
const filterValue = document.getElementById('export-filter-select').value;
if (startDate) queryParams.append('start_date', startDate);
if (endDate) queryParams.append('end_date', endDate);
if (filterType !== 'all' && filterValue) {
queryParams.append('filter_type', filterType);
queryParams.append('filter_value', filterValue);
}
type = dataType === 'all' ? 'revenue' : dataType;
}
switch (type) {
case 'revenue':
const revenueResponse = await fetch('/api/admin/super-analytics?metric=revenue_breakdown');
const revenueResult = await revenueResponse.json();
if (revenueResult.success) {
data = revenueResult.data.breakdown.map(org => ({
organization: org.name,
gross_revenue: org.grossRevenue,
platform_fees: org.platformFees,
net_revenue: org.netRevenue,
transactions: org.transactionCount,
events: org.eventCount
}));
} else {
data = [];
}
filename = `revenue-report-${new Date().toISOString().split('T')[0]}.csv`;
break;
case 'organizers':
const organizerResponse = await fetch('/api/admin/super-analytics?metric=organizer_performance');
const organizerResult = await organizerResponse.json();
if (organizerResult.success) {
data = organizerResult.data.organizers.map(org => ({
name: org.name,
events: org.eventCount,
published_events: org.publishedEvents,
tickets_sold: org.ticketsSold,
total_revenue: org.totalRevenue,
platform_fees: org.platformFees,
avg_ticket_price: org.avgTicketPrice,
join_date: org.joinDate
}));
} else {
data = [];
}
filename = `organizers-${new Date().toISOString().split('T')[0]}.csv`;
break;
case 'events':
const eventResponse = await fetch('/api/admin/super-analytics?metric=event_analytics');
const eventResult = await eventResponse.json();
if (eventResult.success) {
data = eventResult.data.events.map(event => ({
title: event.title,
organization_id: event.organizationId,
created_at: event.createdAt,
start_time: event.startTime,
is_published: event.isPublished,
category: event.category,
tickets_sold: event.ticketsSold,
total_revenue: event.totalRevenue,
sell_through_rate: event.sellThroughRate,
avg_ticket_price: event.avgTicketPrice
}));
} else {
data = [];
}
filename = `events-${new Date().toISOString().split('T')[0]}.csv`;
break;
case 'tickets':
// For tickets, we'll use the platform overview to get basic ticket data
const ticketResponse = await fetch('/api/admin/super-analytics?metric=platform_overview');
const ticketResult = await ticketResponse.json();
if (ticketResult.success) {
// Since we don't have detailed ticket data in the API, we'll provide summary data
data = [{
total_tickets: ticketResult.data.summary.totalTickets,
total_revenue: ticketResult.data.summary.totalRevenue,
platform_fees: ticketResult.data.summary.totalPlatformFees,
export_note: 'Summary data only - detailed ticket data available on request'
}];
} else {
data = [];
}
filename = `tickets-summary-${new Date().toISOString().split('T')[0]}.csv`;
break;
}
// Convert to CSV
const csvContent = convertToCSV(data);
downloadCSV(csvContent, filename);
button.textContent = '✓ Downloaded';
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
}, 2000);
} catch (error) {
console.error('Error exporting data:', error);
alert('Error exporting data');
}
}
function convertToCSV(data) {
if (!data.length) return '';
const headers = Object.keys(data[0]).join(',');
const rows = data.map(row =>
Object.values(row).map(value =>
typeof value === 'string' ? `"${value}"` : value
).join(',')
);
return [headers, ...rows].join('\n');
}
function downloadCSV(csvContent, filename) {
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Load revenue breakdown
async function loadRevenueBreakdown() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=revenue_breakdown');
if (!result.success) {
throw new Error(result.error || 'Failed to load revenue breakdown');
}
const { totals } = result.data;
const processingFees = totals.grossRevenue * 0.029; // Approximate processing fees
const netToOrganizers = totals.grossRevenue - totals.platformFees - processingFees;
document.getElementById('revenue-breakdown').innerHTML = `
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Gross Revenue</span>
<span class="text-white font-bold">$${totals.grossRevenue.toLocaleString()}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Platform Fees</span>
<span class="text-white font-bold">$${totals.platformFees.toLocaleString()}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Processing Fees</span>
<span class="text-white font-bold">$${processingFees.toLocaleString()}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Net to Organizers</span>
<span class="text-white font-bold">$${netToOrganizers.toLocaleString()}</span>
</div>
</div>
`;
} catch (error) {
console.error('Error loading revenue breakdown:', error);
}
}
async function loadMonthlyComparison() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=sales_trends');
if (!result.success) {
throw new Error(result.error || 'Failed to load monthly comparison');
}
const { trends, summary } = result.data;
document.getElementById('monthly-comparison').innerHTML = `
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Average Monthly Revenue</span>
<span class="text-white font-bold">$${summary.averageMonthlyRevenue.toFixed(2)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Revenue Growth</span>
<span class="text-white font-bold ${summary.growth > 0 ? 'text-green-400' : 'text-red-400'}">${summary.growth > 0 ? '+' : ''}${summary.growth.toFixed(1)}%</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Average Platform Fees</span>
<span class="text-white font-bold">$${summary.averageMonthlyFees.toFixed(2)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Total Periods</span>
<span class="text-white font-bold">${summary.totalPeriods}</span>
</div>
</div>
`;
} catch (error) {
console.error('Error loading monthly comparison:', error);
document.getElementById('monthly-comparison').innerHTML = '<p class="text-red-400">Error loading data</p>';
}
}
async function loadEventPerformance() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=event_analytics');
if (!result.success) {
throw new Error(result.error || 'Failed to load event performance');
}
const { summary } = result.data;
const now = new Date();
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// Calculate events this month (simplified)
const eventsThisMonth = Math.floor(summary.totalEvents / 12); // Rough estimate
document.getElementById('event-performance').innerHTML = `
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Average Ticket Price</span>
<span class="text-white font-bold">$${summary.avgTicketPrice.toFixed(2)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Events This Month</span>
<span class="text-white font-bold">${eventsThisMonth}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Avg. Tickets per Event</span>
<span class="text-white font-bold">${summary.totalEvents > 0 ? Math.floor(summary.totalTicketsSold / summary.totalEvents) : 0}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Sell-through Rate</span>
<span class="text-white font-bold">${summary.avgSellThroughRate.toFixed(1)}%</span>
</div>
</div>
`;
} catch (error) {
console.error('Error loading event performance:', error);
}
}
async function loadSalesVelocity() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=sales_trends');
if (!result.success) {
throw new Error(result.error || 'Failed to load sales velocity');
}
const { trends } = result.data;
const recentTrends = trends.slice(-6); // Last 6 months
// Calculate velocity metrics
const totalTransactions = recentTrends.reduce((sum, t) => sum + t.transactions, 0);
const totalRevenue = recentTrends.reduce((sum, t) => sum + t.revenue, 0);
const avgTransactionValue = totalTransactions > 0 ? totalRevenue / totalTransactions : 0;
// Calculate month-over-month growth
const currentMonth = recentTrends[recentTrends.length - 1];
const previousMonth = recentTrends[recentTrends.length - 2];
const momGrowth = previousMonth && previousMonth.revenue > 0 ?
((currentMonth.revenue - previousMonth.revenue) / previousMonth.revenue) * 100 : 0;
document.getElementById('sales-velocity').innerHTML = `
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Recent Transactions</span>
<span class="text-white font-bold">${totalTransactions}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Avg Transaction Value</span>
<span class="text-white font-bold">$${avgTransactionValue.toFixed(2)}</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Month-over-Month</span>
<span class="text-white font-bold ${momGrowth > 0 ? 'text-green-400' : 'text-red-400'}">${momGrowth > 0 ? '+' : ''}${momGrowth.toFixed(1)}%</span>
</div>
<div class="flex justify-between items-center p-3 bg-white/5 rounded-lg">
<span class="text-white">Recent Revenue</span>
<span class="text-white font-bold">$${totalRevenue.toFixed(2)}</span>
</div>
</div>
`;
} catch (error) {
console.error('Error loading sales velocity:', error);
document.getElementById('sales-velocity').innerHTML = '<p class="text-red-400">Error loading data</p>';
}
}
// Load global events with filtering
async function loadGlobalEvents() {
try {
// First, load organizers for the filter dropdown
const organizerResult = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=organizer_performance');
if (organizerResult.success && organizerResult.data.organizers) {
const organizerSelect = document.getElementById('event-organizer-filter');
organizerSelect.innerHTML = '<option value="">All Organizers</option>' +
organizerResult.data.organizers.map(org =>
`<option value="${org.id}">${org.name}</option>`
).join('');
}
// Load events
const eventResult = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=event_analytics');
if (!eventResult.success) {
throw new Error(eventResult.error || 'Failed to load events');
}
const { events } = eventResult.data;
// Get organization data for mapping
const organizerData = organizerResult.success ? organizerResult.data.organizers : [];
document.getElementById('global-events-table').innerHTML = `
<table class="w-full">
<thead class="bg-white/5">
<tr>
<th class="text-left p-4 text-white font-medium">Event</th>
<th class="text-left p-4 text-white font-medium">Organizer</th>
<th class="text-left p-4 text-white font-medium">Category</th>
<th class="text-left p-4 text-white font-medium">Date</th>
<th class="text-left p-4 text-white font-medium">Status</th>
<th class="text-left p-4 text-white font-medium">Revenue</th>
<th class="text-left p-4 text-white font-medium">Tickets</th>
<th class="text-left p-4 text-white font-medium">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
${events?.map(event => {
const organizer = organizerData.find(org => org.id === event.organizationId);
const eventDate = new Date(event.startTime);
const isPast = eventDate < new Date();
return `
<tr class="hover:bg-white/5">
<td class="p-4">
<div class="text-white font-medium">${event.title}</div>
<div class="text-white/60 text-sm">${event.sellThroughRate.toFixed(1)}% sold</div>
</td>
<td class="p-4 text-white/80">${organizer?.name || 'Unknown'}</td>
<td class="p-4 text-white/80">${event.category || 'Uncategorized'}</td>
<td class="p-4 text-white/80">
<div>${eventDate.toLocaleDateString()}</div>
<div class="text-xs text-white/60">${eventDate.toLocaleTimeString()}</div>
</td>
<td class="p-4">
<span class="px-2 py-1 rounded-full text-xs ${
!event.isPublished ? 'bg-yellow-500/20 text-yellow-400' :
isPast ? 'bg-gray-500/20 text-gray-400' :
'bg-green-500/20 text-green-400'
}">
${!event.isPublished ? 'Draft' : isPast ? 'Past' : 'Live'}
</span>
</td>
<td class="p-4 text-white/80">$${event.totalRevenue.toLocaleString()}</td>
<td class="p-4 text-white/80">${event.ticketsSold}</td>
<td class="p-4">
<div class="flex space-x-2">
<button onclick="editEvent('${event.id}')" class="bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs">
Edit
</button>
<button onclick="viewOrganizerDashboard('${event.organizationId}')" class="bg-purple-600 hover:bg-purple-700 text-white px-2 py-1 rounded text-xs">
View Org
</button>
<button onclick="deleteEvent('${event.id}')" class="bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded text-xs">
Delete
</button>
</div>
</td>
</tr>
`;
}).join('') || '<tr><td colspan="8" class="p-4 text-white/60 text-center">No events found</td></tr>'}
</tbody>
</table>
`;
// Set up filter handlers
setupEventFilterHandlers();
} catch (error) {
console.error('Error loading global events:', error);
document.getElementById('global-events-table').innerHTML = `
<div class="p-4 text-center text-red-400">
Error loading events: ${error.message}
</div>
`;
}
}
// Set up event filter handlers
function setupEventFilterHandlers() {
document.getElementById('apply-event-filters').addEventListener('click', applyEventFilters);
document.getElementById('reset-event-filters').addEventListener('click', resetEventFilters);
document.getElementById('create-event-btn').addEventListener('click', createNewEvent);
}
// Event management functions
async function applyEventFilters() {
// This would filter the events based on the selected criteria
// For now, just reload the events (in a real implementation, you'd pass filter parameters)
await loadGlobalEvents();
}
function resetEventFilters() {
document.getElementById('event-start-date').value = '';
document.getElementById('event-end-date').value = '';
document.getElementById('event-category-filter').value = '';
document.getElementById('event-organizer-filter').value = '';
document.getElementById('event-status-filter').value = '';
loadGlobalEvents();
}
function createNewEvent() {
// Navigate to event creation page
window.open('/events/new', '_blank');
}
function editEvent(eventId) {
// Navigate to event management page
window.open(`/events/${eventId}/manage`, '_blank');
}
function viewOrganizerDashboard(organizationId) {
// Navigate to organizer dashboard (you might need to implement this)
window.open(`/admin/organizer-view/${organizationId}`, '_blank');
}
function deleteEvent(eventId) {
if (confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
// Implement event deletion
console.log('Deleting event:', eventId);
// You would call an API to delete the event here
}
}
async function loadOrganizerTable() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=organizer_performance');
if (!result.success) {
throw new Error(result.error || 'Failed to load organizer performance');
}
const { organizers } = result.data;
document.getElementById('organizer-table').innerHTML = `
<table class="w-full">
<thead class="bg-white/5">
<tr>
<th class="text-left p-4 text-white font-medium">Organization</th>
<th class="text-left p-4 text-white font-medium">Events</th>
<th class="text-left p-4 text-white font-medium">Revenue</th>
<th class="text-left p-4 text-white font-medium">Platform Fees</th>
<th class="text-left p-4 text-white font-medium">Avg. Ticket Price</th>
<th class="text-left p-4 text-white font-medium">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
${organizers?.map(org => `
<tr class="hover:bg-white/5">
<td class="p-4 text-white">${org.name}</td>
<td class="p-4 text-white/80">${org.eventCount}</td>
<td class="p-4 text-white/80">$${org.totalRevenue.toLocaleString()}</td>
<td class="p-4 text-white/80">$${org.platformFees.toLocaleString()}</td>
<td class="p-4 text-white/80">$${org.avgTicketPrice.toFixed(2)}</td>
<td class="p-4">
<span class="px-2 py-1 bg-green-500/20 text-green-400 rounded-full text-xs">Active</span>
</td>
</tr>
`).join('') || '<tr><td colspan="6" class="p-4 text-white/60 text-center">No organizers found</td></tr>'}
</tbody>
</table>
`;
} catch (error) {
console.error('Error loading organizer table:', error);
}
}
// Ticket Management Functions
let currentTicketPage = 1;
let currentTicketFilters = {};
async function loadTicketManagement() {
try {
await loadTicketFilters();
await loadTicketData();
setupTicketEventHandlers();
} catch (error) {
console.error('Error loading ticket management:', error);
}
}
async function loadTicketFilters() {
try {
const result = await makeAuthenticatedRequest('/api/admin/super-analytics?metric=ticket_analytics');
if (result.success) {
const { events, organizations } = result.data.filters;
// Populate event filter
const eventSelect = document.getElementById('ticket-event-filter');
eventSelect.innerHTML = '<option value="">All Events</option>' +
events.map(event =>
`<option value="${event.id}">${event.title} (${event.organizationName})</option>`
).join('');
// Populate organizer filter
const organizerSelect = document.getElementById('ticket-organizer-filter');
organizerSelect.innerHTML = '<option value="">All Organizers</option>' +
organizations.map(org =>
`<option value="${org.id}">${org.name}</option>`
).join('');
}
} catch (error) {
console.error('Error loading ticket filters:', error);
}
}
async function loadTicketData(page = 1) {
try {
currentTicketPage = page;
// Build query parameters
const params = new URLSearchParams({
metric: 'ticket_analytics',
page: page.toString(),
limit: '50'
});
// Add filters
Object.entries(currentTicketFilters).forEach(([key, value]) => {
if (value) params.append(key, value);
});
const result = await makeAuthenticatedRequest(`/api/admin/super-analytics?${params}`);
if (result.success) {
const { tickets, stats, pagination } = result.data;
// Update statistics
document.getElementById('total-tickets-count').textContent = stats.total;
document.getElementById('used-tickets-count').textContent = stats.used;
document.getElementById('active-tickets-count').textContent = stats.active;
document.getElementById('refunded-tickets-count').textContent = stats.refunded;
// Update table
renderTicketTable(tickets);
// Update pagination
updateTicketPagination(pagination);
}
} catch (error) {
console.error('Error loading ticket data:', error);
document.getElementById('tickets-table').innerHTML = `
<div class="p-4 text-center text-red-400">
Error loading tickets: ${error.message}
</div>
`;
}
}
function renderTicketTable(tickets) {
const tableHtml = `
<table class="w-full">
<thead class="bg-white/5">
<tr>
<th class="text-left p-3 text-white font-medium">Ticket ID</th>
<th class="text-left p-3 text-white font-medium">Customer</th>
<th class="text-left p-3 text-white font-medium">Event</th>
<th class="text-left p-3 text-white font-medium">Type</th>
<th class="text-left p-3 text-white font-medium">Price</th>
<th class="text-left p-3 text-white font-medium">Seat</th>
<th class="text-left p-3 text-white font-medium">Status</th>
<th class="text-left p-3 text-white font-medium">Purchase Date</th>
<th class="text-left p-3 text-white font-medium">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
${tickets.map(ticket => {
const statusColor = {
'active': 'bg-green-500/20 text-green-400',
'used': 'bg-blue-500/20 text-blue-400',
'refunded': 'bg-red-500/20 text-red-400',
'cancelled': 'bg-gray-500/20 text-gray-400'
}[ticket.status] || 'bg-gray-500/20 text-gray-400';
return `
<tr class="hover:bg-white/5">
<td class="p-3">
<div class="text-white font-mono text-sm">${ticket.id.substring(0, 8)}...</div>
</td>
<td class="p-3">
<div class="text-white font-medium">${ticket.customerName || 'N/A'}</div>
<div class="text-white/60 text-sm">${ticket.customerEmail || 'N/A'}</div>
</td>
<td class="p-3">
<div class="text-white font-medium">${ticket.event.title}</div>
<div class="text-white/60 text-sm">${ticket.event.organizationName}</div>
</td>
<td class="p-3 text-white/80">${ticket.ticketType.name || 'General'}</td>
<td class="p-3 text-white/80">$${ticket.price.toFixed(2)}</td>
<td class="p-3 text-white/80">
${ticket.seatRow && ticket.seatNumber ?
`${ticket.seatRow}-${ticket.seatNumber}` :
'GA'}
</td>
<td class="p-3">
<span class="px-2 py-1 rounded-full text-xs ${statusColor}">
${ticket.status.charAt(0).toUpperCase() + ticket.status.slice(1)}
</span>
</td>
<td class="p-3">
<div class="text-white/80 text-sm">${new Date(ticket.createdAt).toLocaleDateString()}</div>
<div class="text-white/60 text-xs">${new Date(ticket.createdAt).toLocaleTimeString()}</div>
</td>
<td class="p-3">
<div class="flex space-x-1">
<button onclick="viewTicketDetails('${ticket.id}')" class="bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs">
View
</button>
${ticket.status === 'active' ? `
<button onclick="refundTicket('${ticket.id}')" class="bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded text-xs">
Refund
</button>
` : ''}
<button onclick="downloadTicketQR('${ticket.id}')" class="bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs">
QR
</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
document.getElementById('tickets-table').innerHTML = tableHtml;
}
function updateTicketPagination(pagination) {
const { page, total, pages } = pagination;
const start = ((page - 1) * 50) + 1;
const end = Math.min(page * 50, total);
document.getElementById('tickets-showing-start').textContent = start;
document.getElementById('tickets-showing-end').textContent = end;
document.getElementById('tickets-total').textContent = total;
document.getElementById('tickets-page-info').textContent = `Page ${page} of ${pages}`;
document.getElementById('tickets-prev-page').disabled = page <= 1;
document.getElementById('tickets-next-page').disabled = page >= pages;
}
function setupTicketEventHandlers() {
// Filter handlers
document.getElementById('apply-ticket-filters').addEventListener('click', applyTicketFilters);
document.getElementById('reset-ticket-filters').addEventListener('click', resetTicketFilters);
document.getElementById('refresh-tickets-btn').addEventListener('click', () => loadTicketData(currentTicketPage));
document.getElementById('export-tickets-btn').addEventListener('click', exportTickets);
// Pagination handlers
document.getElementById('tickets-prev-page').addEventListener('click', () => {
if (currentTicketPage > 1) {
loadTicketData(currentTicketPage - 1);
}
});
document.getElementById('tickets-next-page').addEventListener('click', () => {
loadTicketData(currentTicketPage + 1);
});
// Preset handlers
document.getElementById('ticket-filter-presets').addEventListener('change', loadTicketPreset);
document.getElementById('save-ticket-filter-preset').addEventListener('click', saveTicketPreset);
}
function applyTicketFilters() {
currentTicketFilters = {
start_date: document.getElementById('ticket-start-date').value,
end_date: document.getElementById('ticket-end-date').value,
event_id: document.getElementById('ticket-event-filter').value,
organizer_id: document.getElementById('ticket-organizer-filter').value,
min_price: document.getElementById('ticket-min-price').value,
max_price: document.getElementById('ticket-max-price').value,
ticket_type: document.getElementById('ticket-type-filter').value,
status: document.getElementById('ticket-status-filter').value,
purchase_method: document.getElementById('ticket-purchase-method-filter').value,
search: document.getElementById('ticket-search').value
};
// Remove empty filters
Object.keys(currentTicketFilters).forEach(key => {
if (!currentTicketFilters[key]) {
delete currentTicketFilters[key];
}
});
loadTicketData(1);
}
function resetTicketFilters() {
document.getElementById('ticket-start-date').value = '';
document.getElementById('ticket-end-date').value = '';
document.getElementById('ticket-event-filter').value = '';
document.getElementById('ticket-organizer-filter').value = '';
document.getElementById('ticket-min-price').value = '';
document.getElementById('ticket-max-price').value = '';
document.getElementById('ticket-type-filter').value = '';
document.getElementById('ticket-status-filter').value = '';
document.getElementById('ticket-purchase-method-filter').value = '';
document.getElementById('ticket-search').value = '';
currentTicketFilters = {};
loadTicketData(1);
}
function loadTicketPreset() {
const preset = document.getElementById('ticket-filter-presets').value;
switch (preset) {
case 'high-value':
document.getElementById('ticket-min-price').value = '100';
break;
case 'recent':
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
document.getElementById('ticket-start-date').value = weekAgo.toISOString().split('T')[0];
break;
case 'unused':
document.getElementById('ticket-status-filter').value = 'active';
break;
case 'refunded':
document.getElementById('ticket-status-filter').value = 'refunded';
break;
}
if (preset) {
applyTicketFilters();
}
}
function saveTicketPreset() {
const presetName = prompt('Enter preset name:');
if (presetName) {
// Save to localStorage
const presets = JSON.parse(localStorage.getItem('ticketFilterPresets') || '{}');
presets[presetName] = currentTicketFilters;
localStorage.setItem('ticketFilterPresets', JSON.stringify(presets));
// Update dropdown
const select = document.getElementById('ticket-filter-presets');
const option = document.createElement('option');
option.value = presetName;
option.textContent = presetName;
select.appendChild(option);
alert('Preset saved!');
}
}
function viewTicketDetails(ticketId) {
alert(`Viewing details for ticket: ${ticketId}`);
// Implement ticket details modal
}
function refundTicket(ticketId) {
if (confirm('Are you sure you want to refund this ticket?')) {
alert(`Refunding ticket: ${ticketId}`);
// Implement refund logic
}
}
function downloadTicketQR(ticketId) {
alert(`Downloading QR code for ticket: ${ticketId}`);
// Implement QR code download
}
async function exportTickets() {
try {
const button = document.getElementById('export-tickets-btn');
const originalText = button.textContent;
button.textContent = 'Exporting...';
button.disabled = true;
// Build export parameters
const params = new URLSearchParams({
metric: 'ticket_analytics',
export: 'true'
});
Object.entries(currentTicketFilters).forEach(([key, value]) => {
if (value) params.append(key, value);
});
const result = await makeAuthenticatedRequest(`/api/admin/super-analytics?${params}`);
if (result.success) {
const tickets = result.data.tickets;
const csvContent = convertTicketsToCSV(tickets);
downloadCSV(csvContent, `tickets-export-${new Date().toISOString().split('T')[0]}.csv`);
}
button.textContent = '✓ Exported';
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
}, 2000);
} catch (error) {
console.error('Error exporting tickets:', error);
alert('Error exporting tickets');
}
}
function convertTicketsToCSV(tickets) {
const headers = [
'Ticket ID', 'Customer Name', 'Customer Email', 'Event', 'Organizer',
'Ticket Type', 'Price', 'Seat', 'Status', 'Purchase Date', 'Used Date', 'Refunded Date'
];
const rows = tickets.map(ticket => [
ticket.id,
ticket.customerName || '',
ticket.customerEmail || '',
ticket.event.title,
ticket.event.organizationName,
ticket.ticketType.name || 'General',
ticket.price.toFixed(2),
ticket.seatRow && ticket.seatNumber ? `${ticket.seatRow}-${ticket.seatNumber}` : 'GA',
ticket.status,
new Date(ticket.createdAt).toLocaleDateString(),
ticket.usedAt ? new Date(ticket.usedAt).toLocaleDateString() : '',
ticket.refundedAt ? new Date(ticket.refundedAt).toLocaleDateString() : ''
]);
return [headers, ...rows].map(row =>
row.map(value => typeof value === 'string' ? `"${value}"` : value).join(',')
).join('\n');
}
// Event listeners
document.querySelectorAll('.tab-button').forEach(btn => {
btn.addEventListener('click', (e) => {
const tabName = e.target.id.replace('tab-', '');
switchTab(tabName);
});
});
document.getElementById('logout-btn').addEventListener('click', async () => {
await supabase.auth.signOut();
window.location.href = '/';
});
// Initialize
checkSuperAdminAuth().then(session => {
if (session) {
loadPlatformMetrics();
switchTab('overview');
}
});
</script>