diff --git a/.replit b/.replit index c50bc15..5283d85 100644 --- a/.replit +++ b/.replit @@ -31,6 +31,10 @@ externalPort = 3002 localPort = 43267 externalPort = 3003 +[[ports]] +localPort = 44791 +externalPort = 4200 + [env] PORT = "5000" diff --git a/client/src/App.tsx b/client/src/App.tsx index d07a583..84f2c7e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -23,6 +23,7 @@ import Parameters from "@/pages/parameters"; import Services from "@/pages/services"; import Planning from "@/pages/planning"; import OperationalPlanning from "@/pages/operational-planning"; +import GeneralPlanning from "@/pages/general-planning"; function Router() { const { isAuthenticated, isLoading } = useAuth(); @@ -42,6 +43,7 @@ function Router() { + diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index ca388bc..a766f41 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -55,6 +55,12 @@ const menuItems = [ icon: Calendar, roles: ["admin", "coordinator"], }, + { + title: "Planning Generale", + url: "/general-planning", + icon: BarChart3, + roles: ["admin", "coordinator"], + }, { title: "Gestione Pianificazioni", url: "/advanced-planning", diff --git a/client/src/pages/general-planning.tsx b/client/src/pages/general-planning.tsx new file mode 100644 index 0000000..8fbc718 --- /dev/null +++ b/client/src/pages/general-planning.tsx @@ -0,0 +1,303 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { format, startOfWeek, addWeeks } from "date-fns"; +import { it } from "date-fns/locale"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface GuardWithHours { + guardId: string; + guardName: string; + badgeNumber: string; + hours: number; +} + +interface Vehicle { + vehicleId: string; + licensePlate: string; + brand: string; + model: string; +} + +interface SiteData { + siteId: string; + siteName: string; + serviceType: string; + minGuards: number; + guards: GuardWithHours[]; + vehicles: Vehicle[]; + totalShiftHours: number; + guardsAssigned: number; + missingGuards: number; + shiftsCount: number; +} + +interface DayData { + date: string; + dayOfWeek: string; + sites: SiteData[]; +} + +interface GeneralPlanningResponse { + weekStart: string; + weekEnd: string; + location: string; + days: DayData[]; +} + +export default function GeneralPlanning() { + const [selectedLocation, setSelectedLocation] = useState("roccapiemonte"); + const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date(), { weekStartsOn: 1 })); + + // Query per dati planning settimanale + const { data: planningData, isLoading } = useQuery({ + queryKey: ["/api/general-planning", format(weekStart, "yyyy-MM-dd"), selectedLocation], + queryFn: async () => { + const response = await fetch( + `/api/general-planning?weekStart=${format(weekStart, "yyyy-MM-dd")}&location=${selectedLocation}` + ); + if (!response.ok) throw new Error("Failed to fetch general planning"); + return response.json(); + }, + }); + + // Navigazione settimana + const goToPreviousWeek = () => setWeekStart(addWeeks(weekStart, -1)); + const goToNextWeek = () => setWeekStart(addWeeks(weekStart, 1)); + const goToCurrentWeek = () => setWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 })); + + // Formatta nome sede + const formatLocation = (loc: string) => { + const locations: Record = { + roccapiemonte: "Roccapiemonte", + milano: "Milano", + roma: "Roma", + }; + return locations[loc] || loc; + }; + + // Raggruppa siti unici da tutti i giorni + const allSites = planningData?.days.flatMap(day => day.sites) || []; + const uniqueSites = Array.from( + new Map(allSites.map(site => [site.siteId, site])).values() + ); + + return ( +
+ {/* Header */} +
+
+

+ Planning Generale +

+

+ Vista settimanale turni con calcolo automatico guardie mancanti +

+
+
+ + {/* Filtri e navigazione */} + + +
+ {/* Selezione Sede */} +
+ + +
+ + {/* Navigazione settimana */} +
+ + + + + +
+ + {/* Info settimana */} + {planningData && ( +
+ {format(new Date(planningData.weekStart), "dd MMM", { locale: it })} -{" "} + {format(new Date(planningData.weekEnd), "dd MMM yyyy", { locale: it })} +
+ )} +
+
+
+ + {/* Tabella Planning */} + + + + + Planning Settimanale - {formatLocation(selectedLocation)} + + + + {isLoading ? ( +
+ + + +
+ ) : planningData ? ( +
+ + + + + {planningData.days.map((day) => ( + + ))} + + + + {uniqueSites.map((site) => ( + + + {planningData.days.map((day) => { + const daySiteData = day.sites.find((s) => s.siteId === site.siteId); + + return ( + + ); + })} + + ))} + +
+ Sito + +
+ + {format(new Date(day.date), "EEEE", { locale: it })} + + + {format(new Date(day.date), "dd/MM", { locale: it })} + +
+
+
+ {site.siteName} + + {site.serviceType} + +
+
+ {daySiteData && daySiteData.shiftsCount > 0 ? ( +
+ {/* Guardie assegnate */} + {daySiteData.guards.length > 0 && ( +
+
+ + Guardie: +
+ {daySiteData.guards.map((guard, idx) => ( +
+ {guard.badgeNumber} + + {guard.hours}h + +
+ ))} +
+ )} + + {/* Veicoli */} + {daySiteData.vehicles.length > 0 && ( +
+
+ + Veicoli: +
+ {daySiteData.vehicles.map((vehicle, idx) => ( +
+ {vehicle.licensePlate} +
+ ))} +
+ )} + + {/* Guardie mancanti */} + {daySiteData.missingGuards > 0 && ( +
+ + + Mancano {daySiteData.missingGuards} {daySiteData.missingGuards === 1 ? "guardia" : "guardie"} + +
+ )} + + {/* Info copertura */} +
+
Turni: {daySiteData.shiftsCount}
+
Tot. ore: {daySiteData.totalShiftHours}h
+
+
+ ) : ( +
+ - +
+ )} +
+ + {uniqueSites.length === 0 && ( +
+ +

Nessun sito attivo per la sede selezionata

+
+ )} +
+ ) : ( +
+

Errore nel caricamento dei dati

+
+ )} +
+
+
+ ); +} diff --git a/server/routes.ts b/server/routes.ts index 2c9b127..437e355 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -866,7 +866,7 @@ export async function registerRoutes(app: Express): Promise { ); // Ottieni tutte le assegnazioni dei turni della settimana - const shiftIds = weekShifts.map(s => s.shift.id); + const shiftIds = weekShifts.map((s: any) => s.shift.id); const assignments = shiftIds.length > 0 ? await db .select({ @@ -878,19 +878,19 @@ export async function registerRoutes(app: Express): Promise { .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`, `)})` + sql`${shiftAssignments.shiftId} IN (${sql.join(shiftIds.map((id: string) => sql`${id}`), sql`, `)})` ) : []; // Ottieni veicoli assegnati const vehicleAssignments = weekShifts - .filter(s => s.shift.vehicleId) - .map(s => s.shift.vehicleId); + .filter((s: any) => s.shift.vehicleId) + .map((s: any) => 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`, `)})` + sql`${vehicles.id} IN (${sql.join(vehicleAssignments.map((id: string) => sql`${id}`), sql`, `)})` ) : []; // Costruisci struttura dati per 7 giorni @@ -906,21 +906,21 @@ export async function registerRoutes(app: Express): Promise { const dayEndTimestamp = new Date(dayStr); dayEndTimestamp.setHours(23, 59, 59, 999); - const sitesData = activeSites.map(({ sites: site, service_types: serviceType }) => { + const sitesData = activeSites.map(({ sites: site, service_types: serviceType }: any) => { // Trova turni del giorno per questo sito - const dayShifts = weekShifts.filter(s => + const dayShifts = weekShifts.filter((s: any) => 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) + const dayAssignments = assignments.filter((a: any) => + dayShifts.some((ds: any) => ds.shift.id === a.shift.id) ); // Calcola ore per ogni guardia - const guardsWithHours = dayAssignments.map(a => { + const guardsWithHours = dayAssignments.map((a: any) => { const shiftStart = new Date(a.shift.startTime); const shiftEnd = new Date(a.shift.endTime); const hours = differenceInHours(shiftEnd, shiftStart); @@ -935,9 +935,9 @@ export async function registerRoutes(app: Express): Promise { // 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); + .filter((ds: any) => ds.shift.vehicleId) + .map((ds: any) => { + const vehicle = assignedVehicles.find((v: any) => v.id === ds.shift.vehicleId); return vehicle ? { vehicleId: vehicle.id, licensePlate: vehicle.licensePlate, @@ -953,7 +953,7 @@ export async function registerRoutes(app: Express): Promise { const minGuardie = site.minGuards || 1; // Somma ore totali dei turni del giorno - const totalShiftHours = dayShifts.reduce((sum, ds) => { + const totalShiftHours = dayShifts.reduce((sum: number, ds: any) => { const start = new Date(ds.shift.startTime); const end = new Date(ds.shift.endTime); return sum + differenceInHours(end, start); @@ -966,7 +966,7 @@ export async function registerRoutes(app: Express): Promise { 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; + const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size; // Guardie mancanti const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);