This commit implements comprehensive driver schedule self-service functionality, allowing drivers to access their own schedules without requiring administrator permissions, along with full schedule support for multi-day views. Backend Changes: - Added /drivers/me/* endpoints for driver self-service operations: - GET /drivers/me - Get authenticated driver's profile - GET /drivers/me/schedule/ics - Export driver's own schedule as ICS - GET /drivers/me/schedule/pdf - Export driver's own schedule as PDF - POST /drivers/me/send-schedule - Send schedule to driver via Signal - PATCH /drivers/me - Update driver's own profile - Added fullSchedule parameter support to schedule export service: - Defaults to true (full upcoming schedule) - Pass fullSchedule=false for single-day view - Applied to ICS, PDF, and Signal message generation - Fixed route ordering in drivers.controller.ts: - Static routes (send-all-schedules) now come before :id routes - Prevents path matching issues - TypeScript improvements in copilot.service.ts: - Fixed type errors with proper null handling - Added explicit return types Frontend Changes: - Created MySchedule page with simplified driver-focused UI: - Preview PDF button - Opens schedule PDF in new browser tab - Send to Signal button - Sends schedule directly to driver's phone - Uses /drivers/me/* endpoints to avoid permission issues - No longer requires driver ID parameter - Resolved "Forbidden Resource" errors for driver role users: - Replaced /drivers/:id endpoints with /drivers/me endpoints - Drivers can now access their own data without admin permissions Key Features: 1. Full Schedule by Default - Drivers see all upcoming events, not just today 2. Self-Service Access - Drivers manage their own schedules independently 3. PDF Preview - Quick browser-based preview without downloading 4. Signal Integration - Direct schedule delivery to mobile devices 5. Role-Based Security - Proper CASL permissions for driver self-access This resolves the driver schedule access issue and provides a streamlined experience for drivers to view and share their schedules. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3153 lines
115 KiB
TypeScript
3153 lines
115 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import Anthropic from '@anthropic-ai/sdk';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { MessagesService } from '../signal/messages.service';
|
|
import { ScheduleExportService } from '../drivers/schedule-export.service';
|
|
|
|
interface ChatMessage {
|
|
role: 'user' | 'assistant';
|
|
content: string | any[];
|
|
}
|
|
|
|
interface ToolResult {
|
|
success: boolean;
|
|
data?: any;
|
|
error?: string;
|
|
message?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class CopilotService {
|
|
private readonly logger = new Logger(CopilotService.name);
|
|
private readonly anthropic: Anthropic;
|
|
|
|
// Define available tools for Claude
|
|
private readonly tools: Anthropic.Tool[] = [
|
|
{
|
|
name: 'search_vips',
|
|
description: 'Search for VIPs by name, organization, department, or arrival mode. Returns a list of matching VIPs with their details.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
name: { type: 'string', description: 'VIP name to search for (partial match)' },
|
|
organization: { type: 'string', description: 'Organization name to filter by' },
|
|
department: { type: 'string', enum: ['OFFICE_OF_DEVELOPMENT', 'ADMIN'], description: 'Department to filter by' },
|
|
arrivalMode: { type: 'string', enum: ['FLIGHT', 'SELF_DRIVING'], description: 'Arrival mode to filter by' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_vip_details',
|
|
description: 'Get detailed information about a specific VIP including their flights and scheduled events.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'The VIP ID' },
|
|
},
|
|
required: ['vipId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'search_drivers',
|
|
description: 'Search for drivers by name, phone, or department. Returns a list of drivers with their availability.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
name: { type: 'string', description: 'Driver name to search for' },
|
|
department: { type: 'string', enum: ['OFFICE_OF_DEVELOPMENT', 'ADMIN'], description: 'Department to filter by' },
|
|
availableOnly: { type: 'boolean', description: 'Only return available drivers' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_driver_schedule',
|
|
description: 'Get a driver\'s schedule for a specific date range.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
driverId: { type: 'string', description: 'The driver ID' },
|
|
startDate: { type: 'string', description: 'Start date (ISO format)' },
|
|
endDate: { type: 'string', description: 'End date (ISO format)' },
|
|
},
|
|
required: ['driverId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'search_events',
|
|
description: 'Search for scheduled events/activities. Can filter by VIP name, event title, driver name, date, or status.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'Filter by VIP ID' },
|
|
vipName: { type: 'string', description: 'Filter by VIP name (partial match)' },
|
|
title: { type: 'string', description: 'Filter by event title (partial match)' },
|
|
driverId: { type: 'string', description: 'Filter by driver ID' },
|
|
driverName: { type: 'string', description: 'Filter by driver name (partial match) - will find events assigned to drivers matching this name' },
|
|
date: { type: 'string', description: 'Filter by date (ISO format or YYYY-MM-DD)' },
|
|
status: { type: 'string', enum: ['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'], description: 'Filter by status' },
|
|
type: { type: 'string', enum: ['TRANSPORT', 'MEETING', 'EVENT', 'MEAL', 'ACCOMMODATION'], description: 'Filter by event type' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_available_vehicles',
|
|
description: 'Get a list of available vehicles, optionally filtered by type or seat capacity.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
type: { type: 'string', enum: ['VAN', 'SUV', 'SEDAN', 'BUS', 'GOLF_CART', 'TRUCK'], description: 'Vehicle type' },
|
|
minSeats: { type: 'number', description: 'Minimum seat capacity required' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_flights_for_vip',
|
|
description: 'Get all flights associated with a VIP.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'The VIP ID' },
|
|
},
|
|
required: ['vipId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'update_flight',
|
|
description: 'Update flight information for a VIP. Use this when flight times change.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
flightId: { type: 'string', description: 'The flight ID to update' },
|
|
scheduledDeparture: { type: 'string', description: 'New scheduled departure time (ISO format)' },
|
|
scheduledArrival: { type: 'string', description: 'New scheduled arrival time (ISO format)' },
|
|
status: { type: 'string', description: 'New flight status' },
|
|
},
|
|
required: ['flightId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'create_event',
|
|
description: 'Create a new scheduled event/activity for a VIP. Only use this for NEW events, not to modify existing ones.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'The VIP ID' },
|
|
title: { type: 'string', description: 'Event title' },
|
|
type: { type: 'string', enum: ['TRANSPORT', 'MEETING', 'EVENT', 'MEAL', 'ACCOMMODATION'], description: 'Event type' },
|
|
startTime: { type: 'string', description: 'Start time (ISO format)' },
|
|
endTime: { type: 'string', description: 'End time (ISO format)' },
|
|
location: { type: 'string', description: 'Event location' },
|
|
pickupLocation: { type: 'string', description: 'Pickup location (for transport)' },
|
|
dropoffLocation: { type: 'string', description: 'Dropoff location (for transport)' },
|
|
driverId: { type: 'string', description: 'Assigned driver ID (optional)' },
|
|
vehicleId: { type: 'string', description: 'Assigned vehicle ID (optional)' },
|
|
description: { type: 'string', description: 'Additional notes' },
|
|
},
|
|
required: ['vipId', 'title', 'type', 'startTime', 'endTime'],
|
|
},
|
|
},
|
|
{
|
|
name: 'assign_driver_to_event',
|
|
description: 'Assign or change the driver for an existing event.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
eventId: { type: 'string', description: 'The event ID' },
|
|
driverId: { type: 'string', description: 'The driver ID to assign' },
|
|
},
|
|
required: ['eventId', 'driverId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'update_event',
|
|
description: 'Update an existing event. Use this to change event time, location, title, status, or other details.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
eventId: { type: 'string', description: 'The event ID to update' },
|
|
title: { type: 'string', description: 'New event title' },
|
|
startTime: { type: 'string', description: 'New start time (ISO format)' },
|
|
endTime: { type: 'string', description: 'New end time (ISO format)' },
|
|
location: { type: 'string', description: 'New location' },
|
|
pickupLocation: { type: 'string', description: 'New pickup location' },
|
|
dropoffLocation: { type: 'string', description: 'New dropoff location' },
|
|
status: { type: 'string', enum: ['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'], description: 'New status' },
|
|
driverId: { type: 'string', description: 'New driver ID (use null to unassign)' },
|
|
vehicleId: { type: 'string', description: 'New vehicle ID (use null to unassign)' },
|
|
description: { type: 'string', description: 'New description/notes' },
|
|
},
|
|
required: ['eventId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'delete_event',
|
|
description: 'Delete (soft delete) an event. Use this when an event is cancelled or no longer needed.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
eventId: { type: 'string', description: 'The event ID to delete' },
|
|
},
|
|
required: ['eventId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_todays_summary',
|
|
description: 'Get a summary of today\'s activities including upcoming events, arriving VIPs, and driver assignments.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'create_flight',
|
|
description: 'Create a new flight for a VIP.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'The VIP ID' },
|
|
flightNumber: { type: 'string', description: 'Flight number (e.g., AA1234)' },
|
|
flightDate: { type: 'string', description: 'Flight date (ISO format or YYYY-MM-DD)' },
|
|
departureAirport: { type: 'string', description: 'Departure airport IATA code (e.g., JFK)' },
|
|
arrivalAirport: { type: 'string', description: 'Arrival airport IATA code (e.g., LAX)' },
|
|
scheduledDeparture: { type: 'string', description: 'Scheduled departure time (ISO format)' },
|
|
scheduledArrival: { type: 'string', description: 'Scheduled arrival time (ISO format)' },
|
|
segment: { type: 'number', description: 'Flight segment number for multi-leg trips (default 1)' },
|
|
},
|
|
required: ['vipId', 'flightNumber', 'flightDate', 'departureAirport', 'arrivalAirport'],
|
|
},
|
|
},
|
|
{
|
|
name: 'delete_flight',
|
|
description: 'Delete a flight record.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
flightId: { type: 'string', description: 'The flight ID to delete' },
|
|
},
|
|
required: ['flightId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'create_vip',
|
|
description: 'Create a new VIP in the system.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
name: { type: 'string', description: 'VIP full name' },
|
|
organization: { type: 'string', description: 'Organization/company name' },
|
|
department: { type: 'string', enum: ['OFFICE_OF_DEVELOPMENT', 'ADMIN'], description: 'Department' },
|
|
arrivalMode: { type: 'string', enum: ['FLIGHT', 'SELF_DRIVING'], description: 'How VIP will arrive' },
|
|
expectedArrival: { type: 'string', description: 'Expected arrival time for self-driving (ISO format)' },
|
|
airportPickup: { type: 'boolean', description: 'Whether VIP needs airport pickup' },
|
|
venueTransport: { type: 'boolean', description: 'Whether VIP needs venue transport' },
|
|
notes: { type: 'string', description: 'Additional notes about the VIP' },
|
|
},
|
|
required: ['name', 'department', 'arrivalMode'],
|
|
},
|
|
},
|
|
{
|
|
name: 'update_vip',
|
|
description: 'Update VIP information.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'The VIP ID to update' },
|
|
name: { type: 'string', description: 'New name' },
|
|
organization: { type: 'string', description: 'New organization' },
|
|
department: { type: 'string', enum: ['OFFICE_OF_DEVELOPMENT', 'ADMIN'], description: 'New department' },
|
|
arrivalMode: { type: 'string', enum: ['FLIGHT', 'SELF_DRIVING'], description: 'New arrival mode' },
|
|
expectedArrival: { type: 'string', description: 'New expected arrival time' },
|
|
airportPickup: { type: 'boolean', description: 'Whether VIP needs airport pickup' },
|
|
venueTransport: { type: 'boolean', description: 'Whether VIP needs venue transport' },
|
|
notes: { type: 'string', description: 'New notes' },
|
|
},
|
|
required: ['vipId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'assign_vehicle_to_event',
|
|
description: 'Assign or change the vehicle for an existing event.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
eventId: { type: 'string', description: 'The event ID' },
|
|
vehicleId: { type: 'string', description: 'The vehicle ID to assign (use null to unassign)' },
|
|
},
|
|
required: ['eventId', 'vehicleId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'update_driver',
|
|
description: 'Update driver information like availability, contact info, or shift times.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
driverId: { type: 'string', description: 'The driver ID to update' },
|
|
name: { type: 'string', description: 'New name' },
|
|
phone: { type: 'string', description: 'New phone number' },
|
|
department: { type: 'string', enum: ['OFFICE_OF_DEVELOPMENT', 'ADMIN'], description: 'New department' },
|
|
isAvailable: { type: 'boolean', description: 'Whether driver is available' },
|
|
shiftStartTime: { type: 'string', description: 'Shift start time (HH:MM format)' },
|
|
shiftEndTime: { type: 'string', description: 'Shift end time (HH:MM format)' },
|
|
},
|
|
required: ['driverId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'check_driver_conflicts',
|
|
description: 'Check if a driver has any scheduling conflicts for a given time period.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
driverId: { type: 'string', description: 'The driver ID to check' },
|
|
startTime: { type: 'string', description: 'Start time to check (ISO format)' },
|
|
endTime: { type: 'string', description: 'End time to check (ISO format)' },
|
|
excludeEventId: { type: 'string', description: 'Event ID to exclude from conflict check (for updates)' },
|
|
},
|
|
required: ['driverId', 'startTime', 'endTime'],
|
|
},
|
|
},
|
|
{
|
|
name: 'reassign_driver_events',
|
|
description: 'Reassign all events from one driver to another. Use this when a driver is sick, unavailable, or needs to swap schedules. Searches by driver NAME - you do not need IDs.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
fromDriverName: { type: 'string', description: 'Name of the driver to reassign FROM (the one who is sick/unavailable)' },
|
|
toDriverName: { type: 'string', description: 'Name of the driver to reassign TO (the replacement driver)' },
|
|
date: { type: 'string', description: 'Optional: only reassign events on this date (YYYY-MM-DD). If not provided, reassigns all future events.' },
|
|
onlyStatus: { type: 'string', enum: ['SCHEDULED', 'IN_PROGRESS'], description: 'Optional: only reassign events with this status' },
|
|
},
|
|
required: ['fromDriverName', 'toDriverName'],
|
|
},
|
|
},
|
|
{
|
|
name: 'list_all_drivers',
|
|
description: 'List ALL drivers in the system with their basic info. Use this when you need to see available driver names or find the correct spelling of a driver name.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
includeUnavailable: { type: 'boolean', description: 'Include unavailable drivers (default true)' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_vip_itinerary',
|
|
description: 'Get the complete itinerary for a VIP including all flights and events in chronological order.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipId: { type: 'string', description: 'The VIP ID' },
|
|
startDate: { type: 'string', description: 'Start date for itinerary (optional)' },
|
|
endDate: { type: 'string', description: 'End date for itinerary (optional)' },
|
|
},
|
|
required: ['vipId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'find_available_drivers_for_timerange',
|
|
description: 'Find drivers who have no conflicting events during a specific time range. Returns a list of available drivers with their info.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
startTime: { type: 'string', description: 'Start time of the time range (ISO format)' },
|
|
endTime: { type: 'string', description: 'End time of the time range (ISO format)' },
|
|
preferredDepartment: { type: 'string', enum: ['OFFICE_OF_DEVELOPMENT', 'ADMIN'], description: 'Optional: filter by department' },
|
|
},
|
|
required: ['startTime', 'endTime'],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_daily_driver_manifest',
|
|
description: 'Get a driver\'s complete schedule for a specific day with all event details including VIP names, locations, vehicles, and gaps between events.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
driverName: { type: 'string', description: 'Driver name (partial match works)' },
|
|
driverId: { type: 'string', description: 'Driver ID (use this if you already have the ID)' },
|
|
date: { type: 'string', description: 'Date in YYYY-MM-DD format (optional, defaults to today)' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'send_driver_notification_via_signal',
|
|
description: 'Send a message to a driver via Signal messaging. Use this to notify drivers about schedule changes, reminders, or important updates.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
driverName: { type: 'string', description: 'Driver name (partial match works)' },
|
|
driverId: { type: 'string', description: 'Driver ID (use this if you already have the ID)' },
|
|
message: { type: 'string', description: 'The message content to send to the driver' },
|
|
relatedEventId: { type: 'string', description: 'Optional: Event ID if this message relates to a specific event' },
|
|
},
|
|
required: ['message'],
|
|
},
|
|
},
|
|
{
|
|
name: 'bulk_send_driver_schedules',
|
|
description: 'Send daily schedules to multiple drivers or all drivers via Signal. Automatically generates and sends PDF/ICS schedule files.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
date: { type: 'string', description: 'Date in YYYY-MM-DD format for which to send schedules' },
|
|
driverNames: { type: 'array', items: { type: 'string' }, description: 'Optional: array of driver names. If empty or not provided, sends to all drivers with events on that date.' },
|
|
},
|
|
required: ['date'],
|
|
},
|
|
},
|
|
{
|
|
name: 'find_unassigned_events',
|
|
description: 'Find events that are missing driver and/or vehicle assignments. Useful for identifying scheduling gaps that need attention.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
startDate: { type: 'string', description: 'Start date to search (ISO format or YYYY-MM-DD)' },
|
|
endDate: { type: 'string', description: 'End date to search (ISO format or YYYY-MM-DD)' },
|
|
missingDriver: { type: 'boolean', description: 'Find events missing driver assignment (default true)' },
|
|
missingVehicle: { type: 'boolean', description: 'Find events missing vehicle assignment (default true)' },
|
|
},
|
|
required: ['startDate', 'endDate'],
|
|
},
|
|
},
|
|
{
|
|
name: 'check_vip_conflicts',
|
|
description: 'Check if a VIP has overlapping events in a time range. Useful for preventing double-booking VIPs.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vipName: { type: 'string', description: 'VIP name (partial match works)' },
|
|
vipId: { type: 'string', description: 'VIP ID (use this if you already have the ID)' },
|
|
startTime: { type: 'string', description: 'Start time to check (ISO format)' },
|
|
endTime: { type: 'string', description: 'End time to check (ISO format)' },
|
|
excludeEventId: { type: 'string', description: 'Optional: event ID to exclude from conflict check (for updates)' },
|
|
},
|
|
required: ['startTime', 'endTime'],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_weekly_lookahead',
|
|
description: 'Get a week-by-week summary of upcoming events, VIP arrivals, and unassigned events for planning purposes.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
startDate: { type: 'string', description: 'Start date (optional, defaults to today, YYYY-MM-DD format)' },
|
|
weeksAhead: { type: 'number', description: 'Number of weeks to look ahead (default 1)' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'identify_scheduling_gaps',
|
|
description: 'Audit the upcoming schedule for problems including unassigned events, driver conflicts, VIP conflicts, and capacity issues.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
lookaheadDays: { type: 'number', description: 'Number of days ahead to audit (default 7)' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'suggest_vehicle_for_event',
|
|
description: 'Recommend vehicles for an event based on capacity requirements and availability during the event time.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
eventId: { type: 'string', description: 'The event ID to find vehicle suggestions for' },
|
|
},
|
|
required: ['eventId'],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_vehicle_schedule',
|
|
description: 'Get a vehicle\'s schedule for a date range, showing all events using this vehicle with driver and VIP details.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
vehicleName: { type: 'string', description: 'Vehicle name (partial match works)' },
|
|
vehicleId: { type: 'string', description: 'Vehicle ID (use this if you already have the ID)' },
|
|
startDate: { type: 'string', description: 'Start date (ISO format or YYYY-MM-DD)' },
|
|
endDate: { type: 'string', description: 'End date (ISO format or YYYY-MM-DD)' },
|
|
},
|
|
required: ['startDate', 'endDate'],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_driver_workload_summary',
|
|
description: 'Get workload statistics for all drivers including event count, total hours, and availability percentage for a date range.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
startDate: { type: 'string', description: 'Start date (ISO format or YYYY-MM-DD)' },
|
|
endDate: { type: 'string', description: 'End date (ISO format or YYYY-MM-DD)' },
|
|
},
|
|
required: ['startDate', 'endDate'],
|
|
},
|
|
},
|
|
// ==================== SELF-AWARENESS / HELP TOOLS ====================
|
|
{
|
|
name: 'get_my_capabilities',
|
|
description: 'Get a comprehensive list of all available tools and capabilities. Use this when the user asks "what can you do?" or when you need to understand your own capabilities. Returns tools organized by category with usage examples.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
category: {
|
|
type: 'string',
|
|
enum: ['all', 'search', 'create', 'update', 'delete', 'communication', 'analytics', 'scheduling'],
|
|
description: 'Filter by category (optional, defaults to all)'
|
|
},
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_workflow_guide',
|
|
description: 'Get step-by-step guidance for common VIP coordination tasks. Use this when you need to understand how to accomplish a complex task or when the user asks "how do I...?" questions.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
task: {
|
|
type: 'string',
|
|
enum: [
|
|
'schedule_airport_pickup',
|
|
'reassign_driver',
|
|
'handle_flight_delay',
|
|
'create_vip_itinerary',
|
|
'morning_briefing',
|
|
'send_driver_notifications',
|
|
'check_schedule_problems',
|
|
'vehicle_assignment',
|
|
'bulk_schedule_update'
|
|
],
|
|
description: 'The task to get guidance for'
|
|
},
|
|
},
|
|
required: ['task'],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_current_system_status',
|
|
description: 'Get a quick status overview of the entire VIP Coordinator system - counts of VIPs, drivers, vehicles, upcoming events, and any immediate issues. Use this to understand the current state of operations.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {},
|
|
required: [],
|
|
},
|
|
},
|
|
{
|
|
name: 'get_api_documentation',
|
|
description: 'Get documentation for the VIP Coordinator REST API endpoints. Use this when the user asks about API capabilities, how to integrate, or what endpoints are available. Shows all HTTP endpoints with methods, paths, and descriptions.',
|
|
input_schema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
resource: {
|
|
type: 'string',
|
|
enum: ['all', 'auth', 'users', 'vips', 'drivers', 'events', 'vehicles', 'flights', 'signal'],
|
|
description: 'Filter by resource type (optional, defaults to all)'
|
|
},
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly messagesService: MessagesService,
|
|
private readonly scheduleExportService: ScheduleExportService,
|
|
) {
|
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
if (!apiKey) {
|
|
this.logger.warn('ANTHROPIC_API_KEY not set - Copilot features will be disabled');
|
|
}
|
|
this.anthropic = new Anthropic({ apiKey: apiKey || 'dummy' });
|
|
}
|
|
|
|
async chat(
|
|
messages: ChatMessage[],
|
|
userId: string,
|
|
userRole: string,
|
|
): Promise<{ response: string; toolCalls?: any[] }> {
|
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
return { response: 'AI Copilot is not configured. Please add ANTHROPIC_API_KEY to your environment.' };
|
|
}
|
|
|
|
const systemPrompt = this.buildSystemPrompt(userRole);
|
|
|
|
try {
|
|
// Convert messages to Anthropic format
|
|
const anthropicMessages = messages.map(msg => ({
|
|
role: msg.role as 'user' | 'assistant',
|
|
content: msg.content,
|
|
}));
|
|
|
|
let response = await this.anthropic.messages.create({
|
|
model: 'claude-3-5-haiku-20241022',
|
|
max_tokens: 4096,
|
|
system: systemPrompt,
|
|
tools: this.tools,
|
|
messages: anthropicMessages,
|
|
});
|
|
|
|
// Handle tool use loop
|
|
const toolCalls: any[] = [];
|
|
while (response.stop_reason === 'tool_use') {
|
|
const toolUseBlocks = response.content.filter(
|
|
(block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
|
|
);
|
|
|
|
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
|
|
for (const toolUse of toolUseBlocks) {
|
|
this.logger.log(`Executing tool: ${toolUse.name}`);
|
|
const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, any>);
|
|
|
|
toolCalls.push({
|
|
tool: toolUse.name,
|
|
input: toolUse.input,
|
|
result: result,
|
|
});
|
|
|
|
toolResults.push({
|
|
type: 'tool_result',
|
|
tool_use_id: toolUse.id,
|
|
content: JSON.stringify(result),
|
|
});
|
|
}
|
|
|
|
// Continue conversation with tool results
|
|
response = await this.anthropic.messages.create({
|
|
model: 'claude-3-5-haiku-20241022',
|
|
max_tokens: 4096,
|
|
system: systemPrompt,
|
|
tools: this.tools,
|
|
messages: [
|
|
...anthropicMessages,
|
|
{ role: 'assistant', content: response.content },
|
|
{ role: 'user', content: toolResults },
|
|
],
|
|
});
|
|
}
|
|
|
|
// Extract final text response
|
|
const textBlock = response.content.find(
|
|
(block): block is Anthropic.TextBlock => block.type === 'text'
|
|
);
|
|
|
|
return {
|
|
response: textBlock?.text || 'I apologize, but I was unable to generate a response.',
|
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error('Claude API error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private buildSystemPrompt(userRole: string): string {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
return `You are an AI administrative assistant for VIP Coordinator, a transportation and event logistics application. You help coordinators manage VIP guests, drivers, vehicles, and events.
|
|
|
|
Current date: ${today}
|
|
User role: ${userRole}
|
|
|
|
## Your capabilities:
|
|
- Search and retrieve information about VIPs, drivers, vehicles, events, and flights
|
|
- CREATE new VIPs, events, and flights
|
|
- UPDATE existing events, flights, VIP info, and driver assignments
|
|
- DELETE events and flights that are no longer needed
|
|
- Assign/reassign drivers and vehicles to events
|
|
- Check for scheduling conflicts and identify gaps
|
|
- Get VIP itineraries and driver manifests
|
|
- **BULK REASSIGN** events from one driver to another by NAME
|
|
- **SEND MESSAGES** to drivers via Signal
|
|
- **SEND SCHEDULES** to drivers (PDF/ICS via Signal)
|
|
- **FIND AVAILABLE DRIVERS** for specific time ranges
|
|
- **SUGGEST VEHICLES** based on capacity and availability
|
|
- **AUDIT SCHEDULES** for conflicts, unassigned events, and capacity issues
|
|
- **WORKLOAD ANALYSIS** for driver utilization
|
|
|
|
## IMPORTANT: Use the right tool for the job
|
|
- To MODIFY an existing event → use update_event (don't create a new one)
|
|
- To REMOVE an event → use delete_event
|
|
- To CHANGE a flight → use update_flight
|
|
- To ADD a flight → use create_flight
|
|
- To ASSIGN a driver → use assign_driver_to_event
|
|
- To ASSIGN a vehicle → use assign_vehicle_to_event
|
|
- **To REASSIGN all events from one driver to another** → use reassign_driver_events (works by NAME, no IDs needed!)
|
|
- **To SEND a message to a driver** → use send_driver_notification_via_signal
|
|
- **To SEND schedules to drivers** → use bulk_send_driver_schedules
|
|
- **To FIND available drivers** → use find_available_drivers_for_timerange
|
|
- **To GET a driver's daily schedule** → use get_daily_driver_manifest
|
|
- **To FIND unassigned events** → use find_unassigned_events
|
|
- **To CHECK for VIP conflicts** → use check_vip_conflicts
|
|
- **To AUDIT the schedule** → use identify_scheduling_gaps
|
|
- **To SUGGEST vehicles** → use suggest_vehicle_for_event
|
|
- **To GET vehicle schedule** → use get_vehicle_schedule
|
|
- **To ANALYZE workload** → use get_driver_workload_summary
|
|
|
|
## CRITICAL: Never ask for IDs - use names!
|
|
- You can search for drivers, VIPs, vehicles, and events by NAME
|
|
- Most tools accept both name and ID parameters - prefer names
|
|
- If a search returns no results, use list_all_drivers to see available driver names
|
|
- Always try searching before telling the user you can't find something
|
|
|
|
## For actions that MODIFY data (create, update, delete):
|
|
1. First, search to find the relevant records (use names, not IDs)
|
|
2. Clearly state what changes you're proposing
|
|
3. Ask for confirmation before executing
|
|
4. After execution, show a summary of what was changed
|
|
|
|
## When reassigning a driver's events (driver sick, swapping schedules, etc.):
|
|
1. Use reassign_driver_events with the FROM and TO driver names
|
|
2. Optionally specify a date to limit the reassignment
|
|
3. The tool handles everything - no need to search for IDs first
|
|
|
|
## When sending messages to drivers:
|
|
1. Use send_driver_notification_via_signal with the driver's name
|
|
2. Keep messages clear and actionable
|
|
3. For bulk schedule sending, use bulk_send_driver_schedules
|
|
|
|
## When processing images (screenshots of emails, itineraries, etc.):
|
|
1. Carefully read and extract all relevant information
|
|
2. Identify the VIP, flight number, and any changed times
|
|
3. Search for the matching VIP and flight in the system
|
|
4. Propose the necessary updates and ask for confirmation
|
|
|
|
## Response style:
|
|
- Be concise but thorough
|
|
- Use markdown formatting for readability
|
|
- DON'T ask for IDs - use names and search tools to find records
|
|
- If you can't find a record, list available options (use list_all_drivers, etc.)
|
|
- Always confirm successful actions with a brief summary`;
|
|
}
|
|
|
|
private async executeTool(name: string, input: Record<string, any>): Promise<ToolResult> {
|
|
try {
|
|
switch (name) {
|
|
case 'search_vips':
|
|
return await this.searchVips(input);
|
|
case 'get_vip_details':
|
|
return await this.getVipDetails(input.vipId);
|
|
case 'search_drivers':
|
|
return await this.searchDrivers(input);
|
|
case 'get_driver_schedule':
|
|
return await this.getDriverSchedule(input.driverId, input.startDate, input.endDate);
|
|
case 'search_events':
|
|
return await this.searchEvents(input);
|
|
case 'get_available_vehicles':
|
|
return await this.getAvailableVehicles(input);
|
|
case 'get_flights_for_vip':
|
|
return await this.getFlightsForVip(input.vipId);
|
|
case 'update_flight':
|
|
return await this.updateFlight(input);
|
|
case 'create_event':
|
|
return await this.createEvent(input);
|
|
case 'assign_driver_to_event':
|
|
return await this.assignDriverToEvent(input.eventId, input.driverId);
|
|
case 'update_event':
|
|
return await this.updateEvent(input);
|
|
case 'delete_event':
|
|
return await this.deleteEvent(input.eventId);
|
|
case 'get_todays_summary':
|
|
return await this.getTodaysSummary();
|
|
case 'create_flight':
|
|
return await this.createFlight(input);
|
|
case 'delete_flight':
|
|
return await this.deleteFlight(input.flightId);
|
|
case 'create_vip':
|
|
return await this.createVip(input);
|
|
case 'update_vip':
|
|
return await this.updateVip(input);
|
|
case 'assign_vehicle_to_event':
|
|
return await this.assignVehicleToEvent(input.eventId, input.vehicleId);
|
|
case 'update_driver':
|
|
return await this.updateDriver(input);
|
|
case 'check_driver_conflicts':
|
|
return await this.checkDriverConflicts(input);
|
|
case 'get_vip_itinerary':
|
|
return await this.getVipItinerary(input);
|
|
case 'reassign_driver_events':
|
|
return await this.reassignDriverEvents(input);
|
|
case 'list_all_drivers':
|
|
return await this.listAllDrivers(input);
|
|
case 'find_available_drivers_for_timerange':
|
|
return await this.findAvailableDriversForTimerange(input);
|
|
case 'get_daily_driver_manifest':
|
|
return await this.getDailyDriverManifest(input);
|
|
case 'send_driver_notification_via_signal':
|
|
return await this.sendDriverNotificationViaSignal(input);
|
|
case 'bulk_send_driver_schedules':
|
|
return await this.bulkSendDriverSchedules(input);
|
|
case 'find_unassigned_events':
|
|
return await this.findUnassignedEvents(input);
|
|
case 'check_vip_conflicts':
|
|
return await this.checkVipConflicts(input);
|
|
case 'get_weekly_lookahead':
|
|
return await this.getWeeklyLookahead(input);
|
|
case 'identify_scheduling_gaps':
|
|
return await this.identifySchedulingGaps(input);
|
|
case 'suggest_vehicle_for_event':
|
|
return await this.suggestVehicleForEvent(input);
|
|
case 'get_vehicle_schedule':
|
|
return await this.getVehicleSchedule(input);
|
|
case 'get_driver_workload_summary':
|
|
return await this.getDriverWorkloadSummary(input);
|
|
case 'get_my_capabilities':
|
|
return await this.getMyCapabilities(input);
|
|
case 'get_workflow_guide':
|
|
return await this.getWorkflowGuide(input);
|
|
case 'get_current_system_status':
|
|
return await this.getCurrentSystemStatus();
|
|
case 'get_api_documentation':
|
|
return await this.getApiDocumentation(input);
|
|
default:
|
|
return { success: false, error: `Unknown tool: ${name}` };
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(`Tool execution error (${name}):`, error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// Tool implementations
|
|
private async searchVips(filters: Record<string, any>): Promise<ToolResult> {
|
|
const where: any = { deletedAt: null };
|
|
|
|
if (filters.name) {
|
|
where.name = { contains: filters.name, mode: 'insensitive' };
|
|
}
|
|
if (filters.organization) {
|
|
where.organization = { contains: filters.organization, mode: 'insensitive' };
|
|
}
|
|
if (filters.department) {
|
|
where.department = filters.department;
|
|
}
|
|
if (filters.arrivalMode) {
|
|
where.arrivalMode = filters.arrivalMode;
|
|
}
|
|
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where,
|
|
include: {
|
|
flights: true,
|
|
},
|
|
take: 20,
|
|
});
|
|
|
|
// Fetch events for these VIPs
|
|
const vipIds = vips.map(v => v.id);
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
vipIds: { hasSome: vipIds },
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Attach events to VIPs
|
|
const vipsWithEvents = vips.map(vip => ({
|
|
...vip,
|
|
events: events.filter(e => e.vipIds.includes(vip.id)).slice(0, 5),
|
|
}));
|
|
|
|
return { success: true, data: vipsWithEvents };
|
|
}
|
|
|
|
private async getVipDetails(vipId: string): Promise<ToolResult> {
|
|
const vip = await this.prisma.vIP.findUnique({
|
|
where: { id: vipId },
|
|
include: {
|
|
flights: true,
|
|
},
|
|
});
|
|
|
|
if (!vip) {
|
|
return { success: false, error: 'VIP not found' };
|
|
}
|
|
|
|
// Fetch events for this VIP
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
vipIds: { has: vipId },
|
|
},
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
return { success: true, data: { ...vip, events } };
|
|
}
|
|
|
|
private async searchDrivers(filters: Record<string, any>): Promise<ToolResult> {
|
|
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,
|
|
include: {
|
|
events: {
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: new Date() },
|
|
},
|
|
take: 5,
|
|
},
|
|
},
|
|
take: 20,
|
|
});
|
|
|
|
return { success: true, data: drivers };
|
|
}
|
|
|
|
private async getDriverSchedule(
|
|
driverId: string,
|
|
startDate?: string,
|
|
endDate?: string,
|
|
): Promise<ToolResult> {
|
|
const start = startDate ? new Date(startDate) : new Date();
|
|
const end = endDate ? new Date(endDate) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
|
|
const driver = await this.prisma.driver.findUnique({
|
|
where: { id: driverId },
|
|
include: {
|
|
events: {
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: start, lte: end },
|
|
},
|
|
include: {
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!driver) {
|
|
return { success: false, error: 'Driver not found' };
|
|
}
|
|
|
|
// Fetch VIP names for events
|
|
const allVipIds = driver.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 = driver.events.map(event => ({
|
|
...event,
|
|
vipNames: event.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
}));
|
|
|
|
return { success: true, data: { ...driver, events: eventsWithVipNames } };
|
|
}
|
|
|
|
private async searchEvents(filters: Record<string, any>): Promise<ToolResult> {
|
|
const where: any = { deletedAt: null };
|
|
|
|
// If searching by VIP name, first find matching VIPs
|
|
let vipIdsFromName: string[] = [];
|
|
if (filters.vipName) {
|
|
const matchingVips = await this.prisma.vIP.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: filters.vipName, mode: 'insensitive' },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
vipIdsFromName = matchingVips.map(v => v.id);
|
|
if (vipIdsFromName.length === 0) {
|
|
return { success: true, data: [], message: `No VIPs found matching "${filters.vipName}"` };
|
|
}
|
|
}
|
|
|
|
// If searching by driver name, first find matching drivers
|
|
let driverIdsFromName: string[] = [];
|
|
if (filters.driverName) {
|
|
const matchingDrivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: filters.driverName, mode: 'insensitive' },
|
|
},
|
|
select: { id: true, name: true },
|
|
});
|
|
driverIdsFromName = matchingDrivers.map(d => d.id);
|
|
if (driverIdsFromName.length === 0) {
|
|
// List all drivers to help the user find the right name
|
|
const allDrivers = await this.prisma.driver.findMany({
|
|
where: { deletedAt: null },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return {
|
|
success: true,
|
|
data: [],
|
|
message: `No drivers found matching "${filters.driverName}". Available drivers: ${allDrivers.map(d => d.name).join(', ')}`
|
|
};
|
|
}
|
|
}
|
|
|
|
if (filters.vipId) {
|
|
where.vipIds = { has: filters.vipId };
|
|
} else if (vipIdsFromName.length > 0) {
|
|
where.vipIds = { hasSome: vipIdsFromName };
|
|
}
|
|
if (filters.title) {
|
|
where.title = { contains: filters.title, mode: 'insensitive' };
|
|
}
|
|
if (filters.driverId) {
|
|
where.driverId = filters.driverId;
|
|
} else if (driverIdsFromName.length > 0) {
|
|
where.driverId = { in: driverIdsFromName };
|
|
}
|
|
if (filters.status) {
|
|
where.status = filters.status;
|
|
}
|
|
if (filters.type) {
|
|
where.type = filters.type;
|
|
}
|
|
if (filters.date) {
|
|
const date = new Date(filters.date);
|
|
const nextDay = new Date(date);
|
|
nextDay.setDate(nextDay.getDate() + 1);
|
|
where.startTime = { gte: date, lt: nextDay };
|
|
}
|
|
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where,
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
take: 50,
|
|
});
|
|
|
|
// Fetch VIP names for 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: eventsWithVipNames };
|
|
}
|
|
|
|
private async getAvailableVehicles(filters: Record<string, any>): Promise<ToolResult> {
|
|
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: { seatCapacity: 'desc' },
|
|
});
|
|
|
|
return { success: true, data: vehicles };
|
|
}
|
|
|
|
private async getFlightsForVip(vipId: string): Promise<ToolResult> {
|
|
const flights = await this.prisma.flight.findMany({
|
|
where: { vipId },
|
|
orderBy: { flightDate: 'asc' },
|
|
});
|
|
|
|
return { success: true, data: flights };
|
|
}
|
|
|
|
private async updateFlight(input: Record<string, any>): Promise<ToolResult> {
|
|
const { flightId, ...updateData } = input;
|
|
|
|
const flight = await this.prisma.flight.update({
|
|
where: { id: flightId },
|
|
data: updateData,
|
|
include: { vip: true },
|
|
});
|
|
|
|
return { success: true, data: flight };
|
|
}
|
|
|
|
private async createEvent(input: Record<string, any>): Promise<ToolResult> {
|
|
// Support both single vipId and array of vipIds
|
|
const vipIds = input.vipIds || (input.vipId ? [input.vipId] : []);
|
|
|
|
const event = await this.prisma.scheduleEvent.create({
|
|
data: {
|
|
vipIds,
|
|
title: input.title,
|
|
type: input.type,
|
|
startTime: new Date(input.startTime),
|
|
endTime: new Date(input.endTime),
|
|
location: input.location,
|
|
pickupLocation: input.pickupLocation,
|
|
dropoffLocation: input.dropoffLocation,
|
|
driverId: input.driverId,
|
|
vehicleId: input.vehicleId,
|
|
description: input.description,
|
|
status: 'SCHEDULED',
|
|
},
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: vipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
return { success: true, data: { ...event, vipNames: vips.map(v => v.name) } };
|
|
}
|
|
|
|
private async assignDriverToEvent(eventId: string, driverId: string): Promise<ToolResult> {
|
|
const event = await this.prisma.scheduleEvent.update({
|
|
where: { id: eventId },
|
|
data: { driverId },
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: event.vipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
return { success: true, data: { ...event, vipNames: vips.map(v => v.name) } };
|
|
}
|
|
|
|
private async updateEvent(input: Record<string, any>): Promise<ToolResult> {
|
|
const { eventId, ...updateData } = input;
|
|
|
|
// Build update object, only including fields that were provided
|
|
const data: any = {};
|
|
if (updateData.title !== undefined) data.title = updateData.title;
|
|
if (updateData.startTime !== undefined) data.startTime = new Date(updateData.startTime);
|
|
if (updateData.endTime !== undefined) data.endTime = new Date(updateData.endTime);
|
|
if (updateData.location !== undefined) data.location = updateData.location;
|
|
if (updateData.pickupLocation !== undefined) data.pickupLocation = updateData.pickupLocation;
|
|
if (updateData.dropoffLocation !== undefined) data.dropoffLocation = updateData.dropoffLocation;
|
|
if (updateData.status !== undefined) data.status = updateData.status;
|
|
if (updateData.driverId !== undefined) data.driverId = updateData.driverId === 'null' ? null : updateData.driverId;
|
|
if (updateData.vehicleId !== undefined) data.vehicleId = updateData.vehicleId === 'null' ? null : updateData.vehicleId;
|
|
if (updateData.description !== undefined) data.description = updateData.description;
|
|
|
|
const event = await this.prisma.scheduleEvent.update({
|
|
where: { id: eventId },
|
|
data,
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: event.vipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
return { success: true, data: { ...event, vipNames: vips.map(v => v.name) } };
|
|
}
|
|
|
|
private async deleteEvent(eventId: string): Promise<ToolResult> {
|
|
// First get the event to return info about what was deleted
|
|
const event = await this.prisma.scheduleEvent.findUnique({
|
|
where: { id: eventId },
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
});
|
|
|
|
if (!event) {
|
|
return { success: false, error: 'Event not found' };
|
|
}
|
|
|
|
// Soft delete
|
|
await this.prisma.scheduleEvent.update({
|
|
where: { id: eventId },
|
|
data: { deletedAt: new Date() },
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: event.vipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
deleted: true,
|
|
event: { ...event, vipNames: vips.map(v => v.name) }
|
|
}
|
|
};
|
|
}
|
|
|
|
private async getTodaysSummary(): Promise<ToolResult> {
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
const [events, arrivingVips, availableDrivers, availableVehicles] = await Promise.all([
|
|
this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: today, lt: tomorrow },
|
|
},
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
}),
|
|
this.prisma.vIP.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
OR: [
|
|
{ expectedArrival: { gte: today, lt: tomorrow } },
|
|
{
|
|
flights: {
|
|
some: {
|
|
scheduledArrival: { gte: today, lt: tomorrow },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
include: { flights: true },
|
|
}),
|
|
this.prisma.driver.findMany({
|
|
where: { deletedAt: null, isAvailable: true },
|
|
}),
|
|
this.prisma.vehicle.findMany({
|
|
where: { deletedAt: null, status: 'AVAILABLE' },
|
|
}),
|
|
]);
|
|
|
|
// Fetch VIP names for 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: {
|
|
todaysEvents: eventsWithVipNames,
|
|
arrivingVips,
|
|
availableDrivers: availableDrivers.length,
|
|
availableVehicles: availableVehicles.length,
|
|
summary: {
|
|
totalEvents: events.length,
|
|
eventsWithoutDriver: events.filter(e => !e.driverId).length,
|
|
eventsWithoutVehicle: events.filter(e => !e.vehicleId).length,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
private async createFlight(input: Record<string, any>): Promise<ToolResult> {
|
|
const flight = await this.prisma.flight.create({
|
|
data: {
|
|
vipId: input.vipId,
|
|
flightNumber: input.flightNumber,
|
|
flightDate: new Date(input.flightDate),
|
|
departureAirport: input.departureAirport,
|
|
arrivalAirport: input.arrivalAirport,
|
|
scheduledDeparture: input.scheduledDeparture ? new Date(input.scheduledDeparture) : null,
|
|
scheduledArrival: input.scheduledArrival ? new Date(input.scheduledArrival) : null,
|
|
segment: input.segment || 1,
|
|
},
|
|
include: { vip: true },
|
|
});
|
|
|
|
return { success: true, data: flight };
|
|
}
|
|
|
|
private async deleteFlight(flightId: string): Promise<ToolResult> {
|
|
const flight = await this.prisma.flight.findUnique({
|
|
where: { id: flightId },
|
|
include: { vip: true },
|
|
});
|
|
|
|
if (!flight) {
|
|
return { success: false, error: 'Flight not found' };
|
|
}
|
|
|
|
await this.prisma.flight.delete({
|
|
where: { id: flightId },
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: { deleted: true, flight },
|
|
};
|
|
}
|
|
|
|
private async createVip(input: Record<string, any>): Promise<ToolResult> {
|
|
const vip = await this.prisma.vIP.create({
|
|
data: {
|
|
name: input.name,
|
|
organization: input.organization,
|
|
department: input.department,
|
|
arrivalMode: input.arrivalMode,
|
|
expectedArrival: input.expectedArrival ? new Date(input.expectedArrival) : null,
|
|
airportPickup: input.airportPickup ?? false,
|
|
venueTransport: input.venueTransport ?? false,
|
|
notes: input.notes,
|
|
},
|
|
});
|
|
|
|
return { success: true, data: vip };
|
|
}
|
|
|
|
private async updateVip(input: Record<string, any>): Promise<ToolResult> {
|
|
const { vipId, ...updateData } = input;
|
|
|
|
const data: any = {};
|
|
if (updateData.name !== undefined) data.name = updateData.name;
|
|
if (updateData.organization !== undefined) data.organization = updateData.organization;
|
|
if (updateData.department !== undefined) data.department = updateData.department;
|
|
if (updateData.arrivalMode !== undefined) data.arrivalMode = updateData.arrivalMode;
|
|
if (updateData.expectedArrival !== undefined) data.expectedArrival = updateData.expectedArrival ? new Date(updateData.expectedArrival) : null;
|
|
if (updateData.airportPickup !== undefined) data.airportPickup = updateData.airportPickup;
|
|
if (updateData.venueTransport !== undefined) data.venueTransport = updateData.venueTransport;
|
|
if (updateData.notes !== undefined) data.notes = updateData.notes;
|
|
|
|
const vip = await this.prisma.vIP.update({
|
|
where: { id: vipId },
|
|
data,
|
|
include: { flights: true },
|
|
});
|
|
|
|
return { success: true, data: vip };
|
|
}
|
|
|
|
private async assignVehicleToEvent(eventId: string, vehicleId: string): Promise<ToolResult> {
|
|
const event = await this.prisma.scheduleEvent.update({
|
|
where: { id: eventId },
|
|
data: { vehicleId: vehicleId === 'null' ? null : vehicleId },
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
});
|
|
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: event.vipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
return { success: true, data: { ...event, vipNames: vips.map(v => v.name) } };
|
|
}
|
|
|
|
private async updateDriver(input: Record<string, any>): Promise<ToolResult> {
|
|
const { driverId, ...updateData } = input;
|
|
|
|
const data: any = {};
|
|
if (updateData.name !== undefined) data.name = updateData.name;
|
|
if (updateData.phone !== undefined) data.phone = updateData.phone;
|
|
if (updateData.department !== undefined) data.department = updateData.department;
|
|
if (updateData.isAvailable !== undefined) data.isAvailable = updateData.isAvailable;
|
|
if (updateData.shiftStartTime !== undefined) data.shiftStartTime = updateData.shiftStartTime;
|
|
if (updateData.shiftEndTime !== undefined) data.shiftEndTime = updateData.shiftEndTime;
|
|
|
|
const driver = await this.prisma.driver.update({
|
|
where: { id: driverId },
|
|
data,
|
|
});
|
|
|
|
return { success: true, data: driver };
|
|
}
|
|
|
|
private async checkDriverConflicts(input: Record<string, any>): Promise<ToolResult> {
|
|
const { driverId, startTime, endTime, excludeEventId } = input;
|
|
|
|
const start = new Date(startTime);
|
|
const end = new Date(endTime);
|
|
|
|
const where: any = {
|
|
deletedAt: null,
|
|
driverId,
|
|
status: { not: 'CANCELLED' },
|
|
OR: [
|
|
// Event starts during the time period
|
|
{ startTime: { gte: start, lt: end } },
|
|
// Event ends during the time period
|
|
{ endTime: { gt: start, lte: end } },
|
|
// Event spans the entire time period
|
|
{ AND: [{ startTime: { lte: start } }, { endTime: { gte: end } }] },
|
|
],
|
|
};
|
|
|
|
if (excludeEventId) {
|
|
where.id = { not: excludeEventId };
|
|
}
|
|
|
|
const conflictingEvents = await this.prisma.scheduleEvent.findMany({
|
|
where,
|
|
include: { vehicle: true },
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const allVipIds = conflictingEvents.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 = conflictingEvents.map(event => ({
|
|
...event,
|
|
vipNames: event.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
hasConflicts: conflictingEvents.length > 0,
|
|
conflictCount: conflictingEvents.length,
|
|
conflicts: eventsWithVipNames,
|
|
},
|
|
};
|
|
}
|
|
|
|
private async getVipItinerary(input: Record<string, any>): Promise<ToolResult> {
|
|
const { vipId, startDate, endDate } = input;
|
|
|
|
const vip = await this.prisma.vIP.findUnique({
|
|
where: { id: vipId },
|
|
});
|
|
|
|
if (!vip) {
|
|
return { success: false, error: 'VIP not found' };
|
|
}
|
|
|
|
// Build date filters
|
|
const dateFilter: any = {};
|
|
if (startDate) dateFilter.gte = new Date(startDate);
|
|
if (endDate) dateFilter.lte = new Date(endDate);
|
|
|
|
// Get flights
|
|
const flightsWhere: any = { vipId };
|
|
if (startDate || endDate) {
|
|
flightsWhere.flightDate = dateFilter;
|
|
}
|
|
const flights = await this.prisma.flight.findMany({
|
|
where: flightsWhere,
|
|
orderBy: { scheduledDeparture: 'asc' },
|
|
});
|
|
|
|
// Get events
|
|
const eventsWhere: any = {
|
|
deletedAt: null,
|
|
vipIds: { has: vipId },
|
|
};
|
|
if (startDate || endDate) {
|
|
eventsWhere.startTime = dateFilter;
|
|
}
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: eventsWhere,
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Combine and sort chronologically
|
|
const itineraryItems: any[] = [
|
|
...flights.map(f => ({
|
|
type: 'FLIGHT',
|
|
time: f.scheduledDeparture || f.flightDate,
|
|
data: f,
|
|
})),
|
|
...events.map(e => ({
|
|
type: 'EVENT',
|
|
time: e.startTime,
|
|
data: e,
|
|
})),
|
|
].sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
vip,
|
|
itinerary: itineraryItems,
|
|
summary: {
|
|
totalFlights: flights.length,
|
|
totalEvents: events.length,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
private async reassignDriverEvents(input: Record<string, any>): Promise<ToolResult> {
|
|
const { fromDriverName, toDriverName, date, onlyStatus } = input;
|
|
|
|
// Find the source driver by name
|
|
const fromDrivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: fromDriverName, mode: 'insensitive' },
|
|
},
|
|
});
|
|
|
|
if (fromDrivers.length === 0) {
|
|
const allDrivers = await this.prisma.driver.findMany({
|
|
where: { deletedAt: null },
|
|
select: { name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return {
|
|
success: false,
|
|
error: `No driver found matching "${fromDriverName}". Available drivers: ${allDrivers.map(d => d.name).join(', ')}`
|
|
};
|
|
}
|
|
|
|
if (fromDrivers.length > 1) {
|
|
return {
|
|
success: false,
|
|
error: `Multiple drivers match "${fromDriverName}": ${fromDrivers.map(d => d.name).join(', ')}. Please be more specific.`
|
|
};
|
|
}
|
|
|
|
const fromDriver = fromDrivers[0];
|
|
|
|
// Find the target driver by name
|
|
const toDrivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: toDriverName, mode: 'insensitive' },
|
|
},
|
|
});
|
|
|
|
if (toDrivers.length === 0) {
|
|
const allDrivers = await this.prisma.driver.findMany({
|
|
where: { deletedAt: null },
|
|
select: { name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return {
|
|
success: false,
|
|
error: `No driver found matching "${toDriverName}". Available drivers: ${allDrivers.map(d => d.name).join(', ')}`
|
|
};
|
|
}
|
|
|
|
if (toDrivers.length > 1) {
|
|
return {
|
|
success: false,
|
|
error: `Multiple drivers match "${toDriverName}": ${toDrivers.map(d => d.name).join(', ')}. Please be more specific.`
|
|
};
|
|
}
|
|
|
|
const toDriver = toDrivers[0];
|
|
|
|
// Build query for events to reassign
|
|
const where: any = {
|
|
deletedAt: null,
|
|
driverId: fromDriver.id,
|
|
status: { not: 'CANCELLED' },
|
|
};
|
|
|
|
// Filter by date if provided
|
|
if (date) {
|
|
const targetDate = new Date(date);
|
|
const nextDay = new Date(targetDate);
|
|
nextDay.setDate(nextDay.getDate() + 1);
|
|
where.startTime = { gte: targetDate, lt: nextDay };
|
|
} else {
|
|
// Only future events by default
|
|
where.startTime = { gte: new Date() };
|
|
}
|
|
|
|
if (onlyStatus) {
|
|
where.status = onlyStatus;
|
|
}
|
|
|
|
// Get events to reassign
|
|
const eventsToReassign = await this.prisma.scheduleEvent.findMany({
|
|
where,
|
|
include: { vehicle: true },
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
if (eventsToReassign.length === 0) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
reassigned: 0,
|
|
fromDriver: fromDriver.name,
|
|
toDriver: toDriver.name,
|
|
},
|
|
message: `No events found for ${fromDriver.name} to reassign.`
|
|
};
|
|
}
|
|
|
|
// Reassign all events
|
|
await this.prisma.scheduleEvent.updateMany({
|
|
where: { id: { in: eventsToReassign.map(e => e.id) } },
|
|
data: { driverId: toDriver.id },
|
|
});
|
|
|
|
// Fetch VIP names for the events
|
|
const allVipIds = eventsToReassign.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 eventsWithDetails = eventsToReassign.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
startTime: event.startTime,
|
|
endTime: event.endTime,
|
|
vipNames: event.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
reassigned: eventsToReassign.length,
|
|
fromDriver: fromDriver.name,
|
|
toDriver: toDriver.name,
|
|
events: eventsWithDetails,
|
|
},
|
|
message: `Successfully reassigned ${eventsToReassign.length} event(s) from ${fromDriver.name} to ${toDriver.name}.`
|
|
};
|
|
}
|
|
|
|
private async listAllDrivers(input: Record<string, any>): Promise<ToolResult> {
|
|
const includeUnavailable = input.includeUnavailable !== false;
|
|
|
|
const where: any = { deletedAt: null };
|
|
if (!includeUnavailable) {
|
|
where.isAvailable = true;
|
|
}
|
|
|
|
const drivers = await this.prisma.driver.findMany({
|
|
where,
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
phone: true,
|
|
department: true,
|
|
isAvailable: true,
|
|
_count: {
|
|
select: {
|
|
events: {
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: new Date() },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
|
|
const driversWithInfo = drivers.map(d => ({
|
|
id: d.id,
|
|
name: d.name,
|
|
phone: d.phone,
|
|
department: d.department,
|
|
isAvailable: d.isAvailable,
|
|
upcomingEventCount: d._count.events,
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
data: driversWithInfo,
|
|
message: `Found ${drivers.length} driver(s).`
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// NEW TOOLS - HIGH PRIORITY (5)
|
|
// ============================================
|
|
|
|
private async findAvailableDriversForTimerange(input: Record<string, any>): Promise<ToolResult> {
|
|
const { startTime, endTime, preferredDepartment } = input;
|
|
|
|
const start = new Date(startTime);
|
|
const end = new Date(endTime);
|
|
|
|
// Get all drivers
|
|
const where: any = { deletedAt: null, isAvailable: true };
|
|
if (preferredDepartment) {
|
|
where.department = preferredDepartment;
|
|
}
|
|
|
|
const drivers = await this.prisma.driver.findMany({
|
|
where,
|
|
include: {
|
|
events: {
|
|
where: {
|
|
deletedAt: null,
|
|
status: { not: 'CANCELLED' },
|
|
OR: [
|
|
{ startTime: { gte: start, lt: end } },
|
|
{ endTime: { gt: start, lte: end } },
|
|
{ AND: [{ startTime: { lte: start } }, { endTime: { gte: end } }] },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Filter to only drivers with no conflicting events
|
|
const availableDrivers = drivers
|
|
.filter(d => d.events.length === 0)
|
|
.map(d => ({
|
|
id: d.id,
|
|
name: d.name,
|
|
phone: d.phone,
|
|
department: d.department,
|
|
shiftStartTime: d.shiftStartTime,
|
|
shiftEndTime: d.shiftEndTime,
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
data: availableDrivers,
|
|
message: `Found ${availableDrivers.length} available driver(s) for ${start.toLocaleString()} - ${end.toLocaleString()}`,
|
|
};
|
|
}
|
|
|
|
private async getDailyDriverManifest(input: Record<string, any>): Promise<ToolResult> {
|
|
const { driverName, driverId, date } = input;
|
|
|
|
// Find driver by name or ID
|
|
let driver;
|
|
if (driverId) {
|
|
driver = await this.prisma.driver.findUnique({ where: { id: driverId } });
|
|
} else if (driverName) {
|
|
const drivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: driverName, mode: 'insensitive' },
|
|
},
|
|
});
|
|
|
|
if (drivers.length === 0) {
|
|
const allDrivers = await this.prisma.driver.findMany({
|
|
where: { deletedAt: null },
|
|
select: { name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return {
|
|
success: false,
|
|
error: `No driver found matching "${driverName}". Available drivers: ${allDrivers.map(d => d.name).join(', ')}`,
|
|
};
|
|
}
|
|
|
|
if (drivers.length > 1) {
|
|
return {
|
|
success: false,
|
|
error: `Multiple drivers match "${driverName}": ${drivers.map(d => d.name).join(', ')}. Please be more specific.`,
|
|
};
|
|
}
|
|
|
|
driver = drivers[0];
|
|
} else {
|
|
return { success: false, error: 'Either driverName or driverId is required' };
|
|
}
|
|
|
|
if (!driver) {
|
|
return { success: false, error: 'Driver not found' };
|
|
}
|
|
|
|
// Parse date or default to today
|
|
const targetDate = date ? new Date(date) : new Date();
|
|
const startOfDay = new Date(targetDate);
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
const endOfDay = new Date(targetDate);
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
|
|
// Get events for this day
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
driverId: driver.id,
|
|
deletedAt: null,
|
|
startTime: { gte: startOfDay, lte: endOfDay },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
include: {
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const allVipIds = [...new Set(events.flatMap(e => e.vipIds))];
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: allVipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
const vipMap = new Map(vips.map(v => [v.id, v.name]));
|
|
|
|
// Build manifest with gaps
|
|
const manifest = events.map((event, index) => {
|
|
const eventData: any = {
|
|
id: event.id,
|
|
title: event.title,
|
|
type: event.type,
|
|
status: event.status,
|
|
startTime: event.startTime,
|
|
endTime: event.endTime,
|
|
vipNames: event.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
pickupLocation: event.pickupLocation,
|
|
dropoffLocation: event.dropoffLocation,
|
|
location: event.location,
|
|
vehicle: event.vehicle ? {
|
|
name: event.vehicle.name,
|
|
licensePlate: event.vehicle.licensePlate,
|
|
type: event.vehicle.type,
|
|
seatCapacity: event.vehicle.seatCapacity,
|
|
} : null,
|
|
notes: event.notes,
|
|
};
|
|
|
|
// Calculate gap to next event
|
|
if (index < events.length - 1) {
|
|
const nextEvent = events[index + 1];
|
|
const gapMinutes = Math.round(
|
|
(nextEvent.startTime.getTime() - event.endTime.getTime()) / 60000
|
|
);
|
|
eventData.gapToNextEvent = {
|
|
minutes: gapMinutes,
|
|
formatted: `${Math.floor(gapMinutes / 60)}h ${gapMinutes % 60}m`,
|
|
};
|
|
}
|
|
|
|
return eventData;
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
driver: {
|
|
id: driver.id,
|
|
name: driver.name,
|
|
phone: driver.phone,
|
|
department: driver.department,
|
|
shiftStartTime: driver.shiftStartTime,
|
|
shiftEndTime: driver.shiftEndTime,
|
|
},
|
|
date: targetDate.toISOString().split('T')[0],
|
|
eventCount: events.length,
|
|
manifest,
|
|
},
|
|
message: `Manifest for ${driver.name} on ${targetDate.toLocaleDateString()}: ${events.length} event(s)`,
|
|
};
|
|
}
|
|
|
|
private async sendDriverNotificationViaSignal(input: Record<string, any>): Promise<ToolResult> {
|
|
const { driverName, driverId, message, relatedEventId } = input;
|
|
|
|
// Find driver by name or ID
|
|
let driver;
|
|
if (driverId) {
|
|
driver = await this.prisma.driver.findUnique({ where: { id: driverId } });
|
|
} else if (driverName) {
|
|
const drivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: driverName, mode: 'insensitive' },
|
|
},
|
|
});
|
|
|
|
if (drivers.length === 0) {
|
|
const allDrivers = await this.prisma.driver.findMany({
|
|
where: { deletedAt: null },
|
|
select: { name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return {
|
|
success: false,
|
|
error: `No driver found matching "${driverName}". Available drivers: ${allDrivers.map(d => d.name).join(', ')}`,
|
|
};
|
|
}
|
|
|
|
if (drivers.length > 1) {
|
|
return {
|
|
success: false,
|
|
error: `Multiple drivers match "${driverName}": ${drivers.map(d => d.name).join(', ')}. Please be more specific.`,
|
|
};
|
|
}
|
|
|
|
driver = drivers[0];
|
|
} else {
|
|
return { success: false, error: 'Either driverName or driverId is required' };
|
|
}
|
|
|
|
if (!driver) {
|
|
return { success: false, error: 'Driver not found' };
|
|
}
|
|
|
|
// Send message via MessagesService
|
|
try {
|
|
const result = await this.messagesService.sendMessage({
|
|
driverId: driver.id,
|
|
content: message,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
messageId: result.id,
|
|
driverId: driver.id,
|
|
driverName: driver.name,
|
|
messageSent: true,
|
|
timestamp: result.timestamp,
|
|
relatedEventId,
|
|
},
|
|
message: `Message sent to ${driver.name} via Signal`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to send message: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
private async bulkSendDriverSchedules(input: Record<string, any>): Promise<ToolResult> {
|
|
const { date, driverNames } = input;
|
|
|
|
const targetDate = new Date(date);
|
|
const startOfDay = new Date(targetDate);
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
const endOfDay = new Date(targetDate);
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
|
|
// Get all drivers with events on this date
|
|
const eventsOnDate = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: startOfDay, lte: endOfDay },
|
|
status: { not: 'CANCELLED' },
|
|
driverId: { not: null },
|
|
},
|
|
select: { driverId: true },
|
|
});
|
|
|
|
const driverIdsWithEvents = [...new Set(eventsOnDate.map(e => e.driverId).filter((id): id is string => id !== null))];
|
|
|
|
// Filter by names if provided
|
|
let targetDrivers;
|
|
if (driverNames && driverNames.length > 0) {
|
|
targetDrivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
id: { in: driverIdsWithEvents },
|
|
name: { in: driverNames },
|
|
},
|
|
});
|
|
} else {
|
|
targetDrivers = await this.prisma.driver.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
id: { in: driverIdsWithEvents },
|
|
},
|
|
});
|
|
}
|
|
|
|
const results = [];
|
|
const errors = [];
|
|
|
|
for (const driver of targetDrivers) {
|
|
try {
|
|
const result = await this.scheduleExportService.sendScheduleToDriver(
|
|
driver.id,
|
|
targetDate,
|
|
'both',
|
|
);
|
|
results.push({
|
|
driverId: driver.id,
|
|
driverName: driver.name,
|
|
success: result.success,
|
|
message: result.message,
|
|
});
|
|
} catch (error) {
|
|
errors.push({
|
|
driverId: driver.id,
|
|
driverName: driver.name,
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
date: targetDate.toISOString().split('T')[0],
|
|
totalDrivers: targetDrivers.length,
|
|
successful: results.length,
|
|
failed: errors.length,
|
|
results,
|
|
errors,
|
|
},
|
|
message: `Sent schedules to ${results.length} driver(s) for ${targetDate.toLocaleDateString()}. ${errors.length} failed.`,
|
|
};
|
|
}
|
|
|
|
private async findUnassignedEvents(input: Record<string, any>): Promise<ToolResult> {
|
|
const { startDate, endDate, missingDriver = true, missingVehicle = true } = input;
|
|
|
|
const start = new Date(startDate);
|
|
const end = new Date(endDate);
|
|
|
|
const where: any = {
|
|
deletedAt: null,
|
|
status: { not: 'CANCELLED' },
|
|
startTime: { gte: start, lte: end },
|
|
OR: [],
|
|
};
|
|
|
|
if (missingDriver) {
|
|
where.OR.push({ driverId: null });
|
|
}
|
|
if (missingVehicle) {
|
|
where.OR.push({ vehicleId: null });
|
|
}
|
|
|
|
if (where.OR.length === 0) {
|
|
return {
|
|
success: false,
|
|
error: 'At least one of missingDriver or missingVehicle must be true',
|
|
};
|
|
}
|
|
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where,
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const allVipIds = [...new Set(events.flatMap(e => e.vipIds))];
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: allVipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
const vipMap = new Map(vips.map(v => [v.id, v.name]));
|
|
|
|
const eventsWithDetails = events.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
type: event.type,
|
|
startTime: event.startTime,
|
|
endTime: event.endTime,
|
|
vipNames: event.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
driverId: event.driverId,
|
|
driverName: event.driver?.name || null,
|
|
vehicleId: event.vehicleId,
|
|
vehicleName: event.vehicle?.name || null,
|
|
missingDriver: !event.driverId,
|
|
missingVehicle: !event.vehicleId,
|
|
location: event.location,
|
|
pickupLocation: event.pickupLocation,
|
|
dropoffLocation: event.dropoffLocation,
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
totalUnassigned: events.length,
|
|
missingDriverCount: eventsWithDetails.filter(e => e.missingDriver).length,
|
|
missingVehicleCount: eventsWithDetails.filter(e => e.missingVehicle).length,
|
|
events: eventsWithDetails,
|
|
},
|
|
message: `Found ${events.length} event(s) needing attention between ${start.toLocaleDateString()} and ${end.toLocaleDateString()}`,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// NEW TOOLS - MEDIUM PRIORITY (6)
|
|
// ============================================
|
|
|
|
private async checkVipConflicts(input: Record<string, any>): Promise<ToolResult> {
|
|
const { vipName, vipId, startTime, endTime, excludeEventId } = input;
|
|
|
|
// Find VIP by name or ID
|
|
let vip;
|
|
if (vipId) {
|
|
vip = await this.prisma.vIP.findUnique({ where: { id: vipId } });
|
|
} else if (vipName) {
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: vipName, mode: 'insensitive' },
|
|
},
|
|
});
|
|
|
|
if (vips.length === 0) {
|
|
return {
|
|
success: false,
|
|
error: `No VIP found matching "${vipName}"`,
|
|
};
|
|
}
|
|
|
|
if (vips.length > 1) {
|
|
return {
|
|
success: false,
|
|
error: `Multiple VIPs match "${vipName}": ${vips.map(v => v.name).join(', ')}. Please be more specific.`,
|
|
};
|
|
}
|
|
|
|
vip = vips[0];
|
|
} else {
|
|
return { success: false, error: 'Either vipName or vipId is required' };
|
|
}
|
|
|
|
if (!vip) {
|
|
return { success: false, error: 'VIP not found' };
|
|
}
|
|
|
|
const start = new Date(startTime);
|
|
const end = new Date(endTime);
|
|
|
|
const where: any = {
|
|
deletedAt: null,
|
|
vipIds: { has: vip.id },
|
|
status: { not: 'CANCELLED' },
|
|
OR: [
|
|
{ startTime: { gte: start, lt: end } },
|
|
{ endTime: { gt: start, lte: end } },
|
|
{ AND: [{ startTime: { lte: start } }, { endTime: { gte: end } }] },
|
|
],
|
|
};
|
|
|
|
if (excludeEventId) {
|
|
where.id = { not: excludeEventId };
|
|
}
|
|
|
|
const conflictingEvents = await this.prisma.scheduleEvent.findMany({
|
|
where,
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
vip: {
|
|
id: vip.id,
|
|
name: vip.name,
|
|
},
|
|
hasConflicts: conflictingEvents.length > 0,
|
|
conflictCount: conflictingEvents.length,
|
|
conflicts: conflictingEvents.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
startTime: event.startTime,
|
|
endTime: event.endTime,
|
|
driver: event.driver?.name || null,
|
|
vehicle: event.vehicle?.name || null,
|
|
})),
|
|
},
|
|
message: conflictingEvents.length > 0
|
|
? `VIP ${vip.name} has ${conflictingEvents.length} conflicting event(s)`
|
|
: `No conflicts found for VIP ${vip.name}`,
|
|
};
|
|
}
|
|
|
|
private async getWeeklyLookahead(input: Record<string, any>): Promise<ToolResult> {
|
|
const { startDate, weeksAhead = 1 } = input;
|
|
|
|
const start = startDate ? new Date(startDate) : new Date();
|
|
start.setHours(0, 0, 0, 0);
|
|
|
|
const end = new Date(start);
|
|
end.setDate(end.getDate() + (weeksAhead * 7));
|
|
|
|
// Get all events in range
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: start, lt: end },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Get VIP arrivals
|
|
const vipArrivals = await this.prisma.vIP.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
OR: [
|
|
{ expectedArrival: { gte: start, lt: end } },
|
|
{
|
|
flights: {
|
|
some: {
|
|
scheduledArrival: { gte: start, lt: end },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
include: {
|
|
flights: {
|
|
where: {
|
|
scheduledArrival: { gte: start, lt: end },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Group by day
|
|
const dayMap = new Map<string, any>();
|
|
|
|
for (let d = new Date(start); d < end; d.setDate(d.getDate() + 1)) {
|
|
const dateKey = d.toISOString().split('T')[0];
|
|
const dayStart = new Date(d);
|
|
dayStart.setHours(0, 0, 0, 0);
|
|
const dayEnd = new Date(d);
|
|
dayEnd.setHours(23, 59, 59, 999);
|
|
|
|
const dayEvents = events.filter(
|
|
e => e.startTime >= dayStart && e.startTime <= dayEnd
|
|
);
|
|
|
|
const dayArrivals = vipArrivals.filter(v => {
|
|
if (v.expectedArrival && v.expectedArrival >= dayStart && v.expectedArrival <= dayEnd) {
|
|
return true;
|
|
}
|
|
return v.flights.some(f => f.scheduledArrival && f.scheduledArrival >= dayStart && f.scheduledArrival <= dayEnd);
|
|
});
|
|
|
|
dayMap.set(dateKey, {
|
|
date: dateKey,
|
|
dayOfWeek: d.toLocaleDateString('en-US', { weekday: 'long' }),
|
|
eventCount: dayEvents.length,
|
|
unassignedCount: dayEvents.filter(e => !e.driverId || !e.vehicleId).length,
|
|
arrivingVipCount: dayArrivals.length,
|
|
arrivingVips: dayArrivals.map(v => v.name),
|
|
});
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
startDate: start.toISOString().split('T')[0],
|
|
endDate: end.toISOString().split('T')[0],
|
|
weeksAhead,
|
|
days: Array.from(dayMap.values()),
|
|
summary: {
|
|
totalEvents: events.length,
|
|
totalUnassigned: events.filter(e => !e.driverId || !e.vehicleId).length,
|
|
totalArrivingVips: vipArrivals.length,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
private async identifySchedulingGaps(input: Record<string, any>): Promise<ToolResult> {
|
|
const { lookaheadDays = 7 } = input;
|
|
|
|
const start = new Date();
|
|
start.setHours(0, 0, 0, 0);
|
|
const end = new Date(start);
|
|
end.setDate(end.getDate() + lookaheadDays);
|
|
|
|
// Get all events
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: start, lt: end },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
include: {
|
|
driver: true,
|
|
vehicle: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const allVipIds = [...new Set(events.flatMap(e => e.vipIds))];
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: allVipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
const vipMap = new Map(vips.map(v => [v.id, v.name]));
|
|
|
|
// Find unassigned events
|
|
const unassignedEvents = events
|
|
.filter(e => !e.driverId || !e.vehicleId)
|
|
.map(e => ({
|
|
id: e.id,
|
|
title: e.title,
|
|
startTime: e.startTime,
|
|
vipNames: e.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
missingDriver: !e.driverId,
|
|
missingVehicle: !e.vehicleId,
|
|
}));
|
|
|
|
// Find driver conflicts
|
|
const driverConflicts: any[] = [];
|
|
const driverEventMap = new Map<string, any[]>();
|
|
|
|
events.forEach(event => {
|
|
if (!event.driverId) return;
|
|
if (!driverEventMap.has(event.driverId)) {
|
|
driverEventMap.set(event.driverId, []);
|
|
}
|
|
driverEventMap.get(event.driverId)!.push(event);
|
|
});
|
|
|
|
driverEventMap.forEach((driverEvents, driverId) => {
|
|
for (let i = 0; i < driverEvents.length - 1; i++) {
|
|
const current = driverEvents[i];
|
|
const next = driverEvents[i + 1];
|
|
|
|
if (current.endTime > next.startTime) {
|
|
driverConflicts.push({
|
|
driverId,
|
|
driverName: current.driver?.name || 'Unknown',
|
|
event1: {
|
|
id: current.id,
|
|
title: current.title,
|
|
time: `${current.startTime.toLocaleTimeString()} - ${current.endTime.toLocaleTimeString()}`,
|
|
},
|
|
event2: {
|
|
id: next.id,
|
|
title: next.title,
|
|
time: `${next.startTime.toLocaleTimeString()} - ${next.endTime.toLocaleTimeString()}`,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Find VIP conflicts
|
|
const vipConflicts: any[] = [];
|
|
const vipEventMap = new Map<string, any[]>();
|
|
|
|
events.forEach(event => {
|
|
event.vipIds.forEach(vipId => {
|
|
if (!vipEventMap.has(vipId)) {
|
|
vipEventMap.set(vipId, []);
|
|
}
|
|
vipEventMap.get(vipId)!.push(event);
|
|
});
|
|
});
|
|
|
|
vipEventMap.forEach((vipEvents, vipId) => {
|
|
const sortedEvents = vipEvents.sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
|
|
|
|
for (let i = 0; i < sortedEvents.length - 1; i++) {
|
|
const current = sortedEvents[i];
|
|
const next = sortedEvents[i + 1];
|
|
|
|
if (current.endTime > next.startTime) {
|
|
vipConflicts.push({
|
|
vipId,
|
|
vipName: vipMap.get(vipId) || 'Unknown',
|
|
event1: {
|
|
id: current.id,
|
|
title: current.title,
|
|
time: `${current.startTime.toLocaleTimeString()} - ${current.endTime.toLocaleTimeString()}`,
|
|
},
|
|
event2: {
|
|
id: next.id,
|
|
title: next.title,
|
|
time: `${next.startTime.toLocaleTimeString()} - ${next.endTime.toLocaleTimeString()}`,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
dateRange: {
|
|
start: start.toISOString().split('T')[0],
|
|
end: end.toISOString().split('T')[0],
|
|
},
|
|
totalEvents: events.length,
|
|
issues: {
|
|
unassignedEvents: {
|
|
count: unassignedEvents.length,
|
|
events: unassignedEvents,
|
|
},
|
|
driverConflicts: {
|
|
count: driverConflicts.length,
|
|
conflicts: driverConflicts,
|
|
},
|
|
vipConflicts: {
|
|
count: vipConflicts.length,
|
|
conflicts: vipConflicts,
|
|
},
|
|
},
|
|
summary: `Found ${unassignedEvents.length} unassigned events, ${driverConflicts.length} driver conflicts, and ${vipConflicts.length} VIP conflicts in the next ${lookaheadDays} days.`,
|
|
},
|
|
};
|
|
}
|
|
|
|
private async suggestVehicleForEvent(input: Record<string, any>): Promise<ToolResult> {
|
|
const { eventId } = input;
|
|
|
|
const event = await this.prisma.scheduleEvent.findUnique({
|
|
where: { id: eventId },
|
|
include: { vehicle: true },
|
|
});
|
|
|
|
if (!event) {
|
|
return { success: false, error: 'Event not found' };
|
|
}
|
|
|
|
// Get VIP count for capacity calculation
|
|
const vipCount = event.vipIds.length;
|
|
|
|
// Get all vehicles
|
|
const vehicles = await this.prisma.vehicle.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
status: { in: ['AVAILABLE', 'RESERVED'] },
|
|
},
|
|
});
|
|
|
|
// Check availability for each vehicle
|
|
const suggestions = [];
|
|
|
|
for (const vehicle of vehicles) {
|
|
// Check if vehicle has conflicting events
|
|
const conflictingEvents = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
vehicleId: vehicle.id,
|
|
deletedAt: null,
|
|
status: { not: 'CANCELLED' },
|
|
id: { not: eventId },
|
|
OR: [
|
|
{ startTime: { gte: event.startTime, lt: event.endTime } },
|
|
{ endTime: { gt: event.startTime, lte: event.endTime } },
|
|
{ AND: [{ startTime: { lte: event.startTime } }, { endTime: { gte: event.endTime } }] },
|
|
],
|
|
},
|
|
});
|
|
|
|
const isAvailable = conflictingEvents.length === 0;
|
|
const hasCapacity = vehicle.seatCapacity >= vipCount;
|
|
|
|
suggestions.push({
|
|
id: vehicle.id,
|
|
name: vehicle.name,
|
|
type: vehicle.type,
|
|
seatCapacity: vehicle.seatCapacity,
|
|
licensePlate: vehicle.licensePlate,
|
|
status: vehicle.status,
|
|
isAvailable,
|
|
hasCapacity,
|
|
score: (isAvailable ? 10 : 0) + (hasCapacity ? 5 : 0) + (vehicle.status === 'AVAILABLE' ? 3 : 0),
|
|
});
|
|
}
|
|
|
|
// Sort by score (best matches first)
|
|
suggestions.sort((a, b) => b.score - a.score);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
event: {
|
|
id: event.id,
|
|
title: event.title,
|
|
startTime: event.startTime,
|
|
endTime: event.endTime,
|
|
vipCount,
|
|
currentVehicle: event.vehicle?.name || null,
|
|
},
|
|
suggestions,
|
|
recommended: suggestions.filter(s => s.isAvailable && s.hasCapacity),
|
|
},
|
|
message: `Found ${suggestions.filter(s => s.isAvailable && s.hasCapacity).length} suitable vehicle(s) for event "${event.title}"`,
|
|
};
|
|
}
|
|
|
|
private async getVehicleSchedule(input: Record<string, any>): Promise<ToolResult> {
|
|
const { vehicleName, vehicleId, startDate, endDate } = input;
|
|
|
|
// Find vehicle by name or ID
|
|
let vehicle;
|
|
if (vehicleId) {
|
|
vehicle = await this.prisma.vehicle.findUnique({ where: { id: vehicleId } });
|
|
} else if (vehicleName) {
|
|
const vehicles = await this.prisma.vehicle.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
name: { contains: vehicleName, mode: 'insensitive' },
|
|
},
|
|
});
|
|
|
|
if (vehicles.length === 0) {
|
|
const allVehicles = await this.prisma.vehicle.findMany({
|
|
where: { deletedAt: null },
|
|
select: { name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return {
|
|
success: false,
|
|
error: `No vehicle found matching "${vehicleName}". Available vehicles: ${allVehicles.map(v => v.name).join(', ')}`,
|
|
};
|
|
}
|
|
|
|
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 start = new Date(startDate);
|
|
const end = new Date(endDate);
|
|
|
|
const events = await this.prisma.scheduleEvent.findMany({
|
|
where: {
|
|
vehicleId: vehicle.id,
|
|
deletedAt: null,
|
|
startTime: { gte: start, lte: end },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
include: {
|
|
driver: true,
|
|
},
|
|
orderBy: { startTime: 'asc' },
|
|
});
|
|
|
|
// Fetch VIP names
|
|
const allVipIds = [...new Set(events.flatMap(e => e.vipIds))];
|
|
const vips = await this.prisma.vIP.findMany({
|
|
where: { id: { in: allVipIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
const vipMap = new Map(vips.map(v => [v.id, v.name]));
|
|
|
|
const eventsWithDetails = events.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
type: event.type,
|
|
status: event.status,
|
|
startTime: event.startTime,
|
|
endTime: event.endTime,
|
|
vipNames: event.vipIds.map(id => vipMap.get(id) || 'Unknown'),
|
|
driverName: event.driver?.name || null,
|
|
pickupLocation: event.pickupLocation,
|
|
dropoffLocation: event.dropoffLocation,
|
|
location: event.location,
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
vehicle: {
|
|
id: vehicle.id,
|
|
name: vehicle.name,
|
|
type: vehicle.type,
|
|
licensePlate: vehicle.licensePlate,
|
|
seatCapacity: vehicle.seatCapacity,
|
|
status: vehicle.status,
|
|
},
|
|
dateRange: {
|
|
start: start.toISOString().split('T')[0],
|
|
end: end.toISOString().split('T')[0],
|
|
},
|
|
eventCount: events.length,
|
|
schedule: eventsWithDetails,
|
|
},
|
|
message: `Vehicle ${vehicle.name} has ${events.length} scheduled event(s) between ${start.toLocaleDateString()} and ${end.toLocaleDateString()}`,
|
|
};
|
|
}
|
|
|
|
private async getDriverWorkloadSummary(input: Record<string, any>): Promise<ToolResult> {
|
|
const { startDate, endDate } = input;
|
|
|
|
const start = new Date(startDate);
|
|
const end = new Date(endDate);
|
|
|
|
// Get all drivers
|
|
const drivers = await this.prisma.driver.findMany({
|
|
where: { deletedAt: null },
|
|
include: {
|
|
events: {
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: start, lte: end },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
},
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
|
|
const workloadSummary = drivers.map(driver => {
|
|
const events = driver.events;
|
|
|
|
// Calculate total hours
|
|
const totalMinutes = events.reduce((sum, event) => {
|
|
const duration = event.endTime.getTime() - event.startTime.getTime();
|
|
return sum + (duration / 60000); // Convert to minutes
|
|
}, 0);
|
|
|
|
const totalHours = totalMinutes / 60;
|
|
|
|
// Calculate total days in range
|
|
const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
|
const daysWorked = new Set(
|
|
events.map(e => e.startTime.toISOString().split('T')[0])
|
|
).size;
|
|
|
|
return {
|
|
driverId: driver.id,
|
|
driverName: driver.name,
|
|
department: driver.department,
|
|
isAvailable: driver.isAvailable,
|
|
eventCount: events.length,
|
|
totalHours: Math.round(totalHours * 10) / 10,
|
|
averageHoursPerEvent: events.length > 0 ? Math.round((totalHours / events.length) * 10) / 10 : 0,
|
|
daysWorked,
|
|
totalDaysInRange: totalDays,
|
|
utilizationPercent: Math.round((daysWorked / totalDays) * 100),
|
|
};
|
|
});
|
|
|
|
// Sort by event count descending
|
|
workloadSummary.sort((a, b) => b.eventCount - a.eventCount);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
dateRange: {
|
|
start: start.toISOString().split('T')[0],
|
|
end: end.toISOString().split('T')[0],
|
|
},
|
|
totalDrivers: drivers.length,
|
|
workload: workloadSummary,
|
|
summary: {
|
|
totalEvents: workloadSummary.reduce((sum, d) => sum + d.eventCount, 0),
|
|
totalHours: Math.round(workloadSummary.reduce((sum, d) => sum + d.totalHours, 0) * 10) / 10,
|
|
averageEventsPerDriver: Math.round(
|
|
workloadSummary.reduce((sum, d) => sum + d.eventCount, 0) / drivers.length
|
|
),
|
|
},
|
|
},
|
|
message: `Workload summary for ${drivers.length} driver(s) from ${start.toLocaleDateString()} to ${end.toLocaleDateString()}`,
|
|
};
|
|
}
|
|
|
|
// ==================== SELF-AWARENESS / HELP TOOLS ====================
|
|
|
|
private async getMyCapabilities(input: Record<string, any>): Promise<ToolResult> {
|
|
const category = input.category || 'all';
|
|
|
|
const capabilities = {
|
|
search: {
|
|
description: 'Find and lookup information',
|
|
tools: [
|
|
{ name: 'search_vips', description: 'Search VIPs by name, organization, department', example: 'Find VIP named John' },
|
|
{ name: 'search_drivers', description: 'Search drivers by name, department, availability', example: 'Find available drivers' },
|
|
{ name: 'search_events', description: 'Search events by VIP, driver, date, status', example: 'Events for tomorrow' },
|
|
{ name: 'get_vip_details', description: 'Get full VIP info with flights and events', example: 'Details for VIP John Smith' },
|
|
{ name: 'get_vip_itinerary', description: 'Get complete VIP itinerary', example: 'Itinerary for Dr. Martinez' },
|
|
{ name: 'list_all_drivers', description: 'List all drivers in system', example: 'Show all drivers' },
|
|
{ name: 'get_available_vehicles', description: 'Find available vehicles by type/capacity', example: 'Available SUVs' },
|
|
{ name: 'find_available_drivers_for_timerange', description: 'Find drivers free during specific time', example: 'Who is free 2-4pm tomorrow?' },
|
|
{ name: 'get_daily_driver_manifest', description: 'Full daily schedule for a driver', example: "What's Mike's schedule today?" },
|
|
{ name: 'get_vehicle_schedule', description: 'Get vehicle assignments for date range', example: 'Blue van schedule this week' },
|
|
],
|
|
},
|
|
create: {
|
|
description: 'Create new records',
|
|
tools: [
|
|
{ name: 'create_vip', description: 'Create a new VIP profile', example: 'Add VIP Jane Doe from Acme Corp' },
|
|
{ name: 'create_event', description: 'Schedule a new event/transport', example: 'Schedule pickup at 3pm' },
|
|
{ name: 'create_flight', description: 'Add flight for a VIP', example: 'Add flight AA1234 for John' },
|
|
],
|
|
},
|
|
update: {
|
|
description: 'Modify existing records',
|
|
tools: [
|
|
{ name: 'update_vip', description: 'Update VIP information', example: 'Change VIP phone number' },
|
|
{ name: 'update_event', description: 'Modify event details, time, location', example: 'Move pickup to 4pm' },
|
|
{ name: 'update_flight', description: 'Update flight times/status', example: 'Flight delayed to 5pm' },
|
|
{ name: 'update_driver', description: 'Update driver info, availability', example: 'Mark driver as unavailable' },
|
|
{ name: 'assign_driver_to_event', description: 'Assign/change driver for event', example: 'Assign Mike to this pickup' },
|
|
{ name: 'assign_vehicle_to_event', description: 'Assign/change vehicle for event', example: 'Use the SUV for this trip' },
|
|
{ name: 'reassign_driver_events', description: 'Bulk reassign from one driver to another', example: 'Move all of John\'s events to Mike' },
|
|
],
|
|
},
|
|
delete: {
|
|
description: 'Remove records (soft delete)',
|
|
tools: [
|
|
{ name: 'delete_event', description: 'Cancel/delete an event', example: 'Cancel the 3pm pickup' },
|
|
{ name: 'delete_flight', description: 'Remove a flight record', example: 'Delete cancelled flight' },
|
|
],
|
|
},
|
|
communication: {
|
|
description: 'Driver messaging via Signal',
|
|
tools: [
|
|
{ name: 'send_driver_notification_via_signal', description: 'Send message to driver', example: 'Tell Mike the pickup is delayed' },
|
|
{ name: 'bulk_send_driver_schedules', description: 'Send schedules to all drivers', example: 'Send tomorrow\'s schedules to everyone' },
|
|
],
|
|
},
|
|
analytics: {
|
|
description: 'Reports, summaries, and audits',
|
|
tools: [
|
|
{ name: 'get_todays_summary', description: 'Today\'s events, arrivals, stats', example: "What's happening today?" },
|
|
{ name: 'get_weekly_lookahead', description: 'Week-by-week event summary', example: 'What does next week look like?' },
|
|
{ name: 'get_driver_workload_summary', description: 'Driver utilization statistics', example: 'Who is overworked this week?' },
|
|
{ name: 'identify_scheduling_gaps', description: 'Find problems in schedule', example: 'Any issues with the schedule?' },
|
|
{ name: 'find_unassigned_events', description: 'Events without driver/vehicle', example: 'What needs assignment?' },
|
|
],
|
|
},
|
|
scheduling: {
|
|
description: 'Conflict detection and scheduling',
|
|
tools: [
|
|
{ name: 'check_driver_conflicts', description: 'Check driver for time conflicts', example: 'Can Mike do 2-4pm?' },
|
|
{ name: 'check_vip_conflicts', description: 'Check VIP for double-booking', example: 'Is VIP free at 3pm?' },
|
|
{ name: 'suggest_vehicle_for_event', description: 'Recommend best vehicle', example: 'What vehicle for 6 passengers?' },
|
|
{ name: 'get_driver_schedule', description: 'Driver events for date range', example: 'Mike\'s schedule this week' },
|
|
],
|
|
},
|
|
help: {
|
|
description: 'Self-help and system info',
|
|
tools: [
|
|
{ name: 'get_my_capabilities', description: 'List all available tools (this tool)', example: 'What can you do?' },
|
|
{ name: 'get_workflow_guide', description: 'Step-by-step task guides', example: 'How do I handle a flight delay?' },
|
|
{ name: 'get_current_system_status', description: 'System overview and stats', example: 'System status' },
|
|
],
|
|
},
|
|
};
|
|
|
|
const validCategory = category as keyof typeof capabilities;
|
|
if (category !== 'all' && capabilities[validCategory]) {
|
|
return {
|
|
success: true,
|
|
data: { [category]: capabilities[validCategory] },
|
|
message: `Showing ${category} tools. I have ${capabilities[validCategory].tools.length} tools in this category.`,
|
|
};
|
|
}
|
|
|
|
const totalTools = Object.values(capabilities).reduce((sum, cat) => sum + cat.tools.length, 0);
|
|
|
|
return {
|
|
success: true,
|
|
data: capabilities,
|
|
message: `I have ${totalTools} tools available across ${Object.keys(capabilities).length} categories. Ask me about any specific capability!`,
|
|
};
|
|
}
|
|
|
|
private async getWorkflowGuide(input: Record<string, any>): Promise<ToolResult> {
|
|
const workflows = {
|
|
schedule_airport_pickup: {
|
|
title: 'Schedule an Airport Pickup',
|
|
steps: [
|
|
'1. Find the VIP: Use search_vips with the VIP name',
|
|
'2. Get flight info: Use get_flights_for_vip or check if flight already exists',
|
|
'3. Find available driver: Use find_available_drivers_for_timerange for the arrival time',
|
|
'4. Find suitable vehicle: Use suggest_vehicle_for_event or get_available_vehicles',
|
|
'5. Create the event: Use create_event with type TRANSPORT, pickup at airport, dropoff at destination',
|
|
'6. Notify driver: Use send_driver_notification_via_signal to inform them',
|
|
],
|
|
tips: ['Add 30 min buffer after flight arrival', 'Check flight status before the day'],
|
|
},
|
|
reassign_driver: {
|
|
title: 'Reassign a Driver\'s Events (Driver Sick/Unavailable)',
|
|
steps: [
|
|
'1. Use reassign_driver_events with fromDriverName and toDriverName',
|
|
'2. Optionally specify a date to only reassign that day\'s events',
|
|
'3. Review the returned list of reassigned events',
|
|
'4. Use send_driver_notification_via_signal to notify both drivers',
|
|
],
|
|
tips: ['Check the new driver has no conflicts first', 'Update driver availability with update_driver'],
|
|
},
|
|
handle_flight_delay: {
|
|
title: 'Handle a Flight Delay',
|
|
steps: [
|
|
'1. Find the VIP: Use search_vips to get VIP info',
|
|
'2. Update flight: Use update_flight with new arrival time',
|
|
'3. Find affected events: Use search_events filtered by VIP and date',
|
|
'4. Update pickup event: Use update_event to adjust start/end times',
|
|
'5. Check driver conflicts: Use check_driver_conflicts for new time',
|
|
'6. Notify driver: Use send_driver_notification_via_signal about the change',
|
|
],
|
|
tips: ['Check if other VIPs share the same flight', 'Consider ripple effects on later events'],
|
|
},
|
|
create_vip_itinerary: {
|
|
title: 'Create a Complete VIP Itinerary',
|
|
steps: [
|
|
'1. Create VIP: Use create_vip with all details',
|
|
'2. Add flights: Use create_flight for each flight segment',
|
|
'3. Create events: Use create_event for airport pickup, meetings, dinners, etc.',
|
|
'4. Assign resources: Use assign_driver_to_event and assign_vehicle_to_event',
|
|
'5. Review: Use get_vip_itinerary to see the complete schedule',
|
|
],
|
|
tips: ['Schedule in chronological order', 'Add buffer time between events'],
|
|
},
|
|
morning_briefing: {
|
|
title: 'Get Morning Briefing',
|
|
steps: [
|
|
'1. Get today summary: Use get_todays_summary for overview',
|
|
'2. Check for problems: Use identify_scheduling_gaps',
|
|
'3. Review unassigned: Use find_unassigned_events',
|
|
'4. Check workload: Use get_driver_workload_summary for balance',
|
|
'5. Send schedules: Use bulk_send_driver_schedules to notify all drivers',
|
|
],
|
|
tips: ['Do this 30 min before operations start', 'Address gaps before drivers arrive'],
|
|
},
|
|
send_driver_notifications: {
|
|
title: 'Send Notifications to Drivers',
|
|
steps: [
|
|
'1. For single driver: Use send_driver_notification_via_signal with message',
|
|
'2. For all drivers (schedules): Use bulk_send_driver_schedules with date',
|
|
'3. Reference specific event: Include relatedEventId for context',
|
|
],
|
|
tips: ['Keep messages concise', 'Include pickup time and location'],
|
|
},
|
|
check_schedule_problems: {
|
|
title: 'Audit Schedule for Problems',
|
|
steps: [
|
|
'1. Run full audit: Use identify_scheduling_gaps with lookahead days',
|
|
'2. Review driver conflicts: Check the conflicts list',
|
|
'3. Review VIP conflicts: Check for double-bookings',
|
|
'4. Review unassigned: Check events missing drivers/vehicles',
|
|
'5. Fix issues: Use update_event, assign_driver_to_event as needed',
|
|
],
|
|
tips: ['Run this daily', 'Prioritize same-day issues'],
|
|
},
|
|
vehicle_assignment: {
|
|
title: 'Choose and Assign the Right Vehicle',
|
|
steps: [
|
|
'1. Get recommendation: Use suggest_vehicle_for_event with eventId',
|
|
'2. Or search manually: Use get_available_vehicles with type/capacity filters',
|
|
'3. Check availability: Use get_vehicle_schedule to see existing assignments',
|
|
'4. Assign: Use assign_vehicle_to_event',
|
|
],
|
|
tips: ['Consider VIP count for capacity', 'Check vehicle location if multiple stops'],
|
|
},
|
|
bulk_schedule_update: {
|
|
title: 'Handle Bulk Schedule Changes',
|
|
steps: [
|
|
'1. Search affected events: Use search_events with date/driver/VIP filter',
|
|
'2. Update each event: Use update_event for each (or reassign_driver_events for driver swap)',
|
|
'3. Notify drivers: Use send_driver_notification_via_signal or bulk_send_driver_schedules',
|
|
],
|
|
tips: ['Work chronologically', 'Verify no new conflicts after changes'],
|
|
},
|
|
};
|
|
|
|
const task = input.task as keyof typeof workflows;
|
|
if (!workflows[task]) {
|
|
return {
|
|
success: false,
|
|
error: `Unknown workflow: ${task}. Available: ${Object.keys(workflows).join(', ')}`,
|
|
};
|
|
}
|
|
|
|
const workflow = workflows[task];
|
|
return {
|
|
success: true,
|
|
data: workflow,
|
|
message: `Here's the step-by-step guide for: ${workflow.title}`,
|
|
};
|
|
}
|
|
|
|
private async getCurrentSystemStatus(): Promise<ToolResult> {
|
|
const now = new Date();
|
|
const today = new Date(now);
|
|
today.setHours(0, 0, 0, 0);
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
const nextWeek = new Date(today);
|
|
nextWeek.setDate(nextWeek.getDate() + 7);
|
|
|
|
const [
|
|
vipCount,
|
|
driverCount,
|
|
vehicleCount,
|
|
todaysEvents,
|
|
upcomingEvents,
|
|
unassignedEvents,
|
|
availableDrivers,
|
|
availableVehicles,
|
|
] = await Promise.all([
|
|
this.prisma.vIP.count({ where: { deletedAt: null } }),
|
|
this.prisma.driver.count({ where: { deletedAt: null } }),
|
|
this.prisma.vehicle.count({ where: { deletedAt: null } }),
|
|
this.prisma.scheduleEvent.count({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: today, lt: tomorrow },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
}),
|
|
this.prisma.scheduleEvent.count({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: tomorrow, lt: nextWeek },
|
|
status: { not: 'CANCELLED' },
|
|
},
|
|
}),
|
|
this.prisma.scheduleEvent.count({
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gte: now },
|
|
status: { in: ['SCHEDULED'] },
|
|
OR: [{ driverId: null }, { vehicleId: null }],
|
|
},
|
|
}),
|
|
this.prisma.driver.count({ where: { deletedAt: null, isAvailable: true } }),
|
|
this.prisma.vehicle.count({ where: { deletedAt: null, status: 'AVAILABLE' } }),
|
|
]);
|
|
|
|
const status = {
|
|
timestamp: now.toISOString(),
|
|
resources: {
|
|
vips: vipCount,
|
|
drivers: { total: driverCount, available: availableDrivers },
|
|
vehicles: { total: vehicleCount, available: availableVehicles },
|
|
},
|
|
events: {
|
|
today: todaysEvents,
|
|
next7Days: upcomingEvents,
|
|
needingAttention: unassignedEvents,
|
|
},
|
|
alerts: [] as string[],
|
|
};
|
|
|
|
// Add alerts for issues
|
|
if (unassignedEvents > 0) {
|
|
status.alerts.push(`${unassignedEvents} upcoming event(s) need driver/vehicle assignment`);
|
|
}
|
|
if (availableDrivers === 0) {
|
|
status.alerts.push('No drivers currently marked as available');
|
|
}
|
|
if (availableVehicles === 0) {
|
|
status.alerts.push('No vehicles currently available');
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: status,
|
|
message: status.alerts.length > 0
|
|
? `System status retrieved. ATTENTION: ${status.alerts.length} alert(s) require attention.`
|
|
: 'System status retrieved. No immediate issues.',
|
|
};
|
|
}
|
|
|
|
private async getApiDocumentation(input: Record<string, any>): Promise<ToolResult> {
|
|
const resource = input.resource || 'all';
|
|
|
|
const apiDocs = {
|
|
auth: {
|
|
description: 'Authentication endpoints (Auth0 JWT-based)',
|
|
baseUrl: '/api/v1/auth',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/profile', description: 'Get current user profile and role', auth: 'Required' },
|
|
],
|
|
notes: 'Uses Auth0 for authentication. Token must be included as Bearer token in Authorization header.',
|
|
},
|
|
users: {
|
|
description: 'User management (Admin only)',
|
|
baseUrl: '/api/v1/users',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/', description: 'List all users', auth: 'Admin only' },
|
|
{ method: 'GET', path: '/pending', description: 'List users pending approval', auth: 'Admin only' },
|
|
{ method: 'GET', path: '/:id', description: 'Get specific user by ID', auth: 'Admin only' },
|
|
{ method: 'PATCH', path: '/:id', description: 'Update user details', auth: 'Admin only' },
|
|
{ method: 'PATCH', path: '/:id/approve', description: 'Approve a pending user', auth: 'Admin only' },
|
|
{ method: 'DELETE', path: '/:id', description: 'Delete user (soft delete)', auth: 'Admin only' },
|
|
],
|
|
notes: 'First user to register automatically becomes Admin. Other users need approval.',
|
|
},
|
|
vips: {
|
|
description: 'VIP profile management',
|
|
baseUrl: '/api/v1/vips',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/', description: 'List all VIPs', auth: 'All roles' },
|
|
{ method: 'GET', path: '/:id', description: 'Get VIP details with flights and events', auth: 'All roles' },
|
|
{ method: 'POST', path: '/', description: 'Create new VIP', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/:id', description: 'Update VIP information', auth: 'Admin, Coordinator' },
|
|
{ method: 'DELETE', path: '/:id', description: 'Soft delete VIP', auth: 'Admin, Coordinator' },
|
|
],
|
|
fields: ['name', 'organization', 'department (OFFICE_OF_DEVELOPMENT | ADMIN)', 'arrivalMode (FLIGHT | SELF_DRIVING)', 'expectedArrival', 'airportPickup', 'venueTransport', 'notes'],
|
|
},
|
|
drivers: {
|
|
description: 'Driver resource management',
|
|
baseUrl: '/api/v1/drivers',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/', description: 'List all drivers', auth: 'All roles' },
|
|
{ method: 'GET', path: '/me', description: 'Get current user\'s driver profile', auth: 'Driver role' },
|
|
{ method: 'GET', path: '/:id', description: 'Get driver details', auth: 'All roles' },
|
|
{ method: 'GET', path: '/:id/schedule', description: 'Get driver\'s schedule', auth: 'All roles' },
|
|
{ method: 'POST', path: '/', description: 'Create new driver', auth: 'Admin, Coordinator' },
|
|
{ method: 'POST', path: '/:id/send-schedule', description: 'Send schedule to driver via Signal (ICS + PDF)', auth: 'Admin, Coordinator' },
|
|
{ method: 'POST', path: '/send-all-schedules', description: 'Send schedules to all drivers with events', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/:id', description: 'Update driver', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/me', description: 'Update own profile', auth: 'Driver role' },
|
|
{ method: 'DELETE', path: '/:id', description: 'Soft delete driver', auth: 'Admin, Coordinator' },
|
|
],
|
|
fields: ['name', 'phone', 'department', 'isAvailable', 'shiftStartTime', 'shiftEndTime', 'userId (link to User)'],
|
|
},
|
|
events: {
|
|
description: 'Schedule event management (transports, meetings, etc.)',
|
|
baseUrl: '/api/v1/events',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/', description: 'List events (supports filters: date, driverId, status)', auth: 'All roles' },
|
|
{ method: 'GET', path: '/:id', description: 'Get event details', auth: 'All roles' },
|
|
{ method: 'POST', path: '/', description: 'Create new event (checks for conflicts)', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/:id', description: 'Update event', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/:id/status', description: 'Update event status only', auth: 'All roles (Drivers can update their events)' },
|
|
{ method: 'DELETE', path: '/:id', description: 'Cancel/delete event', auth: 'Admin, Coordinator' },
|
|
],
|
|
fields: ['vipIds[]', 'title', 'type (TRANSPORT | MEETING | EVENT | MEAL | ACCOMMODATION)', 'status (SCHEDULED | IN_PROGRESS | COMPLETED | CANCELLED)', 'startTime', 'endTime', 'pickupLocation', 'dropoffLocation', 'location', 'driverId', 'vehicleId', 'description', 'notes'],
|
|
notes: 'Events support multiple VIPs via vipIds array. Conflict detection runs on create/update.',
|
|
},
|
|
vehicles: {
|
|
description: 'Vehicle fleet management',
|
|
baseUrl: '/api/v1/vehicles',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/', description: 'List all vehicles (supports filters: type, status)', auth: 'All roles' },
|
|
{ method: 'GET', path: '/:id', description: 'Get vehicle details', auth: 'All roles' },
|
|
{ method: 'POST', path: '/', description: 'Create new vehicle', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/:id', description: 'Update vehicle', auth: 'Admin, Coordinator' },
|
|
{ method: 'DELETE', path: '/:id', description: 'Soft delete vehicle', auth: 'Admin, Coordinator' },
|
|
],
|
|
fields: ['name', 'type (VAN | SUV | SEDAN | BUS | GOLF_CART | TRUCK)', 'licensePlate', 'seatCapacity', 'status (AVAILABLE | IN_USE | MAINTENANCE)', 'notes'],
|
|
},
|
|
flights: {
|
|
description: 'Flight tracking and management',
|
|
baseUrl: '/api/v1/flights',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/', description: 'List all flights', auth: 'All roles' },
|
|
{ method: 'GET', path: '/:id', description: 'Get flight details', auth: 'All roles' },
|
|
{ method: 'GET', path: '/vip/:vipId', description: 'Get all flights for a VIP', auth: 'All roles' },
|
|
{ method: 'POST', path: '/', description: 'Create flight record', auth: 'Admin, Coordinator' },
|
|
{ method: 'POST', path: '/track/:flightNumber', description: 'Fetch live flight status from AviationStack', auth: 'Admin, Coordinator' },
|
|
{ method: 'PATCH', path: '/:id', description: 'Update flight info', auth: 'Admin, Coordinator' },
|
|
{ method: 'DELETE', path: '/:id', description: 'Delete flight', auth: 'Admin, Coordinator' },
|
|
],
|
|
fields: ['vipId', 'flightNumber', 'flightDate', 'segment', 'departureAirport (IATA code)', 'arrivalAirport', 'scheduledDeparture', 'scheduledArrival', 'actualDeparture', 'actualArrival', 'status'],
|
|
},
|
|
signal: {
|
|
description: 'Signal messaging integration for driver communication',
|
|
baseUrl: '/api/v1/signal',
|
|
endpoints: [
|
|
{ method: 'GET', path: '/status', description: 'Get Signal service status and linked number', auth: 'Admin, Coordinator' },
|
|
{ method: 'GET', path: '/messages', description: 'Get message history (supports driverId filter)', auth: 'Admin, Coordinator' },
|
|
{ method: 'GET', path: '/messages/unread-counts', description: 'Get unread message counts per driver', auth: 'Admin, Coordinator' },
|
|
{ method: 'GET', path: '/messages/driver/:driverId', description: 'Get messages for specific driver', auth: 'Admin, Coordinator' },
|
|
{ method: 'POST', path: '/messages/send', description: 'Send message to driver via Signal', auth: 'Admin, Coordinator' },
|
|
{ method: 'POST', path: '/messages/mark-read/:driverId', description: 'Mark driver messages as read', auth: 'Admin, Coordinator' },
|
|
{ method: 'POST', path: '/messages/check-responses', description: 'Check if drivers responded since event start times', auth: 'Admin, Coordinator' },
|
|
],
|
|
notes: 'Requires Signal CLI to be running and linked. Messages are stored in database for history.',
|
|
},
|
|
};
|
|
|
|
const validResource = resource as keyof typeof apiDocs;
|
|
if (resource !== 'all' && apiDocs[validResource]) {
|
|
const doc = apiDocs[validResource];
|
|
return {
|
|
success: true,
|
|
data: { [resource]: doc },
|
|
message: `API documentation for ${resource} endpoints. Base URL: ${doc.baseUrl}`,
|
|
};
|
|
}
|
|
|
|
const totalEndpoints = Object.values(apiDocs).reduce(
|
|
(sum, r) => sum + r.endpoints.length,
|
|
0
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
overview: {
|
|
baseUrl: '/api/v1',
|
|
authentication: 'Auth0 JWT Bearer token required on all endpoints',
|
|
roles: ['ADMINISTRATOR (full access)', 'COORDINATOR (manage VIPs, drivers, events)', 'DRIVER (view + update own events)'],
|
|
},
|
|
resources: apiDocs,
|
|
},
|
|
message: `VIP Coordinator API has ${totalEndpoints} endpoints across ${Object.keys(apiDocs).length} resources. All endpoints require authentication.`,
|
|
};
|
|
}
|
|
}
|