feat: add advanced analytics and territory management system

- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-26 09:25:10 -06:00
parent d5c3953888
commit aa81eb5adb
438 changed files with 90509 additions and 2787 deletions

487
test-data-setup.cjs Normal file
View File

@@ -0,0 +1,487 @@
// @ts-check
const { test, expect } = require('@playwright/test');
/**
* Test Data Setup and Event Creation
*
* This script helps create test events and data for ticket purchasing tests.
* It can be run independently or as part of the test suite setup.
*/
class TestDataManager {
constructor(page) {
this.page = page;
}
async createTestEvent(eventData) {
// This would typically interact with your admin interface or API
// to create test events. For now, we'll mock the responses.
const eventSlug = eventData.slug || 'test-event-' + Date.now();
// Mock the event page response
await this.page.route(`**/e/${eventSlug}`, async route => {
const mockEventHTML = this.generateMockEventHTML(eventData, eventSlug);
await route.fulfill({
status: 200,
contentType: 'text/html',
body: mockEventHTML
});
});
return { ...eventData, slug: eventSlug };
}
generateMockEventHTML(eventData, slug) {
return `
<!DOCTYPE html>
<html>
<head>
<title>${eventData.title} - Black Canyon Tickets</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--ui-text-primary: #1f2937;
--ui-text-secondary: #6b7280;
--ui-text-tertiary: #9ca3af;
--ui-bg-elevated: #ffffff;
--ui-bg-secondary: #f9fafb;
--ui-border-primary: #d1d5db;
--ui-border-secondary: #e5e7eb;
--glass-border-focus: #3b82f6;
--success-color: #10b981;
--success-border: #10b981;
--success-bg: #ecfdf5;
--error-color: #ef4444;
--warning-color: #f59e0b;
--warning-bg: #fffbeb;
--warning-border: #f59e0b;
}
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 0; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.event-header { background: linear-gradient(to right, #1f2937, #374151); color: white; padding: 24px; border-radius: 16px; margin-bottom: 24px; }
.ticket-section { background: white; border: 2px solid var(--ui-border-primary); border-radius: 16px; padding: 24px; }
.ticket-type { border: 2px solid var(--ui-border-primary); border-radius: 16px; padding: 16px; margin-bottom: 16px; }
.quantity-controls { display: flex; align-items: center; gap: 12px; }
.quantity-btn { width: 48px; height: 48px; border: 2px solid var(--ui-border-primary); border-radius: 12px; background: var(--ui-bg-elevated); font-size: 18px; font-weight: bold; cursor: pointer; }
.quantity-btn:hover { border-color: var(--glass-border-focus); }
.quantity-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.order-summary { background: var(--ui-bg-secondary); border: 2px solid var(--ui-border-primary); border-radius: 16px; padding: 24px; margin-top: 24px; display: none; }
.order-summary.visible { display: block; }
.form-group { margin-bottom: 16px; }
.form-input { width: 100%; padding: 12px; border: 2px solid var(--ui-border-primary); border-radius: 12px; font-size: 16px; }
.form-input:focus { border-color: var(--glass-border-focus); outline: none; }
.purchase-btn { width: 100%; padding: 16px; background: var(--success-color); color: white; border: none; border-radius: 16px; font-size: 18px; font-weight: bold; cursor: pointer; }
.purchase-btn:hover { background: var(--success-border); }
.timer { background: var(--warning-bg); border: 2px solid var(--warning-border); border-radius: 12px; padding: 12px; margin: 16px 0; display: none; }
.timer.visible { display: block; }
.presale-section { background: var(--ui-bg-secondary); border: 2px solid var(--ui-border-primary); border-radius: 16px; padding: 20px; margin-bottom: 20px; }
${eventData.presaleRequired ? '' : '.presale-section { display: none; }'}
</style>
</head>
<body>
<div class="container">
<!-- Event Header -->
<div class="event-header">
<h1 data-test="event-title">${eventData.title}</h1>
<div data-test="event-date">📅 ${eventData.date}</div>
<div data-test="venue">📍 ${eventData.venue}</div>
${eventData.description ? `<p data-test="event-description">${eventData.description}</p>` : ''}
</div>
<!-- Presale Code Section -->
${eventData.presaleRequired ? `
<div class="presale-section" data-test="presale-code-section">
<label for="presale-code">Presale Code Required:</label>
<div style="display: flex; gap: 12px; margin-top: 8px;">
<input
id="presale-code"
type="text"
placeholder="Enter your presale code"
class="form-input"
style="flex: 1;"
/>
<button onclick="validatePresaleCode()" style="padding: 12px 24px; background: var(--success-color); color: white; border: none; border-radius: 12px; cursor: pointer;">
Apply Code
</button>
</div>
<div id="presale-error" data-test="presale-error" style="color: var(--error-color); margin-top: 8px; display: none;"></div>
<div id="presale-success" data-test="presale-success" style="color: var(--success-color); margin-top: 8px; display: none;">✓ Presale access granted</div>
</div>
` : ''}
<!-- Ticket Selection -->
<div class="ticket-section" data-test="ticket-checkout">
<h2>Get Your Tickets</h2>
${eventData.ticketTypes.map((ticket, index) => `
<div class="ticket-type" data-test="ticket-type" data-ticket-index="${index}">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3>${ticket.name}</h3>
${ticket.description ? `<p style="color: var(--ui-text-secondary); margin: 8px 0;">${ticket.description}</p>` : ''}
<div style="display: flex; align-items: center; gap: 16px;">
<span style="font-size: 24px; font-weight: bold;">$${ticket.price.toFixed(2)}</span>
<span style="color: var(--ui-text-secondary);" data-test="availability-display">
${ticket.available} available
</span>
</div>
</div>
<div class="quantity-controls">
<button
class="quantity-btn decrease-btn"
onclick="changeQuantity(${index}, -1)"
disabled
aria-label="Decrease quantity"
></button>
<span style="font-size: 18px; font-weight: bold; min-width: 24px; text-align: center;" data-test="quantity-display">0</span>
<button
class="quantity-btn increase-btn"
onclick="changeQuantity(${index}, 1)"
aria-label="Increase quantity"
>+</button>
</div>
</div>
</div>
`).join('')}
<!-- Reservation Timer -->
<div class="timer" data-test="reservation-timer">
<span>⏰ Tickets reserved for <span data-test="time-remaining">15:00</span></span>
</div>
<!-- Order Summary -->
<div class="order-summary" data-test="order-summary">
<h3>Order Summary</h3>
<div id="order-items"></div>
<div style="border-top: 1px solid var(--ui-border-secondary); margin: 16px 0; padding-top: 16px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Subtotal:</span>
<span data-test="subtotal">$0.00</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Platform fee:</span>
<span data-test="platform-fee">$0.00</span>
</div>
<div style="display: flex; justify-content: space-between; font-size: 20px; font-weight: bold;">
<span>Total:</span>
<span data-test="total">$0.00</span>
</div>
</div>
<!-- Customer Information Form -->
<form id="purchase-form" onsubmit="handlePurchase(event)">
<div class="form-group">
<label for="email">Email Address:</label>
<input type="email" id="email" class="form-input" required />
</div>
<div class="form-group">
<label for="name">Full Name:</label>
<input type="text" id="name" class="form-input" required />
</div>
<button type="submit" class="purchase-btn">Complete Purchase</button>
</form>
</div>
</div>
</div>
<script>
let quantities = ${JSON.stringify(eventData.ticketTypes.map(() => 0))};
let reservationTimer = null;
let timeRemaining = 15 * 60; // 15 minutes in seconds
let presaleValidated = ${!eventData.presaleRequired};
const ticketTypes = ${JSON.stringify(eventData.ticketTypes)};
function changeQuantity(index, delta) {
if (!presaleValidated && ${eventData.presaleRequired}) {
alert('Please enter a valid presale code first.');
return;
}
const newQuantity = Math.max(0, quantities[index] + delta);
const available = ticketTypes[index].available;
if (newQuantity > available) {
alert('Not enough tickets available.');
return;
}
quantities[index] = newQuantity;
updateQuantityDisplay(index);
updateOrderSummary();
if (getTotalQuantity() > 0 && !reservationTimer) {
startReservationTimer();
} else if (getTotalQuantity() === 0 && reservationTimer) {
stopReservationTimer();
}
}
function updateQuantityDisplay(index) {
const ticketElement = document.querySelector(\`[data-ticket-index="\${index}"]\`);
const quantityDisplay = ticketElement.querySelector('[data-test="quantity-display"]');
const decreaseBtn = ticketElement.querySelector('.decrease-btn');
const increaseBtn = ticketElement.querySelector('.increase-btn');
quantityDisplay.textContent = quantities[index];
decreaseBtn.disabled = quantities[index] <= 0;
increaseBtn.disabled = quantities[index] >= ticketTypes[index].available;
}
function updateOrderSummary() {
const orderSummary = document.querySelector('[data-test="order-summary"]');
const totalQuantity = getTotalQuantity();
if (totalQuantity > 0) {
orderSummary.classList.add('visible');
let subtotal = 0;
let orderItemsHTML = '';
quantities.forEach((qty, index) => {
if (qty > 0) {
const ticket = ticketTypes[index];
const itemTotal = qty * ticket.price;
subtotal += itemTotal;
orderItemsHTML += \`
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>\${qty}x \${ticket.name}</span>
<span>$\${itemTotal.toFixed(2)}</span>
</div>
\`;
}
});
document.getElementById('order-items').innerHTML = orderItemsHTML;
const platformFee = Math.round(subtotal * 0.05 * 100) / 100; // 5% platform fee
const total = subtotal + platformFee;
document.querySelector('[data-test="subtotal"]').textContent = \`$\${subtotal.toFixed(2)}\`;
document.querySelector('[data-test="platform-fee"]').textContent = \`$\${platformFee.toFixed(2)}\`;
document.querySelector('[data-test="total"]').textContent = \`$\${total.toFixed(2)}\`;
} else {
orderSummary.classList.remove('visible');
}
}
function getTotalQuantity() {
return quantities.reduce((sum, qty) => sum + qty, 0);
}
function startReservationTimer() {
const timerElement = document.querySelector('[data-test="reservation-timer"]');
const timeDisplay = document.querySelector('[data-test="time-remaining"]');
timerElement.classList.add('visible');
reservationTimer = setInterval(() => {
timeRemaining--;
if (timeRemaining <= 0) {
alert('Your ticket reservation has expired. Please select your tickets again.');
location.reload();
return;
}
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
timeDisplay.textContent = \`\${minutes}:\${seconds.toString().padStart(2, '0')}\`;
}, 1000);
}
function stopReservationTimer() {
if (reservationTimer) {
clearInterval(reservationTimer);
reservationTimer = null;
document.querySelector('[data-test="reservation-timer"]').classList.remove('visible');
timeRemaining = 15 * 60;
}
}
function validatePresaleCode() {
const code = document.getElementById('presale-code').value.trim().toUpperCase();
const errorDiv = document.getElementById('presale-error');
const successDiv = document.getElementById('presale-success');
if (!code) {
errorDiv.textContent = 'Please enter a presale code';
errorDiv.style.display = 'block';
successDiv.style.display = 'none';
return;
}
// Mock validation - accept any code that contains "VALID"
if (code.includes('VALID')) {
presaleValidated = true;
errorDiv.style.display = 'none';
successDiv.style.display = 'block';
} else {
errorDiv.textContent = 'Invalid presale code';
errorDiv.style.display = 'block';
successDiv.style.display = 'none';
}
}
function handlePurchase(event) {
event.preventDefault();
const email = document.getElementById('email').value;
const name = document.getElementById('name').value;
if (!email || !name) {
alert('Please fill in all required fields.');
return;
}
// Mock purchase completion
alert(\`Purchase completed!\\n\\nCustomer: \${name}\\nEmail: \${email}\\nTickets: \${getTotalQuantity()}\`);
// Reset form
document.getElementById('purchase-form').reset();
quantities = quantities.map(() => 0);
quantities.forEach((_, index) => updateQuantityDisplay(index));
updateOrderSummary();
stopReservationTimer();
}
// Initialize displays
quantities.forEach((_, index) => updateQuantityDisplay(index));
</script>
</body>
</html>
`;
}
// Predefined test events
static getTestEvents() {
return {
basicEvent: {
title: 'Sample Music Concert',
date: 'Friday, December 15, 2024 at 8:00 PM',
venue: 'Grand Theater',
description: 'An amazing evening of live music featuring local artists.',
presaleRequired: false,
ticketTypes: [
{ name: 'General Admission', price: 25.00, available: 50, description: 'Standing room on the main floor' },
{ name: 'VIP Seating', price: 75.00, available: 20, description: 'Reserved seating with complimentary drink' },
{ name: 'Student Discount', price: 15.00, available: 30, description: 'Valid student ID required at entry' }
]
},
presaleEvent: {
title: 'Exclusive Art Gallery Opening',
date: 'Saturday, December 16, 2024 at 6:00 PM',
venue: 'Modern Art Museum',
description: 'Private preview of the new contemporary art exhibition.',
presaleRequired: true,
ticketTypes: [
{ name: 'Early Bird Special', price: 35.00, available: 25, description: 'First access to the exhibition' },
{ name: 'Collector Pass', price: 95.00, available: 10, description: 'Includes exclusive artist meet & greet' }
]
},
soldOutEvent: {
title: 'Popular Dance Performance',
date: 'Sunday, December 17, 2024 at 7:30 PM',
venue: 'City Performance Hall',
description: 'Award-winning dance troupe presents their latest show.',
presaleRequired: false,
ticketTypes: [
{ name: 'Orchestra Seating', price: 65.00, available: 0, description: 'Best view of the performance' },
{ name: 'Balcony Seating', price: 45.00, available: 0, description: 'Elevated view with great acoustics' }
]
},
lowStockEvent: {
title: 'Intimate Jazz Session',
date: 'Monday, December 18, 2024 at 9:00 PM',
venue: 'Blue Note Lounge',
description: 'Close-up performance with renowned jazz musicians.',
presaleRequired: false,
ticketTypes: [
{ name: 'Table for Two', price: 120.00, available: 2, description: 'Prime table with bottle service' },
{ name: 'Bar Seating', price: 45.00, available: 3, description: 'Seats at the jazz bar' },
{ name: 'Standing Room', price: 25.00, available: 8, description: 'Standing area near the stage' }
]
}
};
}
}
// Export for use in other test files
if (typeof module !== 'undefined' && module.exports) {
module.exports = { TestDataManager };
}
// Standalone test for data setup validation
test.describe('Test Data Setup', () => {
test('should create basic test event successfully', async ({ page }) => {
const dataManager = new TestDataManager(page);
const testEvents = TestDataManager.getTestEvents();
const event = await dataManager.createTestEvent(testEvents.basicEvent);
await page.goto(`/e/${event.slug}`);
// Verify event was created correctly
await expect(page.locator('[data-test="event-title"]')).toContainText(event.title);
await expect(page.locator('[data-test="venue"]')).toContainText(event.venue);
// Verify ticket types are present
const ticketTypes = page.locator('[data-test="ticket-type"]');
await expect(ticketTypes).toHaveCount(event.ticketTypes.length);
console.log(`✓ Test event created: ${event.title} (${event.slug})`);
});
test('should create presale event with code validation', async ({ page }) => {
const dataManager = new TestDataManager(page);
const testEvents = TestDataManager.getTestEvents();
const event = await dataManager.createTestEvent(testEvents.presaleEvent);
await page.goto(`/e/${event.slug}`);
// Verify presale section is visible
await expect(page.locator('[data-test="presale-code-section"]')).toBeVisible();
// Test invalid code
await page.fill('#presale-code', 'INVALID123');
await page.click('button:has-text("Apply Code")');
await expect(page.locator('[data-test="presale-error"]')).toBeVisible();
// Test valid code
await page.fill('#presale-code', 'VALID123');
await page.click('button:has-text("Apply Code")');
await expect(page.locator('[data-test="presale-success"]')).toBeVisible();
console.log(`✓ Presale event created with validation: ${event.title}`);
});
test('should create sold out event with disabled controls', async ({ page }) => {
const dataManager = new TestDataManager(page);
const testEvents = TestDataManager.getTestEvents();
const event = await dataManager.createTestEvent(testEvents.soldOutEvent);
await page.goto(`/e/${event.slug}`);
// Verify all increase buttons are disabled
const increaseButtons = page.locator('.increase-btn');
const buttonCount = await increaseButtons.count();
for (let i = 0; i < buttonCount; i++) {
await expect(increaseButtons.nth(i)).toBeDisabled();
}
// Verify availability display shows 0
await expect(page.locator('[data-test="availability-display"]').first()).toContainText('0 available');
console.log(`✓ Sold out event created: ${event.title}`);
});
});