import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { toDateString, startOfDay } from '../common/utils/date.utils'; interface ToolResult { success: boolean; data?: any; error?: string; message?: string; } @Injectable() export class CopilotFleetService { private readonly logger = new Logger(CopilotFleetService.name); constructor(private readonly prisma: PrismaService) {} async getAvailableVehicles(filters: Record): Promise { const where: any = { deletedAt: null, status: 'AVAILABLE' }; if (filters.type) { where.type = filters.type; } if (filters.minSeats) { where.seatCapacity = { gte: filters.minSeats }; } const vehicles = await this.prisma.vehicle.findMany({ where, orderBy: [{ type: 'asc' }, { seatCapacity: 'desc' }], }); return { success: true, data: vehicles, message: `Found ${vehicles.length} available vehicle(s).`, }; } async assignVehicleToEvent(eventId: string, vehicleId: string): Promise { const event = await this.prisma.scheduleEvent.findFirst({ where: { id: eventId, deletedAt: null }, }); if (!event) { return { success: false, error: `Event with ID ${eventId} not found.` }; } // If vehicleId is null, we're unassigning if (vehicleId === null || vehicleId === 'null') { const updatedEvent = await this.prisma.scheduleEvent.update({ where: { id: eventId }, data: { vehicleId: null }, include: { driver: true, }, }); return { success: true, data: updatedEvent, message: `Vehicle unassigned from event "${updatedEvent.title}"`, }; } // Verify vehicle exists const vehicle = await this.prisma.vehicle.findFirst({ where: { id: vehicleId, deletedAt: null }, }); if (!vehicle) { return { success: false, error: `Vehicle with ID ${vehicleId} not found.` }; } const updatedEvent = await this.prisma.scheduleEvent.update({ where: { id: eventId }, data: { vehicleId }, include: { driver: true, vehicle: true, }, }); return { success: true, data: updatedEvent, message: `Vehicle ${vehicle.name} assigned to event "${updatedEvent.title}"`, }; } async suggestVehicleForEvent(input: Record): Promise { const { eventId } = input; const event = await this.prisma.scheduleEvent.findFirst({ where: { id: eventId, deletedAt: null }, }); if (!event) { return { success: false, error: `Event with ID ${eventId} not found.` }; } // Fetch VIP info to determine party size const vips = await this.prisma.vIP.findMany({ where: { id: { in: event.vipIds } }, select: { id: true, name: true, partySize: true }, }); // Determine required capacity based on total party size const requiredSeats = vips.reduce((sum, v) => sum + (v.partySize || 1), 0); // Find vehicles not in use during this event time const busyVehicleIds = await this.prisma.scheduleEvent.findMany({ where: { deletedAt: null, id: { not: eventId }, status: { not: 'CANCELLED' }, vehicleId: { not: null }, OR: [ { startTime: { lte: event.startTime }, endTime: { gt: event.startTime }, }, { startTime: { lt: event.endTime }, endTime: { gte: event.endTime }, }, ], }, select: { vehicleId: true }, }); const busyIds = busyVehicleIds.map((e) => e.vehicleId).filter((id): id is string => id !== null); // Find available vehicles with sufficient capacity const suitableVehicles = await this.prisma.vehicle.findMany({ where: { deletedAt: null, status: 'AVAILABLE', seatCapacity: { gte: requiredSeats }, id: { notIn: busyIds }, }, orderBy: [ { seatCapacity: 'asc' }, // Prefer smallest suitable vehicle ], }); return { success: true, data: { eventId, eventTitle: event.title, vipNames: vips.map((v) => v.name), requiredSeats, suggestions: suitableVehicles.map((v) => ({ id: v.id, name: v.name, type: v.type, seatCapacity: v.seatCapacity, })), }, message: suitableVehicles.length > 0 ? `Found ${suitableVehicles.length} suitable vehicle(s) for this event (requires ${requiredSeats} seat(s)).` : `No available vehicles found with capacity for ${requiredSeats} passenger(s) during this time.`, }; } async getVehicleSchedule(input: Record): Promise { const { vehicleName, vehicleId, startDate, endDate } = input; let vehicle; if (vehicleId) { vehicle = await this.prisma.vehicle.findFirst({ where: { id: vehicleId, deletedAt: null }, }); } else if (vehicleName) { const vehicles = await this.prisma.vehicle.findMany({ where: { deletedAt: null, name: { contains: vehicleName, mode: 'insensitive' }, }, }); if (vehicles.length === 0) { return { success: false, error: `No vehicle found matching "${vehicleName}".` }; } if (vehicles.length > 1) { return { success: false, error: `Multiple vehicles match "${vehicleName}": ${vehicles.map((v) => v.name).join(', ')}. Please be more specific.`, }; } vehicle = vehicles[0]; } else { return { success: false, error: 'Either vehicleName or vehicleId is required.' }; } if (!vehicle) { return { success: false, error: 'Vehicle not found.' }; } const dateStart = startOfDay(new Date(startDate)); const dateEnd = new Date(endDate); dateEnd.setHours(23, 59, 59, 999); const events = await this.prisma.scheduleEvent.findMany({ where: { deletedAt: null, vehicleId: vehicle.id, startTime: { gte: dateStart, lte: dateEnd }, status: { not: 'CANCELLED' }, }, include: { driver: true, }, orderBy: { startTime: 'asc' }, }); // Fetch VIP names for all events const allVipIds = events.flatMap((e) => e.vipIds); const uniqueVipIds = [...new Set(allVipIds)]; const vips = await this.prisma.vIP.findMany({ where: { id: { in: uniqueVipIds } }, select: { id: true, name: true }, }); const vipMap = new Map(vips.map((v) => [v.id, v.name])); const totalHours = events.reduce((sum, e) => { return sum + (e.endTime.getTime() - e.startTime.getTime()); }, 0) / 3600000; return { success: true, data: { vehicle: { id: vehicle.id, name: vehicle.name, type: vehicle.type, seatCapacity: vehicle.seatCapacity, status: vehicle.status, }, dateRange: { start: toDateString(dateStart), end: toDateString(dateEnd), }, eventCount: events.length, totalHours: Math.round(totalHours * 10) / 10, events: events.map((e) => ({ eventId: e.id, title: e.title, type: e.type, startTime: e.startTime, endTime: e.endTime, vipNames: e.vipIds.map((id) => vipMap.get(id) || 'Unknown'), driverName: e.driver?.name || null, pickupLocation: e.pickupLocation, dropoffLocation: e.dropoffLocation, location: e.location, })), }, message: `Vehicle ${vehicle.name} has ${events.length} scheduled event(s) (${Math.round(totalHours * 10) / 10} hours total).`, }; } async searchDrivers(filters: Record): Promise { const where: any = { deletedAt: null }; if (filters.name) { where.name = { contains: filters.name, mode: 'insensitive' }; } if (filters.department) { where.department = filters.department; } if (filters.availableOnly) { where.isAvailable = true; } const drivers = await this.prisma.driver.findMany({ where, orderBy: { name: 'asc' }, }); return { success: true, data: drivers, message: `Found ${drivers.length} driver(s) matching the criteria.`, }; } async getDriverSchedule( driverId: string, startDate?: string, endDate?: string, ): Promise { const driver = await this.prisma.driver.findFirst({ where: { id: driverId, deletedAt: null }, }); if (!driver) { return { success: false, error: `Driver with ID ${driverId} not found.` }; } const where: any = { deletedAt: null, driverId, status: { not: 'CANCELLED' }, }; if (startDate) { where.startTime = { gte: new Date(startDate) }; } if (endDate) { where.endTime = { lte: new Date(endDate) }; } const events = await this.prisma.scheduleEvent.findMany({ where, include: { vehicle: true, }, orderBy: { startTime: 'asc' }, }); // Fetch VIP names for all events const allVipIds = events.flatMap((e) => e.vipIds); const uniqueVipIds = [...new Set(allVipIds)]; const vips = await this.prisma.vIP.findMany({ where: { id: { in: uniqueVipIds } }, select: { id: true, name: true }, }); const vipMap = new Map(vips.map((v) => [v.id, v.name])); const eventsWithVipNames = events.map((event) => ({ ...event, vipNames: event.vipIds.map((id) => vipMap.get(id) || 'Unknown'), })); return { success: true, data: { driver, events: eventsWithVipNames, eventCount: events.length, }, message: `Driver ${driver.name} has ${events.length} scheduled event(s).`, }; } async listAllDrivers(input: Record): Promise { const { includeUnavailable = true } = input; const where: any = { deletedAt: null }; if (!includeUnavailable) { where.isAvailable = true; } const drivers = await this.prisma.driver.findMany({ where, orderBy: { name: 'asc' }, select: { id: true, name: true, phone: true, department: true, isAvailable: true, }, }); return { success: true, data: drivers, message: `Found ${drivers.length} driver(s) in the system.`, }; } async findAvailableDriversForTimerange(input: Record): Promise { const { startTime, endTime, preferredDepartment } = input; // Get all drivers const where: any = { deletedAt: null, isAvailable: true }; if (preferredDepartment) { where.department = preferredDepartment; } const allDrivers = await this.prisma.driver.findMany({ where, }); // Find drivers with conflicting events const busyDriverIds = await this.prisma.scheduleEvent.findMany({ where: { deletedAt: null, driverId: { not: null }, status: { not: 'CANCELLED' }, OR: [ { startTime: { lte: new Date(startTime) }, endTime: { gt: new Date(startTime) }, }, { startTime: { lt: new Date(endTime) }, endTime: { gte: new Date(endTime) }, }, ], }, select: { driverId: true }, }); const busyIds = new Set(busyDriverIds.map((e) => e.driverId)); const availableDrivers = allDrivers.filter((d) => !busyIds.has(d.id)); return { success: true, data: availableDrivers, message: `Found ${availableDrivers.length} available driver(s) for the specified time range.`, }; } async updateDriver(input: Record): Promise { const { driverId, ...updates } = input; const existingDriver = await this.prisma.driver.findFirst({ where: { id: driverId, deletedAt: null }, }); if (!existingDriver) { return { success: false, error: `Driver with ID ${driverId} not found.` }; } const updateData: any = {}; if (updates.name !== undefined) updateData.name = updates.name; if (updates.phone !== undefined) updateData.phone = updates.phone; if (updates.department !== undefined) updateData.department = updates.department; if (updates.isAvailable !== undefined) updateData.isAvailable = updates.isAvailable; if (updates.shiftStartTime !== undefined) updateData.shiftStartTime = updates.shiftStartTime; if (updates.shiftEndTime !== undefined) updateData.shiftEndTime = updates.shiftEndTime; const driver = await this.prisma.driver.update({ where: { id: driverId }, data: updateData, }); this.logger.log(`Driver updated: ${driverId}`); return { success: true, data: driver, message: `Driver ${driver.name} updated successfully.`, }; } }