feat: comprehensive update with Signal, Copilot, themes, and PDF features
## Signal Messaging Integration - Added SignalService for sending messages to drivers via Signal - SignalMessage model for tracking message history - Driver chat modal for real-time messaging - Send schedule via Signal (ICS + PDF attachments) ## AI Copilot - Natural language interface for VIP Coordinator - Capabilities: create VIPs, schedule events, assign drivers - Help and guidance for users - Floating copilot button in UI ## Theme System - Dark/light/system theme support - Color scheme selection (blue, green, purple, orange, red) - ThemeContext for global state - AppearanceMenu in header ## PDF Schedule Export - VIPSchedulePDF component for schedule generation - PDF settings (header, footer, branding) - Preview PDF in browser - Settings stored in database ## Database Migrations - add_signal_messages: SignalMessage model - add_pdf_settings: Settings model for PDF config - add_reminder_tracking: lastReminderSent for events - make_driver_phone_optional: phone field nullable ## Event Management - Event status service for automated updates - IN_PROGRESS/COMPLETED status tracking - Reminder tracking for notifications ## UI/UX Improvements - Driver schedule modal - Improved My Schedule page - Better error handling and loading states - Responsive design improvements ## Other Changes - AGENT_TEAM.md documentation - Seed data improvements - Ability factory updates - Driver profile page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
626
backend/src/seed/seed.service.ts
Normal file
626
backend/src/seed/seed.service.ts
Normal file
@@ -0,0 +1,626 @@
|
||||
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
|
||||
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 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`);
|
||||
|
||||
return { vips, drivers, vehicles, flights, events };
|
||||
});
|
||||
|
||||
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 eventData = this.getEventData(vips, drivers, vehicles);
|
||||
await this.prisma.scheduleEvent.createMany({ data: eventData });
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
return {
|
||||
success: true,
|
||||
elapsed: `${elapsed}ms`,
|
||||
created: { events: eventData.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, 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.' },
|
||||
|
||||
// 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.' },
|
||||
];
|
||||
}
|
||||
|
||||
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 }, // Off duty until 2pm
|
||||
{ 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: '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' },
|
||||
];
|
||||
}
|
||||
|
||||
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'; // Assuming Salt Lake City for the Jamboree
|
||||
|
||||
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; // -60 to +150 minutes from now
|
||||
const scheduledArrival = this.relativeTime(arrivalOffset);
|
||||
const scheduledDeparture = new Date(scheduledArrival.getTime() - 3 * 60 * 60 * 1000); // 3 hours before
|
||||
|
||||
// Some flights are delayed, some landed, some on time
|
||||
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;
|
||||
}
|
||||
|
||||
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 }
|
||||
const vehicleSchedule: Map<string, Array<{ start: Date; end: Date }>> = new Map();
|
||||
const driverSchedule: Map<string, Array<{ start: Date; end: Date }>> = 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
|
||||
const hasConflict = (schedule: Array<{ start: Date; end: Date }>, start: Date, end: Date): boolean => {
|
||||
return schedule.some(slot =>
|
||||
(start < slot.end && end > slot.start) // Overlapping
|
||||
);
|
||||
};
|
||||
|
||||
// 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
|
||||
for (const driver of drivers) {
|
||||
const schedule = driverSchedule.get(driver.id) || [];
|
||||
if (!hasConflict(schedule, start, end)) {
|
||||
schedule.push({ start, end });
|
||||
return driver;
|
||||
}
|
||||
}
|
||||
return null; // No available driver
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ADD STANDARD DAY EVENTS FOR ALL VIPS
|
||||
// ============================================================
|
||||
|
||||
// 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`,
|
||||
});
|
||||
|
||||
// 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({
|
||||
vipIds: [vip.id],
|
||||
driverId: driver?.id,
|
||||
vehicleId: vehicle?.id,
|
||||
title: `Transport to Scout Exhibition`,
|
||||
type: EventType.TRANSPORT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
pickupLocation: 'VIP Lodge',
|
||||
dropoffLocation: 'Exhibition Grounds',
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
description: `Transport ${vip.name} to Scout Exhibition area`,
|
||||
});
|
||||
}
|
||||
|
||||
// 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],
|
||||
driverId: driver?.id,
|
||||
vehicleId: vehicle?.id,
|
||||
title: `Airport Departure - ${vip.name}`,
|
||||
type: EventType.TRANSPORT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
pickupLocation: 'VIP Lodge',
|
||||
dropoffLocation: 'Airport - Departures',
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
description: `Transport ${vip.name} to airport for departure flight`,
|
||||
notes: 'Confirm flight status before pickup',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 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 });
|
||||
}
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user