interface ScheduleEvent { id: string; title: string; location: string; startTime: string; endTime: string; assignedDriverId?: string; vipId: string; vipName: string; } interface ConflictInfo { type: 'overlap' | 'tight_turnaround' | 'back_to_back'; severity: 'low' | 'medium' | 'high'; message: string; conflictingEvent: ScheduleEvent; timeDifference?: number; // minutes } interface DriverAvailability { driverId: string; driverName: string; status: 'available' | 'scheduled' | 'overlapping' | 'tight_turnaround'; assignmentCount: number; conflicts: ConflictInfo[]; currentAssignments: ScheduleEvent[]; } class DriverConflictService { // Check for conflicts when assigning a driver to an event checkDriverConflicts( driverId: string, newEvent: { startTime: string; endTime: string; location: string }, allSchedules: { [vipId: string]: ScheduleEvent[] }, drivers: any[] ): ConflictInfo[] { const conflicts: ConflictInfo[] = []; const driver = drivers.find(d => d.id === driverId); if (!driver) return conflicts; // Get all events assigned to this driver const driverEvents = this.getDriverEvents(driverId, allSchedules); const newStartTime = new Date(newEvent.startTime); const newEndTime = new Date(newEvent.endTime); for (const existingEvent of driverEvents) { const existingStart = new Date(existingEvent.startTime); const existingEnd = new Date(existingEvent.endTime); // Check for direct time overlap if (this.hasTimeOverlap(newStartTime, newEndTime, existingStart, existingEnd)) { conflicts.push({ type: 'overlap', severity: 'high', message: `Direct time conflict with "${existingEvent.title}" for ${existingEvent.vipName}`, conflictingEvent: existingEvent }); } // Check for tight turnaround (less than 15 minutes between events) else { const timeBetween = this.getTimeBetweenEvents( newStartTime, newEndTime, existingStart, existingEnd ); if (timeBetween !== null && timeBetween < 15) { conflicts.push({ type: 'tight_turnaround', severity: timeBetween < 5 ? 'high' : 'medium', message: `Only ${timeBetween} minutes between events. Previous: "${existingEvent.title}"`, conflictingEvent: existingEvent, timeDifference: timeBetween }); } } } return conflicts; } // Get availability status for all drivers for a specific time slot getDriverAvailability( eventTime: { startTime: string; endTime: string; location: string }, allSchedules: { [vipId: string]: ScheduleEvent[] }, drivers: any[] ): DriverAvailability[] { return drivers.map(driver => { const conflicts = this.checkDriverConflicts(driver.id, eventTime, allSchedules, drivers); const driverEvents = this.getDriverEvents(driver.id, allSchedules); let status: DriverAvailability['status'] = 'available'; if (conflicts.length > 0) { const hasOverlap = conflicts.some(c => c.type === 'overlap'); const hasTightTurnaround = conflicts.some(c => c.type === 'tight_turnaround'); if (hasOverlap) { status = 'overlapping'; } else if (hasTightTurnaround) { status = 'tight_turnaround'; } } else if (driverEvents.length > 0) { status = 'scheduled'; } return { driverId: driver.id, driverName: driver.name, status, assignmentCount: driverEvents.length, conflicts, currentAssignments: driverEvents }; }); } // Get all events assigned to a specific driver private getDriverEvents(driverId: string, allSchedules: { [vipId: string]: ScheduleEvent[] }): ScheduleEvent[] { const driverEvents: ScheduleEvent[] = []; Object.entries(allSchedules).forEach(([vipId, events]) => { events.forEach(event => { if (event.assignedDriverId === driverId) { driverEvents.push({ ...event, vipId, vipName: event.title // We'll need to get actual VIP name from VIP data }); } }); }); // Sort by start time return driverEvents.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime() ); } // Check if two time periods overlap private hasTimeOverlap( start1: Date, end1: Date, start2: Date, end2: Date ): boolean { return start1 < end2 && start2 < end1; } // Get minutes between two events (null if they overlap) private getTimeBetweenEvents( newStart: Date, newEnd: Date, existingStart: Date, existingEnd: Date ): number | null { // If new event is after existing event if (newStart >= existingEnd) { return Math.floor((newStart.getTime() - existingEnd.getTime()) / (1000 * 60)); } // If new event is before existing event else if (newEnd <= existingStart) { return Math.floor((existingStart.getTime() - newEnd.getTime()) / (1000 * 60)); } // Events overlap return null; } // Generate summary message for driver status getDriverStatusSummary(availability: DriverAvailability): string { switch (availability.status) { case 'available': return `✅ Fully available (${availability.assignmentCount} assignments)`; case 'scheduled': return `🟡 Has ${availability.assignmentCount} assignment(s) but available for this time`; case 'tight_turnaround': const tightConflict = availability.conflicts.find(c => c.type === 'tight_turnaround'); return `⚡ Tight turnaround - ${tightConflict?.timeDifference} min between events`; case 'overlapping': return `🔴 Time conflict with existing assignment`; default: return 'Unknown status'; } } } export default new DriverConflictService(); export { DriverAvailability, ConflictInfo, ScheduleEvent };