diff --git a/client/src/App.tsx b/client/src/App.tsx
index 9ce1ec6..c66293a 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -17,10 +17,11 @@ import Shifts from "@/pages/shifts";
import Reports from "@/pages/reports";
import Notifications from "@/pages/notifications";
import Users from "@/pages/users";
-import Planning from "@/pages/planning";
+import AdvancedPlanning from "@/pages/advanced-planning";
import Vehicles from "@/pages/vehicles";
import Parameters from "@/pages/parameters";
import Services from "@/pages/services";
+import Planning from "@/pages/planning";
function Router() {
const { isAuthenticated, isLoading } = useAuth();
@@ -39,6 +40,7 @@ function Router() {
+
diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx
index 6556728..061b14b 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 Avanzata",
+ url: "/advanced-planning",
+ icon: ClipboardList,
+ roles: ["admin", "coordinator"],
+ },
{
title: "Guardie",
url: "/guards",
diff --git a/client/src/pages/advanced-planning.tsx b/client/src/pages/advanced-planning.tsx
new file mode 100644
index 0000000..b7f6202
--- /dev/null
+++ b/client/src/pages/advanced-planning.tsx
@@ -0,0 +1,480 @@
+import { useState } from "react";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { queryClient, apiRequest } from "@/lib/queryClient";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+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 {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { useToast } from "@/hooks/use-toast";
+import { Calendar, GraduationCap, HeartPulse, PartyPopper, Plus, Trash2 } from "lucide-react";
+import { format } from "date-fns";
+import { it } from "date-fns/locale";
+
+export default function PlanningPage() {
+ const { toast } = useToast();
+ const [activeTab, setActiveTab] = useState("training");
+
+ return (
+
+
+
+
Pianificazione Avanzata
+
+ Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni
+
+
+
+
+
+
+
+
+ Formazione
+
+
+
+ Assenze
+
+
+
+ Festività
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function TrainingTab() {
+ const { toast } = useToast();
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+
+ const { data: courses = [], isLoading } = useQuery({
+ queryKey: ["/api/training-courses"],
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (data: any) => apiRequest("/api/training-courses", "POST", data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
+ toast({ title: "Corso creato con successo" });
+ setIsCreateOpen(false);
+ },
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (id: string) => apiRequest(`/api/training-courses/${id}`, "DELETE"),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
+ toast({ title: "Corso eliminato" });
+ },
+ });
+
+ if (isLoading) {
+ return Caricamento corsi...
;
+ }
+
+ return (
+
+
+
+
+
+
+ {courses.map((course: any) => (
+
+
+
+
+ {course.courseName}
+
+ Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"}
+
+
+
+
+ {course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"}
+
+
+ {course.status}
+
+
+
+
+
+
+ ))}
+ {courses.length === 0 && (
+
+ Nessun corso di formazione programmato
+
+ )}
+
+
+ );
+}
+
+function AbsencesTab() {
+ const { toast } = useToast();
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+
+ const { data: absences = [], isLoading } = useQuery({
+ queryKey: ["/api/absences"],
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (data: any) => apiRequest("/api/absences", "POST", data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/absences"] });
+ toast({ title: "Assenza registrata" });
+ setIsCreateOpen(false);
+ },
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (id: string) => apiRequest(`/api/absences/${id}`, "DELETE"),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/absences"] });
+ toast({ title: "Assenza eliminata" });
+ },
+ });
+
+ if (isLoading) {
+ return Caricamento assenze...
;
+ }
+
+ return (
+
+
+
+
+
+
+ {absences.map((absence: any) => (
+
+
+
+
+
+ {absence.type === "sick_leave" && "Malattia"}
+ {absence.type === "vacation" && "Ferie"}
+ {absence.type === "personal_leave" && "Permesso"}
+ {absence.type === "injury" && "Infortunio"}
+
+
+ Dal {format(new Date(absence.startDate), "dd MMM", { locale: it })} al{" "}
+ {format(new Date(absence.endDate), "dd MMM yyyy", { locale: it })}
+
+
+
+
+ {absence.isApproved ? "Approvata" : "In attesa"}
+
+
+
+
+
+
+ ))}
+ {absences.length === 0 && (
+
+ Nessuna assenza registrata
+
+ )}
+
+
+ );
+}
+
+function HolidaysTab() {
+ const { toast } = useToast();
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+ const currentYear = new Date().getFullYear();
+
+ const { data: holidays = [], isLoading } = useQuery({
+ queryKey: ["/api/holidays", currentYear],
+ queryFn: () => fetch(`/api/holidays?year=${currentYear}`).then((r) => r.json()),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (data: any) => apiRequest("/api/holidays", "POST", data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] });
+ toast({ title: "Festività creata" });
+ setIsCreateOpen(false);
+ },
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (id: string) => apiRequest(`/api/holidays/${id}`, "DELETE"),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/holidays", currentYear] });
+ toast({ title: "Festività eliminata" });
+ },
+ });
+
+ if (isLoading) {
+ return Caricamento festività...
;
+ }
+
+ return (
+
+
+
+
+
+
+ {holidays.map((holiday: any) => (
+
+
+
+
+ {holiday.name}
+
+ {format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })}
+
+
+
+
+ {holiday.isNational ? "Nazionale" : "Regionale"}
+
+
+
+
+
+
+ ))}
+ {holidays.length === 0 && (
+
+ Nessuna festività configurata per {currentYear}
+
+ )}
+
+
+ );
+}
diff --git a/client/src/pages/planning.tsx b/client/src/pages/planning.tsx
index b7f6202..2b1bc0d 100644
--- a/client/src/pages/planning.tsx
+++ b/client/src/pages/planning.tsx
@@ -1,480 +1,270 @@
import { useState } from "react";
-import { useQuery, useMutation } from "@tanstack/react-query";
-import { queryClient, apiRequest } from "@/lib/queryClient";
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
- DialogFooter,
-} from "@/components/ui/dialog";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { useToast } from "@/hooks/use-toast";
-import { Calendar, GraduationCap, HeartPulse, PartyPopper, Plus, Trash2 } from "lucide-react";
+import { Vehicle, Site, ShiftWithDetails } from "@shared/schema";
+import { Building2, Car, MapPin, Calendar } from "lucide-react";
import { format } from "date-fns";
import { it } from "date-fns/locale";
-export default function PlanningPage() {
- const { toast } = useToast();
- const [activeTab, setActiveTab] = useState("training");
+const locationLabels = {
+ all: "Tutte le Sedi",
+ roccapiemonte: "Roccapiemonte",
+ milano: "Milano",
+ roma: "Roma"
+} as const;
+
+const vehicleStatusLabels = {
+ available: "Disponibile",
+ in_use: "In uso",
+ maintenance: "In manutenzione",
+ out_of_service: "Fuori servizio",
+};
+
+const vehicleStatusColors = {
+ available: "bg-green-500/10 text-green-500 border-green-500/20",
+ in_use: "bg-blue-500/10 text-blue-500 border-blue-500/20",
+ maintenance: "bg-orange-500/10 text-orange-500 border-orange-500/20",
+ out_of_service: "bg-red-500/10 text-red-500 border-red-500/20",
+};
+
+export default function Planning() {
+ const [selectedLocation, setSelectedLocation] = useState("all");
+
+ const { data: vehicles = [], isLoading: loadingVehicles } = useQuery({
+ queryKey: ["/api/vehicles"],
+ });
+
+ const { data: sites = [], isLoading: loadingSites } = useQuery({
+ queryKey: ["/api/sites"],
+ });
+
+ const { data: shifts = [], isLoading: loadingShifts } = useQuery({
+ queryKey: ["/api/shifts/active"],
+ });
+
+ // Filter by location
+ const filteredVehicles = selectedLocation === "all"
+ ? vehicles
+ : vehicles.filter(v => v.location === selectedLocation);
+
+ const filteredSites = selectedLocation === "all"
+ ? sites
+ : sites.filter(s => s.location === selectedLocation);
+
+ const filteredShifts = selectedLocation === "all"
+ ? shifts
+ : shifts.filter(s => {
+ const site = sites.find(site => site.id === s.siteId);
+ return site?.location === selectedLocation;
+ });
+
+ const isLoading = loadingVehicles || loadingSites || loadingShifts;
+
+ // Calculate stats
+ const availableVehicles = filteredVehicles.filter(v => v.status === "available").length;
+ const inUseVehicles = filteredVehicles.filter(v => v.status === "in_use").length;
+ const activeSites = filteredSites.filter(s => s.isActive).length;
+ const activeShifts = filteredShifts.filter(s => s.status === "active").length;
return (
-
-
+
+
-
Pianificazione Avanzata
-
- Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni
+
Pianificazione Operativa
+
+ Vista per sede con gestione automezzi e turni attivi
+
+
+
+
-
-
-
-
- Formazione
-
-
-
- Assenze
-
-
-
- Festività
-
-
+ {isLoading ? (
+ Caricamento dati...
+ ) : (
+ <>
+ {/* Stats Summary */}
+
+
+
+ Turni Attivi
+
+
+
+ {activeShifts}
+
+ {locationLabels[selectedLocation as keyof typeof locationLabels]}
+
+
+
+
+
+
+ Siti Attivi
+
+
+
+ {activeSites}
+
+ su {filteredSites.length} totali
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function TrainingTab() {
- const { toast } = useToast();
- const [isCreateOpen, setIsCreateOpen] = useState(false);
-
- const { data: courses = [], isLoading } = useQuery({
- queryKey: ["/api/training-courses"],
- });
-
- const createMutation = useMutation({
- mutationFn: (data: any) => apiRequest("/api/training-courses", "POST", data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
- toast({ title: "Corso creato con successo" });
- setIsCreateOpen(false);
- },
- });
-
- const deleteMutation = useMutation({
- mutationFn: (id: string) => apiRequest(`/api/training-courses/${id}`, "DELETE"),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["/api/training-courses"] });
- toast({ title: "Corso eliminato" });
- },
- });
-
- if (isLoading) {
- return Caricamento corsi...
;
- }
-
- return (
-
-
-