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:
487
test-data-setup.cjs
Normal file
487
test-data-setup.cjs
Normal 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}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user