Update `advanced-planning.tsx` to properly fetch and display training courses and absences. Modify `services.tsx` to enable adding and editing services with form validation and mutation logic. 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/B8lcojv
482 lines
18 KiB
TypeScript
482 lines
18 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
|
import type { TrainingCourse, Absence } from "@shared/schema";
|
|
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">Gestione Pianificazioni</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<TrainingCourse[]>({
|
|
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<Absence[]>({
|
|
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>
|
|
);
|
|
}
|