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 ( +
+
+ + + + + + + Nuovo Corso di Formazione + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + createMutation.mutate({ + guardId: formData.get("guardId"), + courseName: formData.get("courseName"), + courseType: formData.get("courseType"), + scheduledDate: formData.get("scheduledDate"), + status: "scheduled", + }); + }} + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+ {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 ( +
+
+ + + + + + + Registra Assenza + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + createMutation.mutate({ + guardId: formData.get("guardId"), + type: formData.get("type"), + startDate: formData.get("startDate"), + endDate: formData.get("endDate"), + notes: formData.get("notes"), + isApproved: false, + needsSubstitute: true, + }); + }} + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+ {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 ( +
+
+ + + + + + + Nuova Festività + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const dateValue = formData.get("date") as string; + createMutation.mutate({ + name: formData.get("name"), + date: dateValue, + year: new Date(dateValue).getFullYear(), + isNational: formData.get("isNational") === "true", + }); + }} + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ +
+ {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 ( -
-
- - - - - - - Nuovo Corso di Formazione - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - createMutation.mutate({ - guardId: formData.get("guardId"), - courseName: formData.get("courseName"), - courseType: formData.get("courseType"), - scheduledDate: formData.get("scheduledDate"), - status: "scheduled", - }); - }} - > -
-
- - + + + Automezzi Disponibili + + + +
+ {availableVehicles}
-
- - -
-
- - -
-
- - -
-
- - - - - -
-
+

+ su {filteredVehicles.length} totali +

+ + -
- {courses.map((course: any) => ( - - -
-
- {course.courseName} - - Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"} - + + + Automezzi in Uso + + + +
+ {inUseVehicles}
-
- - {course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"} - - - {course.status} - - -
-
- - - ))} - {courses.length === 0 && ( -
- Nessun corso di formazione programmato +

+ assegnati a turni +

+ +
- )} -
-
- ); -} -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 ( -
-
- - - - - - - Registra Assenza - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - createMutation.mutate({ - guardId: formData.get("guardId"), - type: formData.get("type"), - startDate: formData.get("startDate"), - endDate: formData.get("endDate"), - notes: formData.get("notes"), - isApproved: false, - needsSubstitute: true, - }); - }} - > -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - -
-
-
-
- -
- {absences.map((absence: any) => ( - + {/* Vehicles List */} + -
-
- - {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"} - - -
-
+ Automezzi - {locationLabels[selectedLocation as keyof typeof locationLabels]} + + Stato e disponibilità automezzi per sede selezionata +
-
- ))} - {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 ( -
-
- - - - - - - Nuova Festività - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const dateValue = formData.get("date") as string; - createMutation.mutate({ - name: formData.get("name"), - date: dateValue, - year: new Date(dateValue).getFullYear(), - isNational: formData.get("isNational") === "true", - }); - }} - > -
-
- - -
-
- - -
-
- - -
+ +
+ {filteredVehicles.map((vehicle) => ( +
+
+
+ +
+
+

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

+

+ {vehicle.licensePlate} + {vehicle.year && ` • Anno ${vehicle.year}`} +

+
+
+
+ + {locationLabels[vehicle.location as keyof typeof locationLabels]} + + + {vehicleStatusLabels[vehicle.status]} + +
+
+ ))} + {filteredVehicles.length === 0 && ( +
+ +

Nessun automezzo disponibile per questa sede

+
+ )}
- - - - - -
-
+ + -
- {holidays.map((holiday: any) => ( - + {/* Active Shifts */} + -
-
- {holiday.name} - - {format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })} - -
-
- - {holiday.isNational ? "Nazionale" : "Regionale"} - - -
-
+ Turni Attivi - {locationLabels[selectedLocation as keyof typeof locationLabels]} + + Turni in corso per sede selezionata +
+ +
+ {filteredShifts.filter(s => s.status === "active").map((shift) => { + const site = sites.find(s => s.id === shift.siteId); + + return ( +
+
+
+ +
+
+

+ {site?.name || "Sito sconosciuto"} +

+

+ {format(new Date(shift.startTime), "HH:mm", { locale: it })} - + {format(new Date(shift.endTime), "HH:mm", { locale: it })} +

+
+
+
+ + {site && locationLabels[site.location as keyof typeof locationLabels]} + + + Attivo + +
+
+ ); + })} + {filteredShifts.filter(s => s.status === "active").length === 0 && ( +
+ +

Nessun turno attivo per questa sede

+
+ )} +
+
- ))} - {holidays.length === 0 && ( -
- Nessuna festività configurata per {currentYear} -
- )} -
+ + )}
); }