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>
1761 lines
78 KiB
Plaintext
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> |