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,74 +6,113 @@ interface Props {
const { eventId } = Astro.props;
---
<div class="bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl mb-8 overflow-hidden">
<div class="px-8 py-12 text-white">
<div class="flex justify-between items-start">
<div class="flex-1">
<h1 id="event-title" class="text-4xl font-light mb-3 tracking-wide">Loading...</h1>
<div class="flex items-center space-x-6 text-slate-200 mb-4">
<div class="flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="backdrop-blur-xl rounded-3xl shadow-2xl mb-8 overflow-hidden ring-1 transition-all duration-200 hover:shadow-3xl"
style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border-subtle); ring-color: var(--glass-ring-dark);"
data-theme-card="true">
<div class="px-8 py-12" style="color: var(--glass-text-primary);">
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center">
<div class="flex-1 mb-6 lg:mb-0">
<h1 id="event-title" class="text-3xl font-light mb-2 tracking-wide" style="color: var(--glass-text-primary);">Loading...</h1>
<div class="flex flex-wrap items-center gap-4 text-sm mb-3" style="color: var(--glass-text-secondary);">
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span id="event-venue">--</span>
</div>
<div class="flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span id="event-date">--</span>
</div>
</div>
<p id="event-description" class="text-slate-300 max-w-2xl leading-relaxed">Loading event details...</p>
<div class="max-w-2xl">
<p id="event-description" class="text-sm leading-relaxed transition-all duration-300 overflow-hidden body-text" style="color: var(--glass-text-tertiary);">Loading event details...</p>
<button id="description-toggle" class="text-xs mt-1 hidden transition-colors hover:opacity-80" style="color: var(--glass-text-accent);">Show more</button>
</div>
</div>
<div class="flex flex-col items-end space-y-3">
<div class="flex space-x-3">
<div class="flex flex-col items-end space-y-4">
<!-- Revenue Display -->
<div class="text-right">
<div class="text-2xl font-semibold" id="total-revenue" style="color: var(--glass-text-primary);">$0</div>
<div class="text-xs" style="color: var(--glass-text-secondary);">Total Revenue</div>
</div>
<!-- Compact Button Grid -->
<div class="grid grid-cols-3 gap-2 lg:gap-3">
<!-- Top Row: Quick Actions -->
<a
id="preview-link"
href="#"
target="_blank"
class="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 backdrop-blur-sm flex items-center gap-2"
class="px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out backdrop-blur-sm flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg ring-1 hover:ring-2"
style="background: var(--glass-bg-button); color: var(--glass-text-primary); border: 1px solid var(--glass-border); ring-color: var(--glass-ring-dark);"
title="Preview Page"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
Preview Page
Preview
</a>
<button
id="embed-code-btn"
class="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 backdrop-blur-sm flex items-center gap-2"
class="px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out backdrop-blur-sm flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg ring-1 hover:ring-2"
style="background: var(--glass-bg-button); color: var(--glass-text-primary); border: 1px solid var(--glass-border); ring-color: var(--glass-ring-dark);"
title="Get Embed Code"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
</svg>
Get Embed Code
Embed
</button>
<button
id="edit-event-btn"
class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 ease-in-out flex items-center justify-center gap-1 text-xs hover:scale-[1.02] hover:shadow-lg"
title="Edit Event"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
Edit
</button>
<!-- Bottom Row: Tools -->
<a
href="/scan"
class="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 backdrop-blur-sm flex items-center gap-2"
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
title="Scanner"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h4m-6 0h-2v4m0-11v3m0 0h.01M12 12h.01M16 8h4m-6 0h-2v4m0-11v3m0 0h.01M12 12h.01"></path>
</svg>
Scanner
</a>
<button
id="edit-event-btn"
class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center gap-2"
<a
id="kiosk-link"
href="#"
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
title="Sales Kiosk"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
Edit Event
Kiosk
</a>
<button
id="generate-kiosk-pin-btn"
class="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg font-medium transition-all duration-200 backdrop-blur-sm flex items-center justify-center gap-1 text-xs"
title="Generate Kiosk PIN"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
PIN
</button>
</div>
<div class="text-right">
<div class="text-3xl font-semibold" id="total-revenue">$0</div>
<div class="text-sm text-slate-300">Total Revenue</div>
</div>
</div>
</div>
</div>
@@ -87,50 +126,141 @@ const { eventId } = Astro.props;
async function loadEventHeader() {
try {
const { createClient } = await import('@supabase/supabase-js');
const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);
const { api } = await import('/src/lib/api-router.js');
// Load event details and stats using the new API system
const result = await api.loadEventPage(eventId);
if (!result.event) {
return;
}
// Load event data
const { data: event, error } = await supabase
.from('events')
.select('*')
.eq('id', eventId)
.single();
// Update event details
document.getElementById('event-title').textContent = result.event.title;
document.getElementById('event-venue').textContent = result.event.venue;
// Use start_time from database
document.getElementById('event-date').textContent = api.formatDate(result.event.start_time);
// Handle description truncation
const descriptionEl = document.getElementById('event-description');
const toggleBtn = document.getElementById('description-toggle');
const fullDescription = result.event.description;
const maxLength = 120; // Show about one line
if (fullDescription && fullDescription.length > maxLength) {
// Set initial truncated text
descriptionEl.textContent = fullDescription.substring(0, maxLength) + '...';
descriptionEl.dataset.fullText = fullDescription;
descriptionEl.dataset.truncatedText = fullDescription.substring(0, maxLength) + '...';
descriptionEl.dataset.expanded = 'false';
// Show toggle button
toggleBtn.classList.remove('hidden');
// Add click handler
toggleBtn.addEventListener('click', () => {
const isExpanded = descriptionEl.dataset.expanded === 'true';
if (isExpanded) {
descriptionEl.textContent = descriptionEl.dataset.truncatedText;
toggleBtn.textContent = 'Show more';
descriptionEl.dataset.expanded = 'false';
} else {
descriptionEl.textContent = descriptionEl.dataset.fullText;
toggleBtn.textContent = 'Show less';
descriptionEl.dataset.expanded = 'true';
}
});
} else {
// Description is short enough, show full text
descriptionEl.textContent = fullDescription;
}
document.getElementById('preview-link').href = `/e/${result.event.slug}`;
document.getElementById('kiosk-link').href = `/kiosk/${result.event.slug}`;
if (error) throw error;
// Update UI
document.getElementById('event-title').textContent = event.title;
document.getElementById('event-venue').textContent = event.venue;
document.getElementById('event-date').textContent = new Date(event.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
document.getElementById('event-description').textContent = event.description;
document.getElementById('preview-link').href = `/e/${event.slug}`;
// Load stats
const { data: tickets } = await supabase
.from('tickets')
.select('price_paid')
.eq('event_id', eventId)
.eq('status', 'confirmed');
const totalRevenue = tickets?.reduce((sum, ticket) => sum + ticket.price_paid, 0) || 0;
document.getElementById('total-revenue').textContent = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(totalRevenue / 100);
// Update revenue from stats
if (result.stats) {
document.getElementById('total-revenue').textContent = api.formatCurrency(result.stats.totalRevenue);
}
} catch (error) {
console.error('Error loading event header:', error);
// Error loading event header
}
}
// Generate Kiosk PIN functionality
document.getElementById('generate-kiosk-pin-btn').addEventListener('click', async () => {
if (!confirm('Generate a new PIN for the sales kiosk? This will invalidate any existing PIN.')) {
return;
}
const btn = document.getElementById('generate-kiosk-pin-btn');
const originalText = btn.innerHTML;
try {
// Show loading state
btn.innerHTML = '<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> ...';
btn.disabled = true;
const { api } = await import('/src/lib/api-router.js');
const { supabase } = await import('/src/lib/supabase.js');
// Get auth token
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('Not authenticated');
}
// Generate PIN
const response = await fetch('/api/kiosk/generate-pin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
body: JSON.stringify({ eventId })
});
let result;
try {
result = await response.json();
} catch (parseError) {
throw new Error('Server returned invalid response. Please try again.');
}
if (!response.ok) {
throw new Error(result.error || 'Failed to generate PIN');
}
// Send PIN email
const emailResponse = await fetch('/api/kiosk/send-pin-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: result.event,
pin: result.pin,
email: result.userEmail
})
});
const emailResult = await emailResponse.json();
if (!emailResponse.ok) {
alert(`PIN Generated: ${result.pin}\n\nEmail delivery failed. Please note this PIN manually.`);
} else {
alert(`PIN generated successfully!\n\nA new 4-digit PIN has been sent to ${result.userEmail}.\n\nThe PIN expires in 24 hours.`);
}
} catch (error) {
alert('Failed to generate PIN: ' + error.message);
} finally {
// Restore button
btn.innerHTML = originalText;
btn.disabled = false;
}
});
</script>