feat: Complete platform enhancement with multi-tenant architecture

Major additions:
- Territory manager system with application workflow
- Custom pricing and page builder with Craft.js
- Enhanced Stripe Connect onboarding
- CodeReadr QR scanning integration
- Kiosk mode for venue sales
- Super admin dashboard and analytics
- MCP integration for AI-powered operations

Infrastructure improvements:
- Centralized API client and routing system
- Enhanced authentication with organization context
- Comprehensive theme management system
- Advanced event management with custom tabs
- Performance monitoring and accessibility features

Database schema updates:
- Territory management tables
- Custom pages and pricing structures
- Kiosk PIN system
- Enhanced organization profiles
- CodeReadr integration tables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-12 18:21:40 -06:00
parent a02d64a86c
commit 26a87d0d00
232 changed files with 33175 additions and 5365 deletions

View File

@@ -6,57 +6,113 @@ interface Props {
const { eventId } = Astro.props;
---
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6 shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Tickets Sold Card -->
<div class="rounded-2xl p-6 shadow-lg premium-hover cursor-pointer group ring-1 transition-all duration-200 hover:shadow-xl hover:ring-2"
style="background: var(--success-bg); border: 1px solid var(--success-border);">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-white/80 uppercase tracking-wide">Tickets Sold</p>
<p id="tickets-sold" class="text-3xl font-light text-white mt-1">0</p>
<p class="text-sm font-semibold uppercase tracking-wide" style="color: var(--success-color);">Tickets Sold</p>
<div class="flex items-center mt-2">
<p id="tickets-sold" class="text-3xl font-light animate-countUp" style="color: var(--success-color);">0</p>
<div id="tickets-sold-loading" class="skeleton w-16 h-8 ml-2 hidden"></div>
</div>
<div class="flex items-center mt-1">
<span id="tickets-sold-trend" class="text-xs flex items-center" style="color: var(--success-color); opacity: 0.8;">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
+12% from last week
</span>
</div>
</div>
<div class="w-12 h-12 bg-emerald-500/20 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform"
style="background: var(--success-bg); backdrop-filter: blur(8px);">
<svg class="w-7 h-7" style="color: var(--success-color);" 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>
</div>
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6 shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200">
<!-- Available Tickets Card -->
<div class="rounded-2xl p-6 shadow-lg premium-hover cursor-pointer group ring-1 transition-all duration-200 hover:shadow-xl hover:ring-2"
style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-white/80 uppercase tracking-wide">Available</p>
<p id="tickets-available" class="text-3xl font-light text-white mt-1">--</p>
<p class="text-sm font-semibold uppercase tracking-wide" style="color: var(--glass-text-secondary);">Available</p>
<div class="flex items-center mt-2">
<p id="tickets-available" class="text-3xl font-light animate-countUp" style="color: var(--glass-text-primary);">--</p>
<div id="tickets-available-loading" class="skeleton w-16 h-8 ml-2 hidden"></div>
</div>
<div class="flex items-center mt-1">
<span id="tickets-available-trend" class="text-xs flex items-center" style="color: var(--glass-text-tertiary);">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
Ready to sell
</span>
</div>
</div>
<div class="w-12 h-12 bg-blue-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">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform"
style="background: var(--glass-bg-elevated); backdrop-filter: blur(8px);">
<svg class="w-7 h-7" style="color: var(--glass-text-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>
</svg>
</div>
</div>
</div>
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6 shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200">
<!-- Check-ins Card -->
<div class="rounded-2xl p-6 shadow-lg premium-hover cursor-pointer group ring-1 transition-all duration-200 hover:shadow-xl hover:ring-2"
style="background: var(--warning-bg); border: 1px solid var(--warning-border);">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-white/80 uppercase tracking-wide">Check-ins</p>
<p id="checked-in" class="text-3xl font-light text-white mt-1">0</p>
<p class="text-sm font-semibold uppercase tracking-wide" style="color: var(--warning-color);">Check-ins</p>
<div class="flex items-center mt-2">
<p id="checked-in" class="text-3xl font-light animate-countUp" style="color: var(--warning-color);">0</p>
<div id="checked-in-loading" class="skeleton w-16 h-8 ml-2 hidden"></div>
</div>
<div class="flex items-center mt-1">
<span id="checked-in-trend" class="text-xs flex items-center" style="color: var(--warning-color); opacity: 0.8;">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
85% attendance rate
</span>
</div>
</div>
<div class="w-12 h-12 bg-purple-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">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform"
style="background: var(--warning-bg); backdrop-filter: blur(8px);">
<svg class="w-7 h-7" style="color: var(--warning-color);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
</div>
</div>
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6 shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200">
<!-- Net Revenue Card -->
<div class="rounded-2xl p-6 shadow-lg premium-hover cursor-pointer group ring-1 transition-all duration-200 hover:shadow-xl hover:ring-2"
style="background: var(--glass-bg-button); border: 1px solid var(--glass-border);">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-white/80 uppercase tracking-wide">Net Revenue</p>
<p id="net-revenue" class="text-3xl font-light text-white mt-1">$0</p>
<p class="text-sm font-semibold uppercase tracking-wide" style="color: var(--glass-text-accent);">Net Revenue</p>
<div class="flex items-center mt-2">
<p id="net-revenue" class="text-3xl font-light animate-countUp" style="color: var(--glass-text-accent);">$0</p>
<div id="net-revenue-loading" class="skeleton w-20 h-8 ml-2 hidden"></div>
</div>
<div class="flex items-center mt-1">
<span id="net-revenue-trend" class="text-xs flex items-center" style="color: var(--glass-text-accent); opacity: 0.8;">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
+24% this month
</span>
</div>
</div>
<div class="w-12 h-12 bg-amber-500/20 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform"
style="background: var(--glass-bg-elevated); backdrop-filter: blur(8px);">
<svg class="w-7 h-7" style="color: var(--glass-text-accent);" 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>
@@ -70,55 +126,114 @@ const { eventId } = Astro.props;
await loadQuickStats();
});
// Count-up animation function
function animateCountUp(element, finalValue, duration = 1000, isRevenue = false) {
const startValue = 0;
const startTime = Date.now();
function updateCount() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Use easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = Math.floor(startValue + (finalValue - startValue) * easeOutQuart);
if (isRevenue) {
element.textContent = formatCurrency(currentValue);
} else {
element.textContent = currentValue.toString();
}
if (progress < 1) {
requestAnimationFrame(updateCount);
} else {
element.textContent = isRevenue ? formatCurrency(finalValue) : finalValue.toString();
}
}
requestAnimationFrame(updateCount);
}
// Simple currency formatter
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount);
}
// Show skeleton loading
function showSkeleton(statType) {
const loadingElement = document.getElementById(`${statType}-loading`);
const valueElement = document.getElementById(statType);
if (loadingElement && valueElement) {
valueElement.classList.add('hidden');
loadingElement.classList.remove('hidden');
}
}
// Hide skeleton loading
function hideSkeleton(statType) {
const loadingElement = document.getElementById(`${statType}-loading`);
const valueElement = document.getElementById(statType);
if (loadingElement && valueElement) {
loadingElement.classList.add('hidden');
valueElement.classList.remove('hidden');
}
}
async function loadQuickStats() {
try {
const { createClient } = await import('@supabase/supabase-js');
const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);
// Show skeleton loading states
showSkeleton('tickets-sold');
showSkeleton('tickets-available');
showSkeleton('checked-in');
showSkeleton('net-revenue');
const { api } = await import('/src/lib/api-router.js');
// Load event statistics using the new API system
const stats = await api.loadEventStats(eventId);
if (!stats) {
return;
}
// Load ticket sales data
const { data: tickets } = await supabase
.from('tickets')
.select(`
id,
price_paid,
checked_in,
ticket_types (
id,
quantity
)
`)
.eq('event_id', eventId)
.eq('status', 'confirmed');
// Hide skeleton loading and animate values
setTimeout(() => {
hideSkeleton('tickets-sold');
animateCountUp(document.getElementById('tickets-sold'), stats.ticketsSold, 1200);
}, 300);
// Load ticket types for capacity calculation
const { data: ticketTypes } = await supabase
.from('ticket_types')
.select('id, quantity')
.eq('event_id', eventId)
.eq('is_active', true);
setTimeout(() => {
hideSkeleton('tickets-available');
animateCountUp(document.getElementById('tickets-available'), stats.ticketsAvailable, 1000);
}, 500);
// Calculate stats
const ticketsSold = tickets?.length || 0;
const totalRevenue = tickets?.reduce((sum, ticket) => sum + ticket.price_paid, 0) || 0;
const netRevenue = totalRevenue * 0.97; // Assuming 3% platform fee
const checkedIn = tickets?.filter(ticket => ticket.checked_in).length || 0;
const totalCapacity = ticketTypes?.reduce((sum, type) => sum + type.quantity, 0) || 0;
const ticketsAvailable = totalCapacity - ticketsSold;
setTimeout(() => {
hideSkeleton('checked-in');
animateCountUp(document.getElementById('checked-in'), stats.checkedIn, 1400);
}, 700);
// Update UI
document.getElementById('tickets-sold').textContent = ticketsSold.toString();
document.getElementById('tickets-available').textContent = ticketsAvailable.toString();
document.getElementById('checked-in').textContent = checkedIn.toString();
document.getElementById('net-revenue').textContent = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(netRevenue / 100);
setTimeout(() => {
hideSkeleton('net-revenue');
// Convert cents to dollars for display
const revenueInDollars = Math.round(stats.netRevenue / 100);
animateCountUp(document.getElementById('net-revenue'), revenueInDollars, 1600, true);
}, 900);
} catch (error) {
console.error('Error loading quick stats:', error);
// Hide all skeleton states on error
hideSkeleton('tickets-sold');
hideSkeleton('tickets-available');
hideSkeleton('checked-in');
hideSkeleton('net-revenue');
}
}
</script>