refactor: complete code efficiency pass (Issues #10, #14, #16)

Backend:
- Add Prisma soft-delete middleware for automatic deletedAt filtering (#10)
- Split 2758-line copilot.service.ts into focused sub-services (#14):
  - copilot-schedule.service.ts (schedule/event tools)
  - copilot-reports.service.ts (reporting/analytics tools)
  - copilot-fleet.service.ts (vehicle/driver tools)
  - copilot-vip.service.ts (VIP management tools)
  - copilot.service.ts now thin orchestrator
- Remove manual deletedAt: null from 50+ queries

Frontend:
- Create SortableHeader component for reusable table sorting (#16)
- Create useListPage hook for shared search/filter/sort state (#16)
- Update VipList, DriverList, EventList to use shared infrastructure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 16:34:18 +01:00
parent f2b3f34a72
commit 3bc9cd0bca
23 changed files with 2975 additions and 2443 deletions

View File

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