diff --git a/server/routes.ts b/server/routes.ts index a89edb8..2c9b127 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -4,9 +4,9 @@ import { storage } from "./storage"; import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth"; import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth"; import { db } from "./db"; -import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters } from "@shared/schema"; +import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes } from "@shared/schema"; import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm"; -import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid } from "date-fns"; +import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid, addDays } from "date-fns"; // Determina quale sistema auth usare basandosi sull'ambiente const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS; @@ -809,6 +809,201 @@ export async function registerRoutes(app: Express): Promise { } }); + // Endpoint per Planning Generale - vista settimanale con calcolo guardie mancanti + app.get("/api/general-planning", isAuthenticated, async (req, res) => { + try { + // Sanitizza input data inizio settimana + const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd"); + const normalizedWeekStart = rawWeekStart.split("/")[0]; + + // Valida la data + const parsedWeekStart = parseISO(normalizedWeekStart); + if (!isValid(parsedWeekStart)) { + return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" }); + } + + const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd"); + + // Ottieni location dalla query (default: roccapiemonte) + const location = req.query.location as string || "roccapiemonte"; + + // Calcola fine settimana (weekStart + 6 giorni) + const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd"); + + // Ottieni tutti i siti attivi della sede + const activeSites = await db + .select() + .from(sites) + .leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id)) + .where( + and( + eq(sites.isActive, true), + eq(sites.location, location as any) + ) + ); + + // Ottieni tutti i turni della settimana per la sede + const weekStartTimestamp = new Date(weekStartDate); + weekStartTimestamp.setHours(0, 0, 0, 0); + + const weekEndTimestamp = new Date(weekEndDate); + weekEndTimestamp.setHours(23, 59, 59, 999); + + const weekShifts = await db + .select({ + shift: shifts, + site: sites, + }) + .from(shifts) + .innerJoin(sites, eq(shifts.siteId, sites.id)) + .where( + and( + gte(shifts.startTime, weekStartTimestamp), + lte(shifts.startTime, weekEndTimestamp), + ne(shifts.status, "cancelled"), + eq(sites.location, location as any) + ) + ); + + // Ottieni tutte le assegnazioni dei turni della settimana + const shiftIds = weekShifts.map(s => s.shift.id); + + const assignments = shiftIds.length > 0 ? await db + .select({ + assignment: shiftAssignments, + guard: guards, + shift: shifts, + }) + .from(shiftAssignments) + .innerJoin(guards, eq(shiftAssignments.guardId, guards.id)) + .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) + .where( + sql`${shiftAssignments.shiftId} IN (${sql.join(shiftIds.map(id => sql`${id}`), sql`, `)})` + ) : []; + + // Ottieni veicoli assegnati + const vehicleAssignments = weekShifts + .filter(s => s.shift.vehicleId) + .map(s => s.shift.vehicleId); + + const assignedVehicles = vehicleAssignments.length > 0 ? await db + .select() + .from(vehicles) + .where( + sql`${vehicles.id} IN (${sql.join(vehicleAssignments.map(id => sql`${id}`), sql`, `)})` + ) : []; + + // Costruisci struttura dati per 7 giorni + const weekData = []; + + for (let dayOffset = 0; dayOffset < 7; dayOffset++) { + const currentDay = addDays(parsedWeekStart, dayOffset); + const dayStr = format(currentDay, "yyyy-MM-dd"); + + const dayStartTimestamp = new Date(dayStr); + dayStartTimestamp.setHours(0, 0, 0, 0); + + const dayEndTimestamp = new Date(dayStr); + dayEndTimestamp.setHours(23, 59, 59, 999); + + const sitesData = activeSites.map(({ sites: site, service_types: serviceType }) => { + // Trova turni del giorno per questo sito + const dayShifts = weekShifts.filter(s => + s.shift.siteId === site.id && + s.shift.startTime >= dayStartTimestamp && + s.shift.startTime <= dayEndTimestamp + ); + + // Ottieni assegnazioni guardie per i turni del giorno + const dayAssignments = assignments.filter(a => + dayShifts.some(ds => ds.shift.id === a.shift.id) + ); + + // Calcola ore per ogni guardia + const guardsWithHours = dayAssignments.map(a => { + const shiftStart = new Date(a.shift.startTime); + const shiftEnd = new Date(a.shift.endTime); + const hours = differenceInHours(shiftEnd, shiftStart); + + return { + guardId: a.guard.id, + guardName: a.guard.fullName, + badgeNumber: a.guard.badgeNumber, + hours, + }; + }); + + // Veicoli assegnati ai turni del giorno + const dayVehicles = dayShifts + .filter(ds => ds.shift.vehicleId) + .map(ds => { + const vehicle = assignedVehicles.find(v => v.id === ds.shift.vehicleId); + return vehicle ? { + vehicleId: vehicle.id, + licensePlate: vehicle.licensePlate, + brand: vehicle.brand, + model: vehicle.model, + } : null; + }) + .filter(Boolean); + + // Calcolo guardie mancanti + // Formula: ceil(24 / maxOreGuardia) × minGuardie - guardieAssegnate + const maxOreGuardia = 9; // Max ore per guardia + const minGuardie = site.minGuards || 1; + + // Somma ore totali dei turni del giorno + const totalShiftHours = dayShifts.reduce((sum, ds) => { + const start = new Date(ds.shift.startTime); + const end = new Date(ds.shift.endTime); + return sum + differenceInHours(end, start); + }, 0); + + // Slot necessari per coprire le ore totali + const slotsNeeded = totalShiftHours > 0 ? Math.ceil(totalShiftHours / maxOreGuardia) : 0; + + // Guardie totali necessarie (slot × min guardie contemporanee) + const totalGuardsNeeded = slotsNeeded * minGuardie; + + // Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni) + const uniqueGuardsAssigned = new Set(guardsWithHours.map(g => g.guardId)).size; + + // Guardie mancanti + const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned); + + return { + siteId: site.id, + siteName: site.name, + serviceType: serviceType?.label || "N/A", + minGuards: site.minGuards, + guards: guardsWithHours, + vehicles: dayVehicles, + totalShiftHours, + guardsAssigned: uniqueGuardsAssigned, + missingGuards, + shiftsCount: dayShifts.length, + }; + }); + + weekData.push({ + date: dayStr, + dayOfWeek: format(currentDay, "EEEE"), + sites: sitesData, + }); + } + + res.json({ + weekStart: weekStartDate, + weekEnd: weekEndDate, + location, + days: weekData, + }); + } catch (error) { + console.error("Error fetching general planning:", error); + res.status(500).json({ message: "Failed to fetch general planning", error: String(error) }); + } + }); + // ============= CERTIFICATION ROUTES ============= app.post("/api/certifications", isAuthenticated, async (req, res) => { try {