Introduce an advanced planning page and improve existing planning features

Replaces the existing planning page with an advanced version, adds a new route and sidebar link for advanced planning, and refactors the planning page component to support new functionalities.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/kxc8yZp
This commit is contained in:
marco370 2025-10-17 08:00:17 +00:00
parent 5c6bf0ca4d
commit 7ab5ae65a7
4 changed files with 731 additions and 453 deletions

View File

@ -17,10 +17,11 @@ import Shifts from "@/pages/shifts";
import Reports from "@/pages/reports"; import Reports from "@/pages/reports";
import Notifications from "@/pages/notifications"; import Notifications from "@/pages/notifications";
import Users from "@/pages/users"; import Users from "@/pages/users";
import Planning from "@/pages/planning"; import AdvancedPlanning from "@/pages/advanced-planning";
import Vehicles from "@/pages/vehicles"; import Vehicles from "@/pages/vehicles";
import Parameters from "@/pages/parameters"; import Parameters from "@/pages/parameters";
import Services from "@/pages/services"; import Services from "@/pages/services";
import Planning from "@/pages/planning";
function Router() { function Router() {
const { isAuthenticated, isLoading } = useAuth(); const { isAuthenticated, isLoading } = useAuth();
@ -39,6 +40,7 @@ function Router() {
<Route path="/vehicles" component={Vehicles} /> <Route path="/vehicles" component={Vehicles} />
<Route path="/shifts" component={Shifts} /> <Route path="/shifts" component={Shifts} />
<Route path="/planning" component={Planning} /> <Route path="/planning" component={Planning} />
<Route path="/advanced-planning" component={AdvancedPlanning} />
<Route path="/reports" component={Reports} /> <Route path="/reports" component={Reports} />
<Route path="/notifications" component={Notifications} /> <Route path="/notifications" component={Notifications} />
<Route path="/users" component={Users} /> <Route path="/users" component={Users} />

View File

@ -49,6 +49,12 @@ const menuItems = [
icon: ClipboardList, icon: ClipboardList,
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],
}, },
{
title: "Pianificazione Avanzata",
url: "/advanced-planning",
icon: ClipboardList,
roles: ["admin", "coordinator"],
},
{ {
title: "Guardie", title: "Guardie",
url: "/guards", url: "/guards",

View File

@ -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 (
<div className="flex flex-col gap-6 p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Pianificazione Avanzata</h1>
<p className="text-muted-foreground mt-1">
Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="training" data-testid="tab-training">
<GraduationCap className="h-4 w-4 mr-2" />
Formazione
</TabsTrigger>
<TabsTrigger value="absences" data-testid="tab-absences">
<HeartPulse className="h-4 w-4 mr-2" />
Assenze
</TabsTrigger>
<TabsTrigger value="holidays" data-testid="tab-holidays">
<PartyPopper className="h-4 w-4 mr-2" />
Festività
</TabsTrigger>
</TabsList>
<TabsContent value="training">
<TrainingTab />
</TabsContent>
<TabsContent value="absences">
<AbsencesTab />
</TabsContent>
<TabsContent value="holidays">
<HolidaysTab />
</TabsContent>
</Tabs>
</div>
);
}
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 <div className="text-center py-8">Caricamento corsi...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-course">
<Plus className="h-4 w-4 mr-2" />
Nuovo Corso
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuovo Corso di Formazione</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
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",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="courseName">Nome Corso</Label>
<Input id="courseName" name="courseName" required data-testid="input-course-name" />
</div>
<div>
<Label htmlFor="courseType">Tipo</Label>
<Select name="courseType" required>
<SelectTrigger data-testid="select-course-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mandatory">Obbligatorio</SelectItem>
<SelectItem value="optional">Facoltativo</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-guard-id" />
</div>
<div>
<Label htmlFor="scheduledDate">Data Programmata</Label>
<Input id="scheduledDate" name="scheduledDate" type="date" required data-testid="input-scheduled-date" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-course">
{createMutation.isPending ? "Creazione..." : "Crea Corso"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{courses.map((course: any) => (
<Card key={course.id} data-testid={`card-course-${course.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{course.courseName}</CardTitle>
<CardDescription>
Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={course.courseType === "mandatory" ? "default" : "secondary"}>
{course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"}
</Badge>
<Badge variant={course.status === "completed" ? "default" : "secondary"}>
{course.status}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(course.id)}
data-testid={`button-delete-course-${course.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{courses.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessun corso di formazione programmato
</div>
)}
</div>
</div>
);
}
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 <div className="text-center py-8">Caricamento assenze...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-absence">
<Plus className="h-4 w-4 mr-2" />
Nuova Assenza
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Registra Assenza</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
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,
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-absence-guard-id" />
</div>
<div>
<Label htmlFor="type">Tipo Assenza</Label>
<Select name="type" required>
<SelectTrigger data-testid="select-absence-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sick_leave">Malattia</SelectItem>
<SelectItem value="vacation">Ferie</SelectItem>
<SelectItem value="personal_leave">Permesso</SelectItem>
<SelectItem value="injury">Infortunio</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="startDate">Data Inizio</Label>
<Input id="startDate" name="startDate" type="date" required data-testid="input-start-date" />
</div>
<div>
<Label htmlFor="endDate">Data Fine</Label>
<Input id="endDate" name="endDate" type="date" required data-testid="input-end-date" />
</div>
<div>
<Label htmlFor="notes">Note</Label>
<Input id="notes" name="notes" data-testid="input-absence-notes" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-absence">
{createMutation.isPending ? "Registrazione..." : "Registra Assenza"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{absences.map((absence: any) => (
<Card key={absence.id} data-testid={`card-absence-${absence.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">
{absence.type === "sick_leave" && "Malattia"}
{absence.type === "vacation" && "Ferie"}
{absence.type === "personal_leave" && "Permesso"}
{absence.type === "injury" && "Infortunio"}
</CardTitle>
<CardDescription>
Dal {format(new Date(absence.startDate), "dd MMM", { locale: it })} al{" "}
{format(new Date(absence.endDate), "dd MMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={absence.isApproved ? "default" : "secondary"}>
{absence.isApproved ? "Approvata" : "In attesa"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(absence.id)}
data-testid={`button-delete-absence-${absence.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{absences.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessuna assenza registrata
</div>
)}
</div>
</div>
);
}
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 <div className="text-center py-8">Caricamento festività...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-holiday">
<Plus className="h-4 w-4 mr-2" />
Nuova Festività
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuova Festività</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
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",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="name">Nome Festività</Label>
<Input id="name" name="name" required data-testid="input-holiday-name" placeholder="es. Natale, 1° Maggio..." />
</div>
<div>
<Label htmlFor="date">Data</Label>
<Input id="date" name="date" type="date" required data-testid="input-holiday-date" />
</div>
<div>
<Label htmlFor="isNational">Tipo</Label>
<Select name="isNational" defaultValue="true">
<SelectTrigger data-testid="select-holiday-type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Nazionale</SelectItem>
<SelectItem value="false">Regionale</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-holiday">
{createMutation.isPending ? "Creazione..." : "Crea Festività"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{holidays.map((holiday: any) => (
<Card key={holiday.id} data-testid={`card-holiday-${holiday.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{holiday.name}</CardTitle>
<CardDescription>
{format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={holiday.isNational ? "default" : "secondary"}>
{holiday.isNational ? "Nazionale" : "Regionale"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(holiday.id)}
data-testid={`button-delete-holiday-${holiday.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{holidays.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessuna festività configurata per {currentYear}
</div>
)}
</div>
</div>
);
}

View File

@ -1,480 +1,270 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
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 { Badge } from "@/components/ui/badge";
import { import { Vehicle, Site, ShiftWithDetails } from "@shared/schema";
Dialog, import { Building2, Car, MapPin, Calendar } from "lucide-react";
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 { format } from "date-fns";
import { it } from "date-fns/locale"; import { it } from "date-fns/locale";
export default function PlanningPage() { const locationLabels = {
const { toast } = useToast(); all: "Tutte le Sedi",
const [activeTab, setActiveTab] = useState("training"); 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<string>("all");
const { data: vehicles = [], isLoading: loadingVehicles } = useQuery<Vehicle[]>({
queryKey: ["/api/vehicles"],
});
const { data: sites = [], isLoading: loadingSites } = useQuery<Site[]>({
queryKey: ["/api/sites"],
});
const { data: shifts = [], isLoading: loadingShifts } = useQuery<ShiftWithDetails[]>({
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 ( return (
<div className="flex flex-col gap-6 p-8"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Pianificazione Avanzata</h1> <h1 className="text-3xl font-semibold mb-2">Pianificazione Operativa</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground">
Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni Vista per sede con gestione automezzi e turni attivi
</p> </p>
</div> </div>
</div> <div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-muted-foreground" />
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Select value={selectedLocation} onValueChange={setSelectedLocation}>
<TabsList className="grid w-full grid-cols-3"> <SelectTrigger className="w-[200px]" data-testid="select-location">
<TabsTrigger value="training" data-testid="tab-training">
<GraduationCap className="h-4 w-4 mr-2" />
Formazione
</TabsTrigger>
<TabsTrigger value="absences" data-testid="tab-absences">
<HeartPulse className="h-4 w-4 mr-2" />
Assenze
</TabsTrigger>
<TabsTrigger value="holidays" data-testid="tab-holidays">
<PartyPopper className="h-4 w-4 mr-2" />
Festività
</TabsTrigger>
</TabsList>
<TabsContent value="training">
<TrainingTab />
</TabsContent>
<TabsContent value="absences">
<AbsencesTab />
</TabsContent>
<TabsContent value="holidays">
<HolidaysTab />
</TabsContent>
</Tabs>
</div>
);
}
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 <div className="text-center py-8">Caricamento corsi...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-course">
<Plus className="h-4 w-4 mr-2" />
Nuovo Corso
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuovo Corso di Formazione</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
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",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="courseName">Nome Corso</Label>
<Input id="courseName" name="courseName" required data-testid="input-course-name" />
</div>
<div>
<Label htmlFor="courseType">Tipo</Label>
<Select name="courseType" required>
<SelectTrigger data-testid="select-course-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mandatory">Obbligatorio</SelectItem>
<SelectItem value="optional">Facoltativo</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-guard-id" />
</div>
<div>
<Label htmlFor="scheduledDate">Data Programmata</Label>
<Input id="scheduledDate" name="scheduledDate" type="date" required data-testid="input-scheduled-date" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-course">
{createMutation.isPending ? "Creazione..." : "Crea Corso"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{courses.map((course: any) => (
<Card key={course.id} data-testid={`card-course-${course.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{course.courseName}</CardTitle>
<CardDescription>
Programmato: {course.scheduledDate ? format(new Date(course.scheduledDate), "dd MMM yyyy", { locale: it }) : "N/D"}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={course.courseType === "mandatory" ? "default" : "secondary"}>
{course.courseType === "mandatory" ? "Obbligatorio" : "Facoltativo"}
</Badge>
<Badge variant={course.status === "completed" ? "default" : "secondary"}>
{course.status}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(course.id)}
data-testid={`button-delete-course-${course.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{courses.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessun corso di formazione programmato
</div>
)}
</div>
</div>
);
}
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 <div className="text-center py-8">Caricamento assenze...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-absence">
<Plus className="h-4 w-4 mr-2" />
Nuova Assenza
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Registra Assenza</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
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,
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="guardId">ID Guardia</Label>
<Input id="guardId" name="guardId" required data-testid="input-absence-guard-id" />
</div>
<div>
<Label htmlFor="type">Tipo Assenza</Label>
<Select name="type" required>
<SelectTrigger data-testid="select-absence-type">
<SelectValue placeholder="Seleziona tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sick_leave">Malattia</SelectItem>
<SelectItem value="vacation">Ferie</SelectItem>
<SelectItem value="personal_leave">Permesso</SelectItem>
<SelectItem value="injury">Infortunio</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="startDate">Data Inizio</Label>
<Input id="startDate" name="startDate" type="date" required data-testid="input-start-date" />
</div>
<div>
<Label htmlFor="endDate">Data Fine</Label>
<Input id="endDate" name="endDate" type="date" required data-testid="input-end-date" />
</div>
<div>
<Label htmlFor="notes">Note</Label>
<Input id="notes" name="notes" data-testid="input-absence-notes" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-absence">
{createMutation.isPending ? "Registrazione..." : "Registra Assenza"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{absences.map((absence: any) => (
<Card key={absence.id} data-testid={`card-absence-${absence.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">
{absence.type === "sick_leave" && "Malattia"}
{absence.type === "vacation" && "Ferie"}
{absence.type === "personal_leave" && "Permesso"}
{absence.type === "injury" && "Infortunio"}
</CardTitle>
<CardDescription>
Dal {format(new Date(absence.startDate), "dd MMM", { locale: it })} al{" "}
{format(new Date(absence.endDate), "dd MMM yyyy", { locale: it })}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={absence.isApproved ? "default" : "secondary"}>
{absence.isApproved ? "Approvata" : "In attesa"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(absence.id)}
data-testid={`button-delete-absence-${absence.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
{absences.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessuna assenza registrata
</div>
)}
</div>
</div>
);
}
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 <div className="text-center py-8">Caricamento festività...</div>;
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button data-testid="button-create-holiday">
<Plus className="h-4 w-4 mr-2" />
Nuova Festività
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nuova Festività</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
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",
});
}}
>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="name">Nome Festività</Label>
<Input id="name" name="name" required data-testid="input-holiday-name" placeholder="es. Natale, 1° Maggio..." />
</div>
<div>
<Label htmlFor="date">Data</Label>
<Input id="date" name="date" type="date" required data-testid="input-holiday-date" />
</div>
<div>
<Label htmlFor="isNational">Tipo</Label>
<Select name="isNational" defaultValue="true">
<SelectTrigger data-testid="select-holiday-type">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="true">Nazionale</SelectItem> <SelectItem value="all">Tutte le Sedi</SelectItem>
<SelectItem value="false">Regionale</SelectItem> <SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending} data-testid="button-submit-holiday"> {isLoading ? (
{createMutation.isPending ? "Creazione..." : "Crea Festività"} <div className="text-center py-8 text-muted-foreground">Caricamento dati...</div>
</Button> ) : (
</DialogFooter> <>
</form> {/* Stats Summary */}
</DialogContent> <div className="grid gap-4 md:grid-cols-4">
</Dialog> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Turni Attivi</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-active-shifts">{activeShifts}</div>
<p className="text-xs text-muted-foreground">
{locationLabels[selectedLocation as keyof typeof locationLabels]}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Siti Attivi</CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-active-sites">{activeSites}</div>
<p className="text-xs text-muted-foreground">
su {filteredSites.length} totali
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Automezzi Disponibili</CardTitle>
<Car className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-500" data-testid="text-available-vehicles">
{availableVehicles}
</div>
<p className="text-xs text-muted-foreground">
su {filteredVehicles.length} totali
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Automezzi in Uso</CardTitle>
<Car className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-500" data-testid="text-inuse-vehicles">
{inUseVehicles}
</div>
<p className="text-xs text-muted-foreground">
assegnati a turni
</p>
</CardContent>
</Card>
</div> </div>
<div className="grid gap-4"> {/* Vehicles List */}
{holidays.map((holiday: any) => ( <Card>
<Card key={holiday.id} data-testid={`card-holiday-${holiday.id}`}>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <CardTitle>Automezzi - {locationLabels[selectedLocation as keyof typeof locationLabels]}</CardTitle>
<div>
<CardTitle className="text-lg">{holiday.name}</CardTitle>
<CardDescription> <CardDescription>
{format(new Date(holiday.date), "EEEE dd MMMM yyyy", { locale: it })} Stato e disponibilità automezzi per sede selezionata
</CardDescription> </CardDescription>
</div>
<div className="flex gap-2 items-center">
<Badge variant={holiday.isNational ? "default" : "secondary"}>
{holiday.isNational ? "Nazionale" : "Regionale"}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(holiday.id)}
data-testid={`button-delete-holiday-${holiday.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader> </CardHeader>
</Card> <CardContent>
<div className="space-y-4">
{filteredVehicles.map((vehicle) => (
<div
key={vehicle.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card hover-elevate"
data-testid={`card-vehicle-${vehicle.id}`}
>
<div className="flex items-center gap-4">
<div className="p-2 rounded-lg bg-muted">
<Car className="h-6 w-6" />
</div>
<div>
<p className="font-semibold" data-testid={`text-vehicle-${vehicle.id}`}>
{vehicle.brand} {vehicle.model}
</p>
<p className="text-sm text-muted-foreground">
{vehicle.licensePlate}
{vehicle.year && ` • Anno ${vehicle.year}`}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="outline" className="capitalize">
{locationLabels[vehicle.location as keyof typeof locationLabels]}
</Badge>
<Badge
variant="outline"
className={vehicleStatusColors[vehicle.status]}
data-testid={`badge-status-${vehicle.id}`}
>
{vehicleStatusLabels[vehicle.status]}
</Badge>
</div>
</div>
))} ))}
{holidays.length === 0 && ( {filteredVehicles.length === 0 && (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
Nessuna festività configurata per {currentYear} <Car className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nessun automezzo disponibile per questa sede</p>
</div> </div>
)} )}
</div> </div>
</CardContent>
</Card>
{/* Active Shifts */}
<Card>
<CardHeader>
<CardTitle>Turni Attivi - {locationLabels[selectedLocation as keyof typeof locationLabels]}</CardTitle>
<CardDescription>
Turni in corso per sede selezionata
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{filteredShifts.filter(s => s.status === "active").map((shift) => {
const site = sites.find(s => s.id === shift.siteId);
return (
<div
key={shift.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card hover-elevate"
data-testid={`card-shift-${shift.id}`}
>
<div className="flex items-center gap-4">
<div className="p-2 rounded-lg bg-muted">
<Calendar className="h-6 w-6" />
</div>
<div>
<p className="font-semibold" data-testid={`text-shift-site-${shift.id}`}>
{site?.name || "Sito sconosciuto"}
</p>
<p className="text-sm text-muted-foreground">
{format(new Date(shift.startTime), "HH:mm", { locale: it })} -
{format(new Date(shift.endTime), "HH:mm", { locale: it })}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="outline" className="capitalize">
{site && locationLabels[site.location as keyof typeof locationLabels]}
</Badge>
<Badge variant="default">
Attivo
</Badge>
</div>
</div>
);
})}
{filteredShifts.filter(s => s.status === "active").length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nessun turno attivo per questa sede</p>
</div>
)}
</div>
</CardContent>
</Card>
</>
)}
</div> </div>
); );
} }