From 181de6a028da6b8d643609de0a537413a023a925 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Fri, 17 Oct 2025 10:24:01 +0000 Subject: [PATCH] Add operational planning view for vehicle and guard availability Implement GET /api/operational-planning/availability endpoint to fetch and sort vehicles and guards based on their availability and CCNL rules for a given date. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/EEOXc3D --- .replit | 4 ++ server/routes.ts | 121 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/.replit b/.replit index c50bc15..523fe88 100644 --- a/.replit +++ b/.replit @@ -19,6 +19,10 @@ externalPort = 80 localPort = 33035 externalPort = 3001 +[[ports]] +localPort = 34977 +externalPort = 4200 + [[ports]] localPort = 41343 externalPort = 3000 diff --git a/server/routes.ts b/server/routes.ts index 6714962..3f40072 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -534,6 +534,127 @@ export async function registerRoutes(app: Express): Promise { } }); + // ============= OPERATIONAL PLANNING ROUTES ============= + app.get("/api/operational-planning/availability", isAuthenticated, async (req, res) => { + try { + const { getGuardAvailabilityReport } = await import("./ccnlRules"); + const date = req.query.date ? new Date(req.query.date as string) : new Date(); + + // Imposta inizio e fine giornata + const startOfDay = new Date(date); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(date); + endOfDay.setHours(23, 59, 59, 999); + + // Ottieni tutti i veicoli + const allVehicles = await storage.getAllVehicles(); + + // Ottieni turni del giorno per trovare veicoli assegnati + const dayShifts = await db + .select() + .from(shifts) + .where( + and( + gte(shifts.startTime, startOfDay), + lte(shifts.startTime, endOfDay) + ) + ); + + // Mappa veicoli con disponibilità + const vehiclesWithAvailability = await Promise.all( + allVehicles.map(async (vehicle) => { + const assignedShift = dayShifts.find((shift: any) => shift.vehicleId === vehicle.id); + + return { + ...vehicle, + isAvailable: !assignedShift, + assignedShift: assignedShift ? { + id: assignedShift.id, + startTime: assignedShift.startTime, + endTime: assignedShift.endTime, + siteId: assignedShift.siteId + } : null + }; + }) + ); + + // Ottieni tutte le guardie + const allGuards = await storage.getAllGuards(); + + // Ottieni assegnazioni turni del giorno + const dayShiftAssignments = await db + .select() + .from(shiftAssignments) + .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) + .where( + and( + gte(shifts.startTime, startOfDay), + lte(shifts.startTime, endOfDay) + ) + ); + + // Calcola disponibilità agenti con report CCNL + const guardsWithAvailability = await Promise.all( + allGuards.map(async (guard) => { + const assignedShift = dayShiftAssignments.find( + (assignment: any) => assignment.shift_assignments.guardId === guard.id + ); + + // Calcola report disponibilità CCNL + const availabilityReport = await getGuardAvailabilityReport( + guard.id, + startOfDay, + endOfDay + ); + + return { + ...guard, + isAvailable: !assignedShift, + assignedShift: assignedShift ? { + id: assignedShift.shifts.id, + startTime: assignedShift.shifts.startTime, + endTime: assignedShift.shifts.endTime, + siteId: assignedShift.shifts.siteId + } : null, + availability: { + weeklyHours: availabilityReport.weeklyHours.current, + remainingWeeklyHours: availabilityReport.remainingWeeklyHours, + remainingMonthlyHours: availabilityReport.remainingMonthlyHours, + consecutiveDaysWorked: availabilityReport.consecutiveDaysWorked + } + }; + }) + ); + + // Ordina veicoli: disponibili prima, poi per targa + const sortedVehicles = vehiclesWithAvailability.sort((a, b) => { + if (a.isAvailable && !b.isAvailable) return -1; + if (!a.isAvailable && b.isAvailable) return 1; + return a.licensePlate.localeCompare(b.licensePlate); + }); + + // Ordina agenti: disponibili prima, poi per ore settimanali (meno ore = più disponibili) + const sortedGuards = guardsWithAvailability.sort((a, b) => { + if (a.isAvailable && !b.isAvailable) return -1; + if (!a.isAvailable && b.isAvailable) return 1; + // Se entrambi disponibili, ordina per ore settimanali (meno ore = prima) + if (a.isAvailable && b.isAvailable) { + return a.availability.weeklyHours - b.availability.weeklyHours; + } + return 0; + }); + + res.json({ + date: date.toISOString(), + vehicles: sortedVehicles, + guards: sortedGuards + }); + } catch (error) { + console.error("Error fetching operational planning availability:", error); + res.status(500).json({ message: "Failed to fetch availability" }); + } + }); + // ============= CERTIFICATION ROUTES ============= app.post("/api/certifications", isAuthenticated, async (req, res) => { try {