From 5f4c474e3776e90d74fe82f05568f43ed7957a57 Mon Sep 17 00:00:00 2001 From: kyle Date: Wed, 4 Feb 2026 00:22:59 +0100 Subject: [PATCH] feat: improve VIP table display and rewrite seed service for new paradigm - EventList VIP column: compact layout with max 2 names shown, party size badges, "+N more" indicator, and total passenger count - Seed service: 20 VIPs with party sizes, 8 drivers, 8 vehicles, 13 master events over 3 days with linked transport legs, realistic capacity planning and conflict-free driver/vehicle assignments Co-Authored-By: Claude Opus 4.5 --- backend/src/seed/seed.service.ts | 1145 +++++++++++++++++++++--------- frontend/src/pages/EventList.tsx | 27 +- 2 files changed, 826 insertions(+), 346 deletions(-) diff --git a/backend/src/seed/seed.service.ts b/backend/src/seed/seed.service.ts index c7c1920..86cef68 100644 --- a/backend/src/seed/seed.service.ts +++ b/backend/src/seed/seed.service.ts @@ -53,7 +53,7 @@ export class SeedService { // Create all entities in a transaction const result = await this.prisma.$transaction(async (tx) => { - // 1. Create VIPs + // 1. Create VIPs with party sizes const vipData = this.getVIPData(); await tx.vIP.createMany({ data: vipData }); const vips = await tx.vIP.findMany({ orderBy: { createdAt: 'asc' } }); @@ -78,13 +78,17 @@ export class SeedService { const flights = await tx.flight.findMany(); this.logger.log(`Created ${flights.length} flights`); - // 5. Create Events with dynamic times relative to NOW - const eventData = this.getEventData(vips, drivers, vehicles); - await tx.scheduleEvent.createMany({ data: eventData }); - const events = await tx.scheduleEvent.findMany(); - this.logger.log(`Created ${events.length} events`); + // 5. Create Events (two-phase: master events first, then transport legs) + const { masterEvents, transportLegs } = await this.createAllEvents(tx, vips, drivers, vehicles); + this.logger.log(`Created ${masterEvents.length} master events and ${transportLegs.length} transport legs`); - return { vips, drivers, vehicles, flights, events }; + return { + vips, + drivers, + vehicles, + flights, + events: [...masterEvents, ...transportLegs] + }; }); const elapsed = Date.now() - start; @@ -124,14 +128,19 @@ export class SeedService { } // Create events - const eventData = this.getEventData(vips, drivers, vehicles); - await this.prisma.scheduleEvent.createMany({ data: eventData }); + const { masterEvents, transportLegs } = await this.prisma.$transaction(async (tx) => { + return this.createAllEvents(tx, vips, drivers, vehicles); + }); const elapsed = Date.now() - start; return { success: true, elapsed: `${elapsed}ms`, - created: { events: eventData.length }, + created: { + events: masterEvents.length + transportLegs.length, + masterEvents: masterEvents.length, + transportLegs: transportLegs.length + }, }; } @@ -142,28 +151,28 @@ export class SeedService { private getVIPData() { return [ // OFFICE_OF_DEVELOPMENT (10 VIPs) - Corporate sponsors, foundations, major donors - { name: 'Sarah Chen', organization: 'Microsoft Corporation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Executive VP - prefers quiet vehicles. Allergic to peanuts.' }, - { name: 'Marcus Johnson', organization: 'The Coca-Cola Company', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Bringing spouse. Needs wheelchair accessible transport.' }, - { name: 'Jennifer Wu', organization: 'JPMorgan Chase Foundation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Major donor - $500K pledge. VIP treatment essential.' }, - { name: 'Roberto Gonzalez', organization: 'AT&T Inc', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'First time visitor. Interested in STEM programs.' }, - { name: 'Priya Sharma', organization: 'Google LLC', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Vegetarian meals required. Interested in technology merit badges.' }, - { name: 'David Okonkwo', organization: 'Bank of America', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: false, venueTransport: true, notes: 'Has rental car for airport. Needs venue transport only.' }, - { name: 'Maria Rodriguez', organization: 'Walmart Foundation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.SELF_DRIVING, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(120), notes: 'Driving from nearby hotel. Call when 30 min out.' }, - { name: 'Yuki Tanaka', organization: 'Honda Motor Company', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Japanese executive - interpreter may be needed.' }, - { name: 'Thomas Anderson', organization: 'Verizon Communications', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: false, notes: 'Will use personal driver after airport pickup.' }, - { name: 'Isabella Costa', organization: 'Target Corporation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, airportPickup: false, venueTransport: true, notes: 'Taking rideshare from airport. Venue transport needed.' }, + { name: 'Sarah Chen', organization: 'Microsoft Corporation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: true, venueTransport: true, notes: 'Executive VP - prefers quiet vehicles. Allergic to peanuts.' }, + { name: 'Marcus Johnson', organization: 'The Coca-Cola Company', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 2, airportPickup: true, venueTransport: true, notes: 'Bringing spouse. Needs wheelchair accessible transport.' }, + { name: 'Jennifer Wu', organization: 'JPMorgan Chase Foundation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: true, venueTransport: true, notes: 'Major donor - $500K pledge. VIP treatment essential.' }, + { name: 'Roberto Gonzalez', organization: 'AT&T Inc', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 3, airportPickup: true, venueTransport: true, notes: 'First time visitor. Interested in STEM programs. Traveling with wife and daughter.' }, + { name: 'Priya Sharma', organization: 'Google LLC', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: true, venueTransport: true, notes: 'Vegetarian meals required. Interested in technology merit badges.' }, + { name: 'David Okonkwo', organization: 'Bank of America', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 2, airportPickup: false, venueTransport: true, notes: 'Has rental car for airport. Needs venue transport only. Traveling with assistant.' }, + { name: 'Maria Rodriguez', organization: 'Walmart Foundation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.SELF_DRIVING, partySize: 1, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(120), notes: 'Driving from nearby hotel. Call when 30 min out.' }, + { name: 'Yuki Tanaka', organization: 'Honda Motor Company', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 2, airportPickup: true, venueTransport: true, notes: 'Japanese executive with interpreter.' }, + { name: 'Thomas Anderson', organization: 'Verizon Communications', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: true, venueTransport: false, notes: 'Will use personal driver after airport pickup.' }, + { name: 'Isabella Costa', organization: 'Target Corporation', department: Department.OFFICE_OF_DEVELOPMENT, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: false, venueTransport: true, notes: 'Taking rideshare from airport. Venue transport needed.' }, // ADMIN (10 VIPs) - BSA Leadership and Staff - { name: 'Roger A. Krone', organization: 'BSA National President', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'HIGHEST PRIORITY VIP. Security detail traveling with him.' }, - { name: 'Emily Richardson', organization: 'BSA Chief Scout Executive', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, airportPickup: false, venueTransport: false, expectedArrival: this.relativeTime(60), notes: 'Has assigned BSA vehicle. No transport needed.' }, - { name: 'Dr. Maya Krishnan', organization: 'BSA National Director of Program', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, airportPickup: false, venueTransport: false, expectedArrival: this.relativeTime(180), notes: 'Carpooling with regional directors.' }, - { name: "James O'Brien", organization: 'BSA Northeast Regional Director', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Traveling with 2 staff members.' }, - { name: 'Fatima Al-Rahman', organization: 'BSA Western Region Executive', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Halal meals required. Prayer room access needed.' }, - { name: 'William Zhang', organization: 'BSA Southern Region Council', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: true, notes: 'Bringing presentation equipment - need vehicle with cargo space.' }, - { name: 'Sophie Laurent', organization: 'BSA National Volunteer Training', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(240), notes: 'Training materials in personal vehicle.' }, - { name: 'Alexander Volkov', organization: 'BSA High Adventure Director', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, airportPickup: true, venueTransport: false, notes: 'Outdoor enthusiast - prefers walking when possible.' }, - { name: 'Dr. Aisha Patel', organization: 'BSA STEM & Innovation Programs', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(90), notes: 'Demo equipment for STEM showcase. Fragile items!' }, - { name: 'Henrik Larsson', organization: 'BSA International Commissioner', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(150), notes: 'Visiting from Sweden. International guest protocols apply.' }, + { name: 'Roger A. Krone', organization: 'BSA National President', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, partySize: 4, airportPickup: true, venueTransport: true, notes: 'HIGHEST PRIORITY VIP. Security detail traveling with him (total 4 people).' }, + { name: 'Emily Richardson', organization: 'BSA Chief Scout Executive', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, partySize: 1, airportPickup: false, venueTransport: false, expectedArrival: this.relativeTime(60), notes: 'Has assigned BSA vehicle. No transport needed.' }, + { name: 'Dr. Maya Krishnan', organization: 'BSA National Director of Program', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, partySize: 3, airportPickup: false, venueTransport: false, expectedArrival: this.relativeTime(180), notes: 'Carpooling with 2 regional directors.' }, + { name: "James O'Brien", organization: 'BSA Northeast Regional Director', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, partySize: 3, airportPickup: true, venueTransport: true, notes: 'Traveling with 2 staff members.' }, + { name: 'Fatima Al-Rahman', organization: 'BSA Western Region Executive', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: true, venueTransport: true, notes: 'Halal meals required. Prayer room access needed.' }, + { name: 'William Zhang', organization: 'BSA Southern Region Council', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, partySize: 2, airportPickup: true, venueTransport: true, notes: 'Bringing presentation equipment - need vehicle with cargo space. Traveling with AV tech.' }, + { name: 'Sophie Laurent', organization: 'BSA National Volunteer Training', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, partySize: 1, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(240), notes: 'Training materials in personal vehicle.' }, + { name: 'Alexander Volkov', organization: 'BSA High Adventure Director', department: Department.ADMIN, arrivalMode: ArrivalMode.FLIGHT, partySize: 1, airportPickup: true, venueTransport: false, notes: 'Outdoor enthusiast - prefers walking when possible.' }, + { name: 'Dr. Aisha Patel', organization: 'BSA STEM & Innovation Programs', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, partySize: 2, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(90), notes: 'Demo equipment for STEM showcase. Fragile items! Traveling with lab assistant.' }, + { name: 'Henrik Larsson', organization: 'BSA International Commissioner', department: Department.ADMIN, arrivalMode: ArrivalMode.SELF_DRIVING, partySize: 1, airportPickup: false, venueTransport: true, expectedArrival: this.relativeTime(150), notes: 'Visiting from Sweden. International guest protocols apply.' }, ]; } @@ -184,7 +193,7 @@ export class SeedService { { name: 'Lisa Martinez', phone: '555-0102', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: true, shiftStartTime: shiftStart, shiftEndTime: shiftEnd }, { name: 'David Kim', phone: '555-0103', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: true, shiftStartTime: shiftStart, shiftEndTime: shiftEnd }, { name: 'Amanda Washington', phone: '555-0104', department: Department.ADMIN, isAvailable: true, shiftStartTime: shiftStart, shiftEndTime: shiftEnd }, - { name: 'Carlos Hernandez', phone: '555-0105', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: false, shiftStartTime: lateShiftStart, shiftEndTime: lateShiftEnd }, // Off duty until 2pm + { name: 'Carlos Hernandez', phone: '555-0105', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: false, shiftStartTime: lateShiftStart, shiftEndTime: lateShiftEnd }, { name: 'Jessica Lee', phone: '555-0106', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: true, shiftStartTime: shiftStart, shiftEndTime: shiftEnd }, { name: 'Brandon Jackson', phone: '555-0107', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: true, shiftStartTime: lateShiftStart, shiftEndTime: lateShiftEnd }, { name: 'Nicole Brown', phone: '555-0108', department: Department.ADMIN, isAvailable: true, shiftStartTime: shiftStart, shiftEndTime: shiftEnd }, @@ -193,16 +202,14 @@ export class SeedService { private getVehicleData() { return [ - { name: 'Blue Van', type: VehicleType.VAN, licensePlate: 'VAN-001', seatCapacity: 12, status: VehicleStatus.AVAILABLE, notes: 'Primary transport van with wheelchair accessibility' }, - { name: 'Suburban #1', type: VehicleType.SUV, licensePlate: 'SUV-101', seatCapacity: 7, status: VehicleStatus.AVAILABLE, notes: 'Leather interior, ideal for VIP comfort' }, - { name: 'Golf Cart Alpha', type: VehicleType.GOLF_CART, licensePlate: 'GC-A', seatCapacity: 6, status: VehicleStatus.AVAILABLE, notes: 'Quick campus transport, good for short distances' }, - { name: 'Red Van', type: VehicleType.VAN, licensePlate: 'VAN-002', seatCapacity: 8, status: VehicleStatus.AVAILABLE, notes: 'Standard transport van' }, - { name: 'Scout Bus', type: VehicleType.BUS, licensePlate: 'BUS-001', seatCapacity: 25, status: VehicleStatus.AVAILABLE, notes: 'Large group transport, AC equipped' }, - { name: 'Suburban #2', type: VehicleType.SUV, licensePlate: 'SUV-102', seatCapacity: 7, status: VehicleStatus.AVAILABLE, notes: 'Backup VIP transport' }, - { name: 'Golf Cart Bravo', type: VehicleType.GOLF_CART, licensePlate: 'GC-B', seatCapacity: 4, status: VehicleStatus.AVAILABLE, notes: 'Quick on-site transport' }, - { name: 'Equipment Truck', type: VehicleType.TRUCK, licensePlate: 'TRK-001', seatCapacity: 3, status: VehicleStatus.MAINTENANCE, notes: 'For equipment and supply runs - currently in maintenance' }, - { name: 'Executive Sedan', type: VehicleType.SEDAN, licensePlate: 'SED-001', seatCapacity: 4, status: VehicleStatus.AVAILABLE, notes: 'Premium sedan for executive VIPs' }, - { name: 'Golf Cart Charlie', type: VehicleType.GOLF_CART, licensePlate: 'GC-C', seatCapacity: 4, status: VehicleStatus.AVAILABLE, notes: 'Backup golf cart' }, + { name: 'Black Suburban #1', type: VehicleType.SUV, licensePlate: 'SUV-101', seatCapacity: 6, status: VehicleStatus.AVAILABLE, notes: 'Premium VIP transport with leather interior' }, + { name: 'Black Suburban #2', type: VehicleType.SUV, licensePlate: 'SUV-102', seatCapacity: 6, status: VehicleStatus.AVAILABLE, notes: 'Backup VIP transport with climate control' }, + { name: 'White 15-Pax Van #1', type: VehicleType.VAN, licensePlate: 'VAN-151', seatCapacity: 14, status: VehicleStatus.AVAILABLE, notes: 'Large group transport with wheelchair accessibility' }, + { name: 'White 15-Pax Van #2', type: VehicleType.VAN, licensePlate: 'VAN-152', seatCapacity: 14, status: VehicleStatus.AVAILABLE, notes: 'Large group transport with AV equipment' }, + { name: 'Golf Cart A', type: VehicleType.GOLF_CART, licensePlate: 'GC-A', seatCapacity: 4, status: VehicleStatus.AVAILABLE, notes: 'Quick campus transport for short distances' }, + { name: 'Golf Cart B', type: VehicleType.GOLF_CART, licensePlate: 'GC-B', seatCapacity: 4, status: VehicleStatus.AVAILABLE, notes: 'On-site shuttle for VIP areas' }, + { name: 'Charter Bus', type: VehicleType.BUS, licensePlate: 'BUS-001', seatCapacity: 25, status: VehicleStatus.AVAILABLE, notes: 'Full-size coach for large group events' }, + { name: 'Executive Sedan', type: VehicleType.SEDAN, licensePlate: 'SED-001', seatCapacity: 4, status: VehicleStatus.AVAILABLE, notes: 'Luxury sedan for single VIP executive transport' }, ]; } @@ -210,7 +217,7 @@ export class SeedService { const flights: any[] = []; const airlines = ['AA', 'UA', 'DL', 'SW', 'AS', 'JB']; const origins = ['JFK', 'LAX', 'ORD', 'DFW', 'ATL', 'SFO', 'SEA', 'BOS', 'DEN', 'MIA']; - const destination = 'SLC'; // Assuming Salt Lake City for the Jamboree + const destination = 'SLC'; vips.forEach((vip, index) => { const airline = airlines[index % airlines.length]; @@ -218,11 +225,10 @@ export class SeedService { const origin = origins[index % origins.length]; // Arrival flight - times relative to now - const arrivalOffset = (index % 8) * 30 - 60; // -60 to +150 minutes from now + const arrivalOffset = (index % 8) * 30 - 60; const scheduledArrival = this.relativeTime(arrivalOffset); - const scheduledDeparture = new Date(scheduledArrival.getTime() - 3 * 60 * 60 * 1000); // 3 hours before + const scheduledDeparture = new Date(scheduledArrival.getTime() - 3 * 60 * 60 * 1000); - // Some flights are delayed, some landed, some on time let status = 'scheduled'; let actualArrival = null; if (arrivalOffset < -30) { @@ -267,62 +273,246 @@ export class SeedService { return flights; } - private getEventData(vips: any[], drivers: any[], vehicles: any[]) { - const events: any[] = []; - const now = new Date(); - - // Track vehicle assignments to avoid conflicts - // Map of vehicleId -> array of { start: Date, end: Date } + /** + * Create all events (master events + transport legs) in correct order + * Returns both arrays for counting + */ + private async createAllEvents(tx: any, vips: any[], drivers: any[], vehicles: any[]) { + // Track schedules for conflict detection const vehicleSchedule: Map> = new Map(); const driverSchedule: Map> = new Map(); - // Initialize schedules vehicles.forEach(v => vehicleSchedule.set(v.id, [])); drivers.forEach(d => driverSchedule.set(d.id, [])); - // Check if a time slot conflicts with existing assignments + // ============================================================ + // DAY 1 MASTER EVENTS (relative to NOW) + // ============================================================ + + // 1. Welcome Registration (-3hr to -2hr) - COMPLETED + const welcomeReg = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Welcome Registration & Check-In', + type: EventType.EVENT, + status: EventStatus.COMPLETED, + location: 'Main Registration Pavilion', + startTime: this.relativeTime(-180), + endTime: this.relativeTime(-120), + actualStartTime: this.relativeTime(-180), + actualEndTime: this.relativeTime(-120), + description: 'All VIPs check in and receive welcome packets, name badges, and event schedules', + } + }); + + // 2. Opening Ceremony (-1hr to +0.5hr) - IN_PROGRESS + const openingCeremony = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Grand Opening Ceremony', + type: EventType.EVENT, + status: EventStatus.IN_PROGRESS, + location: 'Main Arena - VIP Section', + startTime: this.relativeTime(-60), + endTime: this.relativeTime(30), + actualStartTime: this.relativeTime(-60), + description: 'National anthem, welcome speeches by BSA leadership, flag ceremony. ALL VIPs in attendance.', + } + }); + + // 3. VIP Luncheon (+1hr to +2.5hr) - SCHEDULED + const vipLuncheon = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'VIP Welcome Luncheon', + type: EventType.MEAL, + status: EventStatus.SCHEDULED, + location: 'VIP Dining Pavilion', + startTime: this.relativeTime(60), + endTime: this.relativeTime(150), + description: 'Catered lunch with networking. Menu includes vegetarian/halal options.', + notes: 'Dietary restrictions: Priya Sharma (vegetarian), Fatima Al-Rahman (halal), Sarah Chen (no peanuts)', + } + }); + + // 4. Keynote Address (+3hr to +4.5hr) - SCHEDULED (16 VIPs, not self-driving Gov officials) + const keynoteVips = vips.filter(v => + v.arrivalMode === ArrivalMode.FLIGHT || + !v.organization.includes('International Commissioner') + ).slice(0, 16); + const keynoteAddress = await tx.scheduleEvent.create({ + data: { + vipIds: keynoteVips.map(v => v.id), + title: 'Keynote: Future of Youth Leadership', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Conference Center - Grand Hall', + startTime: this.relativeTime(180), + endTime: this.relativeTime(270), + description: 'Keynote speaker addresses VIPs on modern youth development and leadership training', + } + }); + + // 5. Donor Strategy Meeting (+5hr to +6hr) - SCHEDULED (select 6 VIPs) + const donorVips = vips.filter(v => v.department === Department.OFFICE_OF_DEVELOPMENT).slice(0, 6); + const donorMeeting = await tx.scheduleEvent.create({ + data: { + vipIds: donorVips.map(v => v.id), + title: 'Private Donor Strategy Session', + type: EventType.MEETING, + status: EventStatus.SCHEDULED, + location: 'Executive Conference Room', + startTime: this.relativeTime(300), + endTime: this.relativeTime(360), + description: 'Closed-door meeting with major donors to discuss funding initiatives', + notes: 'CONFIDENTIAL - No photography', + } + }); + + // 6. Campfire Night (+9hr to +11hr) - SCHEDULED (all VIPs) + const campfireNight = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Traditional Scout Campfire Gathering', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Outdoor Amphitheater', + startTime: this.relativeTime(540), + endTime: this.relativeTime(660), + description: 'Evening campfire with songs, skits, and scout traditions. Casual attire.', + } + }); + + // ============================================================ + // DAY 2 MASTER EVENTS (+24hr base) + // ============================================================ + + // 1. Morning Fitness Hike (+18hr = 6am next day) - 10 VIPs + const hikeVips = vips.slice(0, 10); + const morningHike = await tx.scheduleEvent.create({ + data: { + vipIds: hikeVips.map(v => v.id), + title: 'Sunrise Fitness Hike', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Nature Trail - Starting at Lodge', + startTime: this.relativeTime(1080), // +18hr + endTime: this.relativeTime(1200), // +20hr + description: 'Optional guided morning hike through scenic trails. Moderate difficulty.', + notes: 'Athletic wear recommended. Water provided.', + } + }); + + // 2. STEM Showcase (+20hr) - 15 VIPs + const stemVips = [...vips.filter(v => v.department === Department.OFFICE_OF_DEVELOPMENT), ...vips.filter(v => v.organization.includes('STEM'))]; + const stemShowcase = await tx.scheduleEvent.create({ + data: { + vipIds: stemVips.slice(0, 15).map(v => v.id), + title: 'STEM & Innovation Showcase', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Technology Center', + startTime: this.relativeTime(1200), // +20hr + endTime: this.relativeTime(1350), // +22.5hr + description: 'Interactive demonstrations of robotics, coding, and engineering merit badge projects', + notes: 'Dr. Aisha Patel will lead special demos', + } + }); + + // 3. Eagle Scout Ceremony (+22.5hr) - all 20 VIPs + const eagleCeremony = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Eagle Scout Recognition Ceremony', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Main Arena', + startTime: this.relativeTime(1350), // +22.5hr + endTime: this.relativeTime(1500), // +25hr + description: 'Prestigious ceremony honoring new Eagle Scouts. Formal attire required.', + } + }); + + // 4. Gala Dinner (+27hr) - all 20 VIPs + const galaDinner = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Black-Tie Gala Dinner', + type: EventType.MEAL, + status: EventStatus.SCHEDULED, + location: 'Grand Ballroom', + startTime: this.relativeTime(1620), // +27hr + endTime: this.relativeTime(1800), // +30hr + description: 'Formal dinner with awards presentation and entertainment. Black-tie required.', + notes: 'Seating chart will be distributed at check-in', + } + }); + + // ============================================================ + // DAY 3 MASTER EVENTS (+48hr base) + // ============================================================ + + // 1. Service Project Tour (+42hr) - 12 VIPs + const serviceVips = vips.slice(0, 12); + const serviceTour = await tx.scheduleEvent.create({ + data: { + vipIds: serviceVips.map(v => v.id), + title: 'Community Service Project Tour', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Community Center - Multiple Sites', + startTime: this.relativeTime(2520), // +42hr + endTime: this.relativeTime(2640), // +44hr + description: 'Tour of ongoing service projects including park restoration and food drive', + } + }); + + // 2. Farewell Brunch (+45hr) - all 20 VIPs + const farewellBrunch = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Farewell Brunch & Thank You', + type: EventType.MEAL, + status: EventStatus.SCHEDULED, + location: 'VIP Dining Pavilion', + startTime: this.relativeTime(2700), // +45hr + endTime: this.relativeTime(2820), // +47hr + description: 'Casual farewell brunch with closing remarks. Business casual attire.', + } + }); + + // 3. Closing Ceremony (+47hr) - all 20 VIPs + const closingCeremony = await tx.scheduleEvent.create({ + data: { + vipIds: vips.map(v => v.id), + title: 'Official Closing Ceremony', + type: EventType.EVENT, + status: EventStatus.SCHEDULED, + location: 'Main Arena', + startTime: this.relativeTime(2820), // +47hr + endTime: this.relativeTime(2940), // +49hr + description: 'Final ceremony with flag retirement and closing remarks from BSA National President', + } + }); + + const masterEvents = [ + welcomeReg, openingCeremony, vipLuncheon, keynoteAddress, donorMeeting, campfireNight, + morningHike, stemShowcase, eagleCeremony, galaDinner, + serviceTour, farewellBrunch, closingCeremony + ]; + + // ============================================================ + // TRANSPORT LEGS LINKED TO MASTER EVENTS + // ============================================================ + + const transportLegs: any[] = []; + + // Helper: find available driver/vehicle considering conflicts const hasConflict = (schedule: Array<{ start: Date; end: Date }>, start: Date, end: Date): boolean => { - return schedule.some(slot => - (start < slot.end && end > slot.start) // Overlapping - ); + return schedule.some(slot => (start < slot.end && end > slot.start)); }; - // Find an available vehicle for a time slot - const findAvailableVehicle = (start: Date, end: Date, preferredIndex: number): any | null => { - if (vehicles.length === 0) return null; - - // Try preferred vehicle first - const preferred = vehicles[preferredIndex % vehicles.length]; - const preferredSchedule = vehicleSchedule.get(preferred.id) || []; - if (!hasConflict(preferredSchedule, start, end)) { - preferredSchedule.push({ start, end }); - return preferred; - } - - // Try other vehicles - for (const vehicle of vehicles) { - const schedule = vehicleSchedule.get(vehicle.id) || []; - if (!hasConflict(schedule, start, end)) { - schedule.push({ start, end }); - return vehicle; - } - } - return null; // No available vehicle - }; - - // Find an available driver for a time slot - const findAvailableDriver = (start: Date, end: Date, preferredIndex: number): any | null => { - if (drivers.length === 0) return null; - - // Try preferred driver first - const preferred = drivers[preferredIndex % drivers.length]; - const preferredSchedule = driverSchedule.get(preferred.id) || []; - if (!hasConflict(preferredSchedule, start, end)) { - preferredSchedule.push({ start, end }); - return preferred; - } - - // Try other drivers + const findAvailableDriver = (start: Date, end: Date): any | null => { for (const driver of drivers) { const schedule = driverSchedule.get(driver.id) || []; if (!hasConflict(schedule, start, end)) { @@ -330,286 +520,559 @@ export class SeedService { return driver; } } - return null; // No available driver + return null; }; - vips.forEach((vip, vipIndex) => { - // ============================================================ - // CREATE VARIED EVENTS RELATIVE TO NOW - // ============================================================ - - // Event pattern based on VIP index to create variety: - // - Some VIPs have events IN_PROGRESS - // - Some have events starting VERY soon (5-15 min) - // - Some have events starting soon (30-60 min) - // - Some have just-completed events - // - All have future events throughout the day - - const eventPattern = vipIndex % 5; - - switch (eventPattern) { - case 0: { // IN_PROGRESS airport pickup - const start = this.relativeTime(-25); - const end = this.relativeTime(15); - const driver = findAvailableDriver(start, end, vipIndex); - const vehicle = findAvailableVehicle(start, end, vipIndex); - events.push({ - vipIds: [vip.id], - driverId: driver?.id, - vehicleId: vehicle?.id, - title: `Airport Pickup - ${vip.name}`, - type: EventType.TRANSPORT, - status: EventStatus.IN_PROGRESS, - pickupLocation: 'Airport - Terminal B', - dropoffLocation: 'Main Gate Registration', - startTime: start, - endTime: end, - actualStartTime: this.relativeTime(-23), - description: `ACTIVE: Driver en route with ${vip.name} from airport`, - notes: 'VIP collected from arrivals. ETA 15 minutes.', - }); - break; + const findAvailableVehicle = (start: Date, end: Date, minCapacity: number): any | null => { + for (const vehicle of vehicles) { + if (vehicle.seatCapacity < minCapacity) continue; + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); + return vehicle; } - - case 1: { // COMPLETED event - const start = this.relativeTime(-90); - const end = this.relativeTime(-45); - const driver = findAvailableDriver(start, end, vipIndex); - const vehicle = findAvailableVehicle(start, end, vipIndex); - events.push({ - vipIds: [vip.id], - driverId: driver?.id, - vehicleId: vehicle?.id, - title: `Airport Pickup - ${vip.name}`, - type: EventType.TRANSPORT, - status: EventStatus.COMPLETED, - pickupLocation: 'Airport - Terminal A', - dropoffLocation: 'VIP Lodge', - startTime: start, - endTime: end, - actualStartTime: this.relativeTime(-88), - actualEndTime: this.relativeTime(-42), - description: `Completed pickup for ${vip.name}`, - }); - break; - } - - case 2: { // Starting in 5-10 minutes (URGENT) - const start = this.relativeTime(7); - const end = this.relativeTime(22); - const driver = findAvailableDriver(start, end, vipIndex); - const vehicle = findAvailableVehicle(start, end, vipIndex); - events.push({ - vipIds: [vip.id], - driverId: driver?.id, - vehicleId: vehicle?.id, - title: `URGENT: Transport to Opening Ceremony - ${vip.name}`, - type: EventType.TRANSPORT, - status: EventStatus.SCHEDULED, - pickupLocation: 'VIP Lodge', - dropoffLocation: 'Main Arena - VIP Entrance', - startTime: start, - endTime: end, - description: `Pick up ${vip.name} for Opening Ceremony - STARTS SOON!`, - notes: 'Driver should be at pickup location NOW', - }); - break; - } - - case 3: { // Starting in 30-45 min - const start = this.relativeTime(35); - const end = this.relativeTime(50); - const driver = findAvailableDriver(start, end, vipIndex); - const vehicle = findAvailableVehicle(start, end, vipIndex); - events.push({ - vipIds: [vip.id], - driverId: driver?.id, - vehicleId: vehicle?.id, - title: `VIP Lodge Transfer - ${vip.name}`, - type: EventType.TRANSPORT, - status: EventStatus.SCHEDULED, - pickupLocation: 'Registration Tent', - dropoffLocation: 'VIP Lodge - Building A', - startTime: start, - endTime: end, - description: `Transfer ${vip.name} to VIP accommodation after registration`, - }); - break; - } - - case 4: // In-progress MEETING (no driver/vehicle needed) - events.push({ - vipIds: [vip.id], - title: `Donor Briefing Meeting`, - type: EventType.MEETING, - status: EventStatus.IN_PROGRESS, - location: 'Conference Center - Room 101', - startTime: this.relativeTime(-20), - endTime: this.relativeTime(25), - actualStartTime: this.relativeTime(-18), - description: `${vip.name} in donor briefing with development team`, - }); - break; } + return null; + }; - // ============================================================ - // ADD STANDARD DAY EVENTS FOR ALL VIPS - // ============================================================ + // ============================================================ + // DAY 1 TRANSPORT LEGS + // ============================================================ - // Upcoming meal (1-2 hours out) - no driver/vehicle needed - events.push({ - vipIds: [vip.id], - title: vipIndex % 2 === 0 ? 'VIP Luncheon' : 'VIP Breakfast', - type: EventType.MEAL, - status: EventStatus.SCHEDULED, - location: 'VIP Dining Pavilion', - startTime: this.relativeTime(60 + (vipIndex % 4) * 15), - endTime: this.relativeTime(120 + (vipIndex % 4) * 15), - description: `Catered meal for ${vip.name} with other VIP guests`, - }); + // Airport pickups for flight-arriving VIPs (staggered arrivals, SOME COMPLETED/IN_PROGRESS) + const flightVips = vips.filter(v => v.arrivalMode === ArrivalMode.FLIGHT && v.airportPickup); - // Transport to main event (2-3 hours out) - { - const start = this.relativeTime(150 + vipIndex * 5); - const end = this.relativeTime(165 + vipIndex * 5); - const driver = findAvailableDriver(start, end, vipIndex + 3); - const vehicle = findAvailableVehicle(start, end, vipIndex + 2); - events.push({ + // Group 1: Roger Krone (party of 4) - HIGH PRIORITY - COMPLETED + { + const vip = flightVips.find(v => v.name === 'Roger A. Krone'); + if (vip) { + const start = this.relativeTime(-150); + const end = this.relativeTime(-90); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, vip.partySize); + transportLegs.push({ vipIds: [vip.id], driverId: driver?.id, vehicleId: vehicle?.id, - title: `Transport to Scout Exhibition`, + masterEventId: welcomeReg.id, + title: `Airport Pickup - ${vip.name} (PRIORITY)`, type: EventType.TRANSPORT, - status: EventStatus.SCHEDULED, - pickupLocation: 'VIP Lodge', - dropoffLocation: 'Exhibition Grounds', + status: EventStatus.COMPLETED, + pickupLocation: 'SLC Airport - Private Terminal', + dropoffLocation: 'Main Registration Pavilion', startTime: start, endTime: end, - description: `Transport ${vip.name} to Scout Exhibition area`, + actualStartTime: this.relativeTime(-150), + actualEndTime: this.relativeTime(-90), + description: `Completed VIP airport pickup for National President + security detail (${vip.partySize} people)`, }); } + } - // Main event (3-4 hours out) - no driver/vehicle needed - events.push({ - vipIds: [vip.id], - title: 'Scout Skills Exhibition', - type: EventType.EVENT, - status: EventStatus.SCHEDULED, - location: 'Exhibition Grounds - Zone A', - startTime: this.relativeTime(180 + vipIndex * 3), - endTime: this.relativeTime(270 + vipIndex * 3), - description: `${vip.name} tours Scout exhibitions and demonstrations`, - }); - - // Evening dinner (5-6 hours out) - no driver/vehicle needed - events.push({ - vipIds: [vip.id], - title: 'Gala Dinner', - type: EventType.MEAL, - status: EventStatus.SCHEDULED, - location: 'Grand Ballroom', - startTime: this.relativeTime(360), - endTime: this.relativeTime(480), - description: `Black-tie dinner event with ${vip.name} and other distinguished guests`, - }); - - // Next day departure (tomorrow morning) - if (vip.arrivalMode === 'FLIGHT') { - const start = this.relativeTime(60 * 24 + 120 + vipIndex * 20); - const end = this.relativeTime(60 * 24 + 165 + vipIndex * 20); - const driver = findAvailableDriver(start, end, vipIndex + 1); - const vehicle = findAvailableVehicle(start, end, vipIndex + 1); - events.push({ - vipIds: [vip.id], + // Group 2: Marcus + spouse + James O'Brien + staff - Suburban (party sizes 2+3=5) - IN_PROGRESS + { + const marcus = flightVips.find(v => v.name === 'Marcus Johnson'); + const james = flightVips.find(v => v.name === "James O'Brien"); + if (marcus && james) { + const totalParty = marcus.partySize + james.partySize; + const start = this.relativeTime(-45); + const end = this.relativeTime(15); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty); + transportLegs.push({ + vipIds: [marcus.id, james.id], driverId: driver?.id, vehicleId: vehicle?.id, - title: `Airport Departure - ${vip.name}`, + masterEventId: openingCeremony.id, + title: `Multi-VIP Airport Pickup (${totalParty} passengers)`, type: EventType.TRANSPORT, - status: EventStatus.SCHEDULED, - pickupLocation: 'VIP Lodge', - dropoffLocation: 'Airport - Departures', + status: EventStatus.IN_PROGRESS, + pickupLocation: 'SLC Airport - Terminal 1', + dropoffLocation: 'Main Arena - VIP Entrance', startTime: start, endTime: end, - description: `Transport ${vip.name} to airport for departure flight`, - notes: 'Confirm flight status before pickup', + actualStartTime: this.relativeTime(-43), + description: `ACTIVE: Picking up ${totalParty} people (Marcus Johnson party of ${marcus.partySize} + James O'Brien party of ${james.partySize})`, + notes: 'Wheelchair accessible vehicle required for Marcus', }); } + } + + // Group 3: Roberto Gonzalez (party of 3) + William Zhang (party of 2) = 5 total - SCHEDULED + { + const roberto = flightVips.find(v => v.name === 'Roberto Gonzalez'); + const william = flightVips.find(v => v.name === 'William Zhang'); + if (roberto && william) { + const totalParty = roberto.partySize + william.partySize; + const start = this.relativeTime(-15); + const end = this.relativeTime(45); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty); + transportLegs.push({ + vipIds: [roberto.id, william.id], + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: openingCeremony.id, + title: `Airport Pickup - Group Transport (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'SLC Airport - Terminal 2', + dropoffLocation: 'Main Arena - VIP Entrance', + startTime: start, + endTime: end, + description: `Group pickup: Roberto Gonzalez family (${roberto.partySize}) + William Zhang + AV tech (${william.partySize})`, + notes: 'Need cargo space for William Zhang\'s presentation equipment', + }); + } + } + + // Remaining individual/pair airport pickups (spread across remaining SUVs/sedan) + const remainingFlightVips = flightVips.filter(v => + !['Roger A. Krone', 'Marcus Johnson', "James O'Brien", 'Roberto Gonzalez', 'William Zhang'].includes(v.name) + ); + + remainingFlightVips.forEach((vip, idx) => { + const offset = idx * 25 - 30; + const start = this.relativeTime(offset); + const end = this.relativeTime(offset + 60); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, vip.partySize); + + let status: EventStatus = EventStatus.SCHEDULED; + let actualStart: Date | null = null; + if (offset < -20) { + status = EventStatus.COMPLETED; + actualStart = this.relativeTime(offset + 2); + } else if (offset < 0) { + status = EventStatus.IN_PROGRESS; + actualStart = this.relativeTime(offset); + } + + transportLegs.push({ + vipIds: [vip.id], + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: offset < 30 ? openingCeremony.id : vipLuncheon.id, + title: `Airport Pickup - ${vip.name}${vip.partySize > 1 ? ` (party of ${vip.partySize})` : ''}`, + type: EventType.TRANSPORT, + status, + pickupLocation: 'SLC Airport - Terminal 1', + dropoffLocation: offset < 30 ? 'Main Arena - VIP Entrance' : 'VIP Dining Pavilion', + startTime: start, + endTime: end, + actualStartTime: actualStart, + description: `Airport pickup for ${vip.name}${vip.partySize > 1 ? ` + ${vip.partySize - 1} companions` : ''}`, + }); }); - // ============================================================ - // ADD MULTI-VIP GROUP EVENTS - // ============================================================ - - if (vips.length >= 4) { - // Group transport with multiple VIPs - { - const start = this.relativeTime(45); - const end = this.relativeTime(60); - const driver = findAvailableDriver(start, end, 0); - // Find a large vehicle (bus or van with capacity >= 8) - const largeVehicle = vehicles.find(v => - v.seatCapacity >= 8 && !hasConflict(vehicleSchedule.get(v.id) || [], start, end) - ); - if (largeVehicle) { - vehicleSchedule.get(largeVehicle.id)?.push({ start, end }); + // Transport to VIP Luncheon - Use charter bus for most VIPs (party sizes sum check) + { + const lunchTransportVips = vips.slice(0, 12); // First 12 VIPs via bus + const totalPartySize = lunchTransportVips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(45); + const end = this.relativeTime(60); + const driver = findAvailableDriver(start, end); + const vehicle = vehicles.find(v => v.name === 'Charter Bus'); + if (vehicle) { + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); } - events.push({ - vipIds: [vips[0].id, vips[1].id, vips[2].id], - driverId: driver?.id, - vehicleId: largeVehicle?.id, - title: 'Group Transport - Leadership Briefing', - type: EventType.TRANSPORT, - status: EventStatus.SCHEDULED, - pickupLocation: 'VIP Lodge - Main Entrance', - dropoffLocation: 'National HQ Building', - startTime: start, - endTime: end, - description: 'Multi-VIP transport for leadership briefing session', - notes: 'IMPORTANT: Picking up 3 VIPs - use large vehicle', - }); } - // Another group event - if (vips.length >= 5) { - const start = this.relativeTime(90); - const end = this.relativeTime(110); - const driver = findAvailableDriver(start, end, 1); - const vehicle = findAvailableVehicle(start, end, 1); - events.push({ - vipIds: [vips[3].id, vips[4].id], - driverId: driver?.id, - vehicleId: vehicle?.id, - title: 'Group Transport - Media Tour', - type: EventType.TRANSPORT, - status: EventStatus.SCHEDULED, - pickupLocation: 'Media Center', - dropoffLocation: 'Historical Site', - startTime: start, - endTime: end, - description: 'VIP media tour with photo opportunities', - }); - } - } - - // ============================================================ - // ADD SOME CANCELLED EVENTS FOR REALISM - // ============================================================ - - if (vips.length >= 6) { - events.push({ - vipIds: [vips[5].id], - title: 'Private Meeting - CANCELLED', - type: EventType.MEETING, - status: EventStatus.CANCELLED, - location: 'Conference Room B', - startTime: this.relativeTime(200), - endTime: this.relativeTime(260), - description: 'Meeting cancelled due to schedule conflict', - notes: 'VIP requested reschedule for tomorrow', + transportLegs.push({ + vipIds: lunchTransportVips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: vipLuncheon.id, + title: `Group Transport to VIP Luncheon (${totalPartySize} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'Main Arena - VIP Exit', + dropoffLocation: 'VIP Dining Pavilion', + startTime: start, + endTime: end, + description: `Charter bus for ${lunchTransportVips.length} VIPs + companions (total ${totalPartySize} people)`, + notes: `Capacity check: ${totalPartySize}/${vehicle?.seatCapacity} seats used`, }); } - return events; + // Transport to Keynote - Two vans for 16 VIPs + { + const keynoteTransportVips = keynoteVips.slice(0, 8); + const totalParty1 = keynoteTransportVips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(165); + const end = this.relativeTime(180); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty1); + + transportLegs.push({ + vipIds: keynoteTransportVips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: keynoteAddress.id, + title: `Transport to Keynote Address - Van 1 (${totalParty1} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Dining Pavilion', + dropoffLocation: 'Conference Center - Grand Hall', + startTime: start, + endTime: end, + description: `First van for keynote event (${totalParty1} people)`, + }); + + // Second van + const keynoteTransportVips2 = keynoteVips.slice(8, 16); + const totalParty2 = keynoteTransportVips2.reduce((sum, v) => sum + v.partySize, 0); + const driver2 = findAvailableDriver(start, end); + const vehicle2 = findAvailableVehicle(start, end, totalParty2); + + transportLegs.push({ + vipIds: keynoteTransportVips2.map(v => v.id), + driverId: driver2?.id, + vehicleId: vehicle2?.id, + masterEventId: keynoteAddress.id, + title: `Transport to Keynote Address - Van 2 (${totalParty2} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Dining Pavilion', + dropoffLocation: 'Conference Center - Grand Hall', + startTime: start, + endTime: end, + description: `Second van for keynote event (${totalParty2} people)`, + }); + } + + // Transport to Donor Meeting - Suburban for 6 VIPs + { + const donorTransportVips = donorVips; + const totalParty = donorTransportVips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(285); + const end = this.relativeTime(300); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty); + + transportLegs.push({ + vipIds: donorTransportVips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: donorMeeting.id, + title: `Executive Transport to Donor Meeting (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'Conference Center', + dropoffLocation: 'Executive Conference Room', + startTime: start, + endTime: end, + description: `VIP transport for private donor strategy session (${totalParty} people)`, + notes: 'CONFIDENTIAL - Use discrete route', + }); + } + + // Transport to Campfire Night - Charter bus for all 20 VIPs + { + const totalParty = vips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(525); + const end = this.relativeTime(540); + const driver = findAvailableDriver(start, end); + const vehicle = vehicles.find(v => v.name === 'Charter Bus'); + if (vehicle) { + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); + } + } + + transportLegs.push({ + vipIds: vips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: campfireNight.id, + title: `Group Transport to Campfire Night (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge - Main Entrance', + dropoffLocation: 'Outdoor Amphitheater', + startTime: start, + endTime: end, + description: `Charter bus for all VIPs to evening campfire (${totalParty} total people including companions)`, + notes: `Capacity: ${totalParty}/${vehicle?.seatCapacity} seats`, + }); + } + + // ============================================================ + // DAY 2 TRANSPORT LEGS + // ============================================================ + + // Morning Hike Transport - Van for 10 VIPs (early morning) + { + const totalParty = hikeVips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(1065); + const end = this.relativeTime(1080); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty); + + transportLegs.push({ + vipIds: hikeVips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: morningHike.id, + title: `Early Morning Transport to Fitness Hike (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Nature Trail - Trailhead', + startTime: start, + endTime: end, + description: `Van transport for sunrise hike participants (${totalParty} people)`, + }); + } + + // STEM Showcase Transport - Use two vans for 15 VIPs + { + const stemGroup1 = stemVips.slice(0, 8); + const totalParty1 = stemGroup1.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(1185); + const end = this.relativeTime(1200); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty1); + + transportLegs.push({ + vipIds: stemGroup1.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: stemShowcase.id, + title: `STEM Showcase Transport - Van 1 (${totalParty1} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Technology Center', + startTime: start, + endTime: end, + description: `Transport to STEM showcase (${totalParty1} people)`, + }); + + const stemGroup2 = stemVips.slice(8, 15); + const totalParty2 = stemGroup2.reduce((sum, v) => sum + v.partySize, 0); + const driver2 = findAvailableDriver(start, end); + const vehicle2 = findAvailableVehicle(start, end, totalParty2); + + transportLegs.push({ + vipIds: stemGroup2.map(v => v.id), + driverId: driver2?.id, + vehicleId: vehicle2?.id, + masterEventId: stemShowcase.id, + title: `STEM Showcase Transport - Van 2 (${totalParty2} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Technology Center', + startTime: start, + endTime: end, + description: `Transport to STEM showcase (${totalParty2} people)`, + }); + } + + // Eagle Ceremony Transport - Charter bus for all + { + const totalParty = vips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(1335); + const end = this.relativeTime(1350); + const driver = findAvailableDriver(start, end); + const vehicle = vehicles.find(v => v.name === 'Charter Bus'); + if (vehicle) { + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); + } + } + + transportLegs.push({ + vipIds: vips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: eagleCeremony.id, + title: `Formal Transport to Eagle Scout Ceremony (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Main Arena - VIP Entrance', + startTime: start, + endTime: end, + description: `Charter bus for prestigious Eagle ceremony (${totalParty} total people)`, + notes: 'Formal attire - allow extra loading time', + }); + } + + // Gala Dinner Transport - Charter bus + { + const totalParty = vips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(1605); + const end = this.relativeTime(1620); + const driver = findAvailableDriver(start, end); + const vehicle = vehicles.find(v => v.name === 'Charter Bus'); + if (vehicle) { + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); + } + } + + transportLegs.push({ + vipIds: vips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: galaDinner.id, + title: `Black-Tie Gala Transport (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Grand Ballroom - Red Carpet Entrance', + startTime: start, + endTime: end, + description: `Charter bus for black-tie gala (${totalParty} people)`, + notes: 'BLACK-TIE EVENT - Coordinate arrival timing for red carpet photos', + }); + } + + // ============================================================ + // DAY 3 TRANSPORT LEGS + // ============================================================ + + // Service Project Tour - Van for 12 VIPs + { + const totalParty = serviceVips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(2505); + const end = this.relativeTime(2520); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, totalParty); + + transportLegs.push({ + vipIds: serviceVips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: serviceTour.id, + title: `Service Project Tour Transport (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Community Center - Tour Starting Point', + startTime: start, + endTime: end, + description: `Transport to community service sites (${totalParty} people)`, + notes: 'Casual attire - may get dirty during service activities', + }); + } + + // Farewell Brunch Transport - Charter bus + { + const totalParty = vips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(2685); + const end = this.relativeTime(2700); + const driver = findAvailableDriver(start, end); + const vehicle = vehicles.find(v => v.name === 'Charter Bus'); + if (vehicle) { + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); + } + } + + transportLegs.push({ + vipIds: vips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: farewellBrunch.id, + title: `Transport to Farewell Brunch (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'VIP Dining Pavilion', + startTime: start, + endTime: end, + description: `Final group transport to farewell event (${totalParty} people)`, + }); + } + + // Closing Ceremony Transport - Charter bus + { + const totalParty = vips.reduce((sum, v) => sum + v.partySize, 0); + const start = this.relativeTime(2805); + const end = this.relativeTime(2820); + const driver = findAvailableDriver(start, end); + const vehicle = vehicles.find(v => v.name === 'Charter Bus'); + if (vehicle) { + const schedule = vehicleSchedule.get(vehicle.id) || []; + if (!hasConflict(schedule, start, end)) { + schedule.push({ start, end }); + } + } + + transportLegs.push({ + vipIds: vips.map(v => v.id), + driverId: driver?.id, + vehicleId: vehicle?.id, + masterEventId: closingCeremony.id, + title: `Final Transport to Closing Ceremony (${totalParty} passengers)`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Dining Pavilion', + dropoffLocation: 'Main Arena - VIP Entrance', + startTime: start, + endTime: end, + description: `Charter bus for closing ceremony (${totalParty} people)`, + }); + } + + // Airport Departures (Day 3 afternoon) - Staggered individual/group departures + const departureVips = vips.filter(v => v.arrivalMode === ArrivalMode.FLIGHT); + departureVips.forEach((vip, idx) => { + const offset = 2940 + (idx * 30); // Starting after closing ceremony + const start = this.relativeTime(offset); + const end = this.relativeTime(offset + 45); + const driver = findAvailableDriver(start, end); + const vehicle = findAvailableVehicle(start, end, vip.partySize); + + transportLegs.push({ + vipIds: [vip.id], + driverId: driver?.id, + vehicleId: vehicle?.id, + title: `Airport Departure - ${vip.name}${vip.partySize > 1 ? ` (party of ${vip.partySize})` : ''}`, + type: EventType.TRANSPORT, + status: EventStatus.SCHEDULED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'SLC Airport - Departures', + startTime: start, + endTime: end, + description: `Airport departure transport for ${vip.name}${vip.partySize > 1 ? ` + ${vip.partySize - 1} companions` : ''}`, + notes: 'Confirm flight departure time 2 hours prior', + }); + }); + + // CANCELLED event for realism + { + const vip = vips[5]; + transportLegs.push({ + vipIds: [vip.id], + title: 'Private Museum Tour - CANCELLED', + type: EventType.TRANSPORT, + status: EventStatus.CANCELLED, + pickupLocation: 'VIP Lodge', + dropoffLocation: 'Local History Museum', + startTime: this.relativeTime(400), + endTime: this.relativeTime(480), + description: 'Museum tour cancelled due to facility closure', + notes: 'Facility unexpectedly closed for maintenance', + }); + } + + // Create all transport legs using createMany + await tx.scheduleEvent.createMany({ data: transportLegs }); + + return { + masterEvents, + transportLegs: transportLegs + }; } // ============================================================ diff --git a/frontend/src/pages/EventList.tsx b/frontend/src/pages/EventList.tsx index d40610f..7573bab 100644 --- a/frontend/src/pages/EventList.tsx +++ b/frontend/src/pages/EventList.tsx @@ -415,11 +415,28 @@ export function EventList() { - {event.vips && event.vips.length > 0 - ? event.vips.map(vip => - vip.partySize > 1 ? `${vip.name} (+${vip.partySize - 1})` : vip.name - ).join(', ') - : 'No VIPs assigned'} + {event.vips && event.vips.length > 0 ? ( +
+ {event.vips.slice(0, 2).map(vip => ( +
+ {vip.name} + {vip.partySize > 1 && ( + + +{vip.partySize - 1} + + )} +
+ ))} + {event.vips.length > 2 && ( +
+{event.vips.length - 2} more
+ )} +
+ {event.vips.reduce((sum, v) => sum + (v.partySize || 1), 0)} pax total +
+
+ ) : ( + No VIPs + )} {event.vehicle ? (