- 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 <noreply@anthropic.com>
1090 lines
48 KiB
TypeScript
1090 lines
48 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { Department, ArrivalMode, EventType, EventStatus, VehicleType, VehicleStatus } from '@prisma/client';
|
|
|
|
@Injectable()
|
|
export class SeedService {
|
|
private readonly logger = new Logger(SeedService.name);
|
|
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
/**
|
|
* Clear all data using fast deleteMany operations
|
|
*/
|
|
async clearAllData() {
|
|
const start = Date.now();
|
|
|
|
// Delete in order to respect foreign key constraints
|
|
const results = await this.prisma.$transaction([
|
|
this.prisma.signalMessage.deleteMany(),
|
|
this.prisma.scheduleEvent.deleteMany(),
|
|
this.prisma.flight.deleteMany(),
|
|
this.prisma.vehicle.deleteMany(),
|
|
this.prisma.driver.deleteMany(),
|
|
this.prisma.vIP.deleteMany(),
|
|
]);
|
|
|
|
const elapsed = Date.now() - start;
|
|
this.logger.log(`Cleared all data in ${elapsed}ms`);
|
|
|
|
return {
|
|
success: true,
|
|
elapsed: `${elapsed}ms`,
|
|
deleted: {
|
|
messages: results[0].count,
|
|
events: results[1].count,
|
|
flights: results[2].count,
|
|
vehicles: results[3].count,
|
|
drivers: results[4].count,
|
|
vips: results[5].count,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate all test data in a single fast transaction
|
|
*/
|
|
async generateAllTestData(clearFirst: boolean = true) {
|
|
const start = Date.now();
|
|
|
|
if (clearFirst) {
|
|
await this.clearAllData();
|
|
}
|
|
|
|
// Create all entities in a transaction
|
|
const result = await this.prisma.$transaction(async (tx) => {
|
|
// 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' } });
|
|
this.logger.log(`Created ${vips.length} VIPs`);
|
|
|
|
// 2. Create Drivers with shifts
|
|
const driverData = this.getDriverData();
|
|
await tx.driver.createMany({ data: driverData });
|
|
const drivers = await tx.driver.findMany({ orderBy: { createdAt: 'asc' } });
|
|
this.logger.log(`Created ${drivers.length} drivers`);
|
|
|
|
// 3. Create Vehicles
|
|
const vehicleData = this.getVehicleData();
|
|
await tx.vehicle.createMany({ data: vehicleData });
|
|
const vehicles = await tx.vehicle.findMany({ orderBy: { createdAt: 'asc' } });
|
|
this.logger.log(`Created ${vehicles.length} vehicles`);
|
|
|
|
// 4. Create Flights for VIPs arriving by flight
|
|
const flightVips = vips.filter(v => v.arrivalMode === 'FLIGHT');
|
|
const flightData = this.getFlightData(flightVips);
|
|
await tx.flight.createMany({ data: flightData });
|
|
const flights = await tx.flight.findMany();
|
|
this.logger.log(`Created ${flights.length} flights`);
|
|
|
|
// 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: [...masterEvents, ...transportLegs]
|
|
};
|
|
});
|
|
|
|
const elapsed = Date.now() - start;
|
|
this.logger.log(`Generated all test data in ${elapsed}ms`);
|
|
|
|
return {
|
|
success: true,
|
|
elapsed: `${elapsed}ms`,
|
|
created: {
|
|
vips: result.vips.length,
|
|
drivers: result.drivers.length,
|
|
vehicles: result.vehicles.length,
|
|
flights: result.flights.length,
|
|
events: result.events.length,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate only dynamic events (uses existing VIPs/drivers/vehicles)
|
|
*/
|
|
async generateDynamicEvents() {
|
|
const start = Date.now();
|
|
|
|
// Clear existing events
|
|
await this.prisma.scheduleEvent.deleteMany();
|
|
|
|
// Get existing entities
|
|
const [vips, drivers, vehicles] = await Promise.all([
|
|
this.prisma.vIP.findMany({ where: { deletedAt: null } }),
|
|
this.prisma.driver.findMany({ where: { deletedAt: null } }),
|
|
this.prisma.vehicle.findMany({ where: { deletedAt: null } }),
|
|
]);
|
|
|
|
if (vips.length === 0) {
|
|
return { success: false, error: 'No VIPs found. Generate full test data first.' };
|
|
}
|
|
|
|
// Create events
|
|
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: masterEvents.length + transportLegs.length,
|
|
masterEvents: masterEvents.length,
|
|
transportLegs: transportLegs.length
|
|
},
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// DATA GENERATORS
|
|
// ============================================================
|
|
|
|
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, 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, 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.' },
|
|
];
|
|
}
|
|
|
|
private getDriverData() {
|
|
const now = new Date();
|
|
const shiftStart = new Date(now);
|
|
shiftStart.setHours(6, 0, 0, 0);
|
|
const shiftEnd = new Date(now);
|
|
shiftEnd.setHours(22, 0, 0, 0);
|
|
|
|
const lateShiftStart = new Date(now);
|
|
lateShiftStart.setHours(14, 0, 0, 0);
|
|
const lateShiftEnd = new Date(now);
|
|
lateShiftEnd.setHours(23, 59, 0, 0);
|
|
|
|
return [
|
|
{ name: 'Michael Thompson', phone: '555-0101', department: Department.OFFICE_OF_DEVELOPMENT, isAvailable: true, shiftStartTime: shiftStart, shiftEndTime: shiftEnd },
|
|
{ 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 },
|
|
{ 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 },
|
|
];
|
|
}
|
|
|
|
private getVehicleData() {
|
|
return [
|
|
{ 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' },
|
|
];
|
|
}
|
|
|
|
private getFlightData(vips: any[]) {
|
|
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';
|
|
|
|
vips.forEach((vip, index) => {
|
|
const airline = airlines[index % airlines.length];
|
|
const flightNum = `${airline}${1000 + index * 123}`;
|
|
const origin = origins[index % origins.length];
|
|
|
|
// Arrival flight - times relative to now
|
|
const arrivalOffset = (index % 8) * 30 - 60;
|
|
const scheduledArrival = this.relativeTime(arrivalOffset);
|
|
const scheduledDeparture = new Date(scheduledArrival.getTime() - 3 * 60 * 60 * 1000);
|
|
|
|
let status = 'scheduled';
|
|
let actualArrival = null;
|
|
if (arrivalOffset < -30) {
|
|
status = 'landed';
|
|
actualArrival = new Date(scheduledArrival.getTime() + (Math.random() * 20 - 10) * 60000);
|
|
} else if (arrivalOffset < 0) {
|
|
status = 'landing';
|
|
} else if (index % 5 === 0) {
|
|
status = 'delayed';
|
|
}
|
|
|
|
flights.push({
|
|
vipId: vip.id,
|
|
flightNumber: flightNum,
|
|
flightDate: new Date(),
|
|
segment: 1,
|
|
departureAirport: origin,
|
|
arrivalAirport: destination,
|
|
scheduledDeparture,
|
|
scheduledArrival,
|
|
actualArrival,
|
|
status,
|
|
});
|
|
|
|
// Some VIPs have connecting flights (segment 2)
|
|
if (index % 4 === 0) {
|
|
const connectOrigin = origins[(index + 3) % origins.length];
|
|
flights.push({
|
|
vipId: vip.id,
|
|
flightNumber: `${airline}${500 + index}`,
|
|
flightDate: new Date(),
|
|
segment: 2,
|
|
departureAirport: connectOrigin,
|
|
arrivalAirport: origin,
|
|
scheduledDeparture: new Date(scheduledDeparture.getTime() - 4 * 60 * 60 * 1000),
|
|
scheduledArrival: new Date(scheduledDeparture.getTime() - 1 * 60 * 60 * 1000),
|
|
status: 'landed',
|
|
});
|
|
}
|
|
});
|
|
|
|
return flights;
|
|
}
|
|
|
|
/**
|
|
* 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<string, Array<{ start: Date; end: Date }>> = new Map();
|
|
const driverSchedule: Map<string, Array<{ start: Date; end: Date }>> = new Map();
|
|
|
|
vehicles.forEach(v => vehicleSchedule.set(v.id, []));
|
|
drivers.forEach(d => driverSchedule.set(d.id, []));
|
|
|
|
// ============================================================
|
|
// 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));
|
|
};
|
|
|
|
const findAvailableDriver = (start: Date, end: Date): any | null => {
|
|
for (const driver of drivers) {
|
|
const schedule = driverSchedule.get(driver.id) || [];
|
|
if (!hasConflict(schedule, start, end)) {
|
|
schedule.push({ start, end });
|
|
return driver;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// ============================================================
|
|
// DAY 1 TRANSPORT LEGS
|
|
// ============================================================
|
|
|
|
// Airport pickups for flight-arriving VIPs (staggered arrivals, SOME COMPLETED/IN_PROGRESS)
|
|
const flightVips = vips.filter(v => v.arrivalMode === ArrivalMode.FLIGHT && v.airportPickup);
|
|
|
|
// 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,
|
|
masterEventId: welcomeReg.id,
|
|
title: `Airport Pickup - ${vip.name} (PRIORITY)`,
|
|
type: EventType.TRANSPORT,
|
|
status: EventStatus.COMPLETED,
|
|
pickupLocation: 'SLC Airport - Private Terminal',
|
|
dropoffLocation: 'Main Registration Pavilion',
|
|
startTime: start,
|
|
endTime: end,
|
|
actualStartTime: this.relativeTime(-150),
|
|
actualEndTime: this.relativeTime(-90),
|
|
description: `Completed VIP airport pickup for National President + security detail (${vip.partySize} people)`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
masterEventId: openingCeremony.id,
|
|
title: `Multi-VIP Airport Pickup (${totalParty} passengers)`,
|
|
type: EventType.TRANSPORT,
|
|
status: EventStatus.IN_PROGRESS,
|
|
pickupLocation: 'SLC Airport - Terminal 1',
|
|
dropoffLocation: 'Main Arena - VIP Entrance',
|
|
startTime: start,
|
|
endTime: end,
|
|
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` : ''}`,
|
|
});
|
|
});
|
|
|
|
// 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 });
|
|
}
|
|
}
|
|
|
|
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`,
|
|
});
|
|
}
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// HELPER METHODS
|
|
// ============================================================
|
|
|
|
/**
|
|
* Get a date relative to now
|
|
* @param minutesOffset - Minutes from now (negative = past, positive = future)
|
|
*/
|
|
private relativeTime(minutesOffset: number): Date {
|
|
return new Date(Date.now() + minutesOffset * 60 * 1000);
|
|
}
|
|
}
|