From e3ab9e2b83137f9c0c05aaaf47170fd97dc940ad Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Fri, 17 Oct 2025 10:25:42 +0000 Subject: [PATCH] Add operational planning view for vehicle and guard availability Implement a new route and page for operational planning, allowing users to select a date and view available vehicles and guards, with sorting and assignment indicators. 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 --- client/src/App.tsx | 2 + client/src/components/app-sidebar.tsx | 6 + client/src/pages/operational-planning.tsx | 336 ++++++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 client/src/pages/operational-planning.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index c66293a..d07a583 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -22,6 +22,7 @@ import Vehicles from "@/pages/vehicles"; import Parameters from "@/pages/parameters"; import Services from "@/pages/services"; import Planning from "@/pages/planning"; +import OperationalPlanning from "@/pages/operational-planning"; function Router() { const { isAuthenticated, isLoading } = useAuth(); @@ -40,6 +41,7 @@ function Router() { + diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index 64f5846..ca388bc 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -49,6 +49,12 @@ const menuItems = [ icon: ClipboardList, roles: ["admin", "coordinator"], }, + { + title: "Pianificazione Operativa", + url: "/operational-planning", + icon: Calendar, + roles: ["admin", "coordinator"], + }, { title: "Gestione Pianificazioni", url: "/advanced-planning", diff --git a/client/src/pages/operational-planning.tsx b/client/src/pages/operational-planning.tsx new file mode 100644 index 0000000..2359b27 --- /dev/null +++ b/client/src/pages/operational-planning.tsx @@ -0,0 +1,336 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Calendar, Car, Users, Clock, AlertCircle, CheckCircle2, ArrowRight } from "lucide-react"; +import { format } from "date-fns"; +import { it } from "date-fns/locale"; + +interface AssignedShift { + id: string; + startTime: string; + endTime: string; + siteId: string; +} + +interface VehicleAvailability { + id: string; + licensePlate: string; + brand: string; + model: string; + vehicleType: string; + location: string; + status: string; + isAvailable: boolean; + assignedShift: AssignedShift | null; +} + +interface GuardAvailability { + id: string; + badgeNumber: string; + firstName: string; + lastName: string; + location: string; + isAvailable: boolean; + assignedShift: AssignedShift | null; + availability: { + weeklyHours: number; + remainingWeeklyHours: number; + remainingMonthlyHours: number; + consecutiveDaysWorked: number; + }; +} + +interface AvailabilityData { + date: string; + vehicles: VehicleAvailability[]; + guards: GuardAvailability[]; +} + +export default function OperationalPlanning() { + const [selectedDate, setSelectedDate] = useState( + format(new Date(), "yyyy-MM-dd") + ); + + const { data, isLoading, refetch } = useQuery({ + queryKey: ["/api/operational-planning/availability", selectedDate], + enabled: !!selectedDate, + }); + + const handleDateChange = (e: React.ChangeEvent) => { + setSelectedDate(e.target.value); + }; + + const availableVehicles = data?.vehicles.filter((v) => v.isAvailable) || []; + const unavailableVehicles = data?.vehicles.filter((v) => !v.isAvailable) || []; + const availableGuards = data?.guards.filter((g) => g.isAvailable) || []; + const unavailableGuards = data?.guards.filter((g) => !g.isAvailable) || []; + + return ( +
+
+
+

+ + Pianificazione Operativa +

+

+ Visualizza disponibilità automezzi e agenti per data +

+
+
+ + + + + + Seleziona Data + + + +
+
+ + +
+ +
+ {data && ( +

+ Visualizzando disponibilità per: {format(new Date(selectedDate), "dd MMMM yyyy", { locale: it })} +

+ )} +
+
+ +
+ {/* Veicoli */} + + + + + Automezzi Disponibili + + {availableVehicles.length}/{data?.vehicles.length || 0} + + + + + {isLoading ? ( + <> + + + + + ) : ( + <> + {/* Veicoli disponibili */} + {availableVehicles.length > 0 ? ( + availableVehicles.map((vehicle) => ( + + +
+
+
+ +

{vehicle.licensePlate}

+ + {vehicle.location} + +
+

+ {vehicle.brand} {vehicle.model} - {vehicle.vehicleType} +

+
+ +
+
+
+ )) + ) : ( +

+ Nessun veicolo disponibile +

+ )} + + {/* Veicoli non disponibili */} + {unavailableVehicles.length > 0 && ( + <> +
+

Non Disponibili

+
+ {unavailableVehicles.map((vehicle) => ( + + +
+
+
+ +

{vehicle.licensePlate}

+ + {vehicle.location} + +
+

+ {vehicle.brand} {vehicle.model} +

+ {vehicle.assignedShift && ( +
+ + + Assegnato: {format(new Date(vehicle.assignedShift.startTime), "HH:mm")} - + {format(new Date(vehicle.assignedShift.endTime), "HH:mm")} + +
+ )} +
+
+
+
+ ))} + + )} + + )} +
+
+ + {/* Agenti */} + + + + + Agenti Disponibili + + {availableGuards.length}/{data?.guards.length || 0} + + + + + {isLoading ? ( + <> + + + + + ) : ( + <> + {/* Agenti disponibili */} + {availableGuards.length > 0 ? ( + availableGuards.map((guard) => ( + + +
+
+
+ +

+ {guard.firstName} {guard.lastName} +

+ + {guard.badgeNumber} + +
+
+ + + {guard.availability.weeklyHours.toFixed(1)}h questa settimana + + + Residuo: {guard.availability.remainingWeeklyHours.toFixed(1)}h + +
+ {guard.availability.consecutiveDaysWorked > 0 && ( +

+ {guard.availability.consecutiveDaysWorked} giorni consecutivi +

+ )} +
+ +
+
+
+ )) + ) : ( +

+ Nessun agente disponibile +

+ )} + + {/* Agenti non disponibili */} + {unavailableGuards.length > 0 && ( + <> +
+

Non Disponibili

+
+ {unavailableGuards.map((guard) => ( + + +
+
+
+ +

+ {guard.firstName} {guard.lastName} +

+ + {guard.badgeNumber} + +
+ {guard.assignedShift && ( +
+ + + Assegnato: {format(new Date(guard.assignedShift.startTime), "HH:mm")} - + {format(new Date(guard.assignedShift.endTime), "HH:mm")} + +
+ )} +
+ {guard.availability.weeklyHours.toFixed(1)}h settimana +
+
+
+
+
+ ))} + + )} + + )} +
+
+
+
+ ); +}