VigilanzaTurni/client/src/pages/guards.tsx
marco370 a300d18489 Add editing capabilities for guards, sites, and shifts
Implement PATCH endpoints for updating guards, sites, and shifts, along with UI elements and form handling for editing existing records across the application.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/Iga2bds
2025-10-11 15:58:34 +00:00

477 lines
19 KiB
TypeScript

import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { GuardWithCertifications, InsertGuard, InsertCertification } from "@shared/schema";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertGuardSchema, insertCertificationSchema } from "@shared/schema";
import { Plus, Shield, Check, X, AlertCircle, Pencil } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { StatusBadge } from "@/components/status-badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { format } from "date-fns";
export default function Guards() {
const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingGuard, setEditingGuard] = useState<GuardWithCertifications | null>(null);
const { data: guards, isLoading } = useQuery<GuardWithCertifications[]>({
queryKey: ["/api/guards"],
});
const form = useForm<InsertGuard>({
resolver: zodResolver(insertGuardSchema),
defaultValues: {
badgeNumber: "",
phoneNumber: "",
isArmed: false,
hasFireSafety: false,
hasFirstAid: false,
hasDriverLicense: false,
languages: [],
},
});
const editForm = useForm<InsertGuard>({
resolver: zodResolver(insertGuardSchema),
defaultValues: {
badgeNumber: "",
phoneNumber: "",
isArmed: false,
hasFireSafety: false,
hasFirstAid: false,
hasDriverLicense: false,
languages: [],
},
});
const createMutation = useMutation({
mutationFn: async (data: InsertGuard) => {
return await apiRequest("POST", "/api/guards", data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/guards"] });
toast({
title: "Guardia creata",
description: "La guardia è stata aggiunta con successo",
});
setIsDialogOpen(false);
form.reset();
},
onError: (error) => {
toast({
title: "Errore",
description: error.message,
variant: "destructive",
});
},
});
const updateMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: InsertGuard }) => {
return await apiRequest("PATCH", `/api/guards/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/guards"] });
toast({
title: "Guardia aggiornata",
description: "I dati della guardia sono stati aggiornati",
});
setEditingGuard(null);
editForm.reset();
},
onError: (error) => {
toast({
title: "Errore",
description: error.message,
variant: "destructive",
});
},
});
const onSubmit = (data: InsertGuard) => {
createMutation.mutate(data);
};
const onEditSubmit = (data: InsertGuard) => {
if (editingGuard) {
updateMutation.mutate({ id: editingGuard.id, data });
}
};
const openEditDialog = (guard: GuardWithCertifications) => {
setEditingGuard(guard);
editForm.reset({
badgeNumber: guard.badgeNumber,
phoneNumber: guard.phoneNumber || "",
isArmed: guard.isArmed,
hasFireSafety: guard.hasFireSafety,
hasFirstAid: guard.hasFirstAid,
hasDriverLicense: guard.hasDriverLicense,
languages: guard.languages || [],
});
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-semibold mb-2">Gestione Guardie</h1>
<p className="text-muted-foreground">
Anagrafica guardie con competenze e certificazioni
</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button data-testid="button-add-guard">
<Plus className="h-4 w-4 mr-2" />
Aggiungi Guardia
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Nuova Guardia</DialogTitle>
<DialogDescription>
Inserisci i dati della nuova guardia giurata
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="badgeNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Matricola</FormLabel>
<FormControl>
<Input placeholder="GPV-001" {...field} data-testid="input-badge-number" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Telefono</FormLabel>
<FormControl>
<Input placeholder="+39 123 456 7890" {...field} value={field.value || ""} data-testid="input-phone" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<p className="text-sm font-medium">Competenze</p>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="isArmed"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Armato</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-armed" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="hasFireSafety"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Antincendio</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-fire" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="hasFirstAid"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Primo Soccorso</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-first-aid" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="hasDriverLicense"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Patente</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-driver" />
</FormControl>
</FormItem>
)}
/>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
className="flex-1"
data-testid="button-cancel"
>
Annulla
</Button>
<Button
type="submit"
className="flex-1"
disabled={createMutation.isPending}
data-testid="button-submit-guard"
>
{createMutation.isPending ? "Creazione..." : "Crea Guardia"}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
{/* Edit Guard Dialog */}
<Dialog open={!!editingGuard} onOpenChange={(open) => !open && setEditingGuard(null)}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Modifica Guardia</DialogTitle>
<DialogDescription>
Modifica i dati della guardia {editingGuard?.user?.firstName} {editingGuard?.user?.lastName}
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="badgeNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Matricola</FormLabel>
<FormControl>
<Input placeholder="GPV-001" {...field} data-testid="input-edit-badge-number" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Telefono</FormLabel>
<FormControl>
<Input placeholder="+39 123 456 7890" {...field} value={field.value || ""} data-testid="input-edit-phone" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<p className="text-sm font-medium">Competenze</p>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="isArmed"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Armato</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-armed" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="hasFireSafety"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Antincendio</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-fire" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="hasFirstAid"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Primo Soccorso</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-first-aid" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="hasDriverLicense"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-md border p-3">
<FormLabel className="mb-0">Patente</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} data-testid="switch-edit-driver" />
</FormControl>
</FormItem>
)}
/>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setEditingGuard(null)}
className="flex-1"
data-testid="button-edit-cancel"
>
Annulla
</Button>
<Button
type="submit"
className="flex-1"
disabled={updateMutation.isPending}
data-testid="button-submit-edit-guard"
>
{updateMutation.isPending ? "Aggiornamento..." : "Salva Modifiche"}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
{isLoading ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-64" />
<Skeleton className="h-64" />
<Skeleton className="h-64" />
</div>
) : guards && guards.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{guards.map((guard) => (
<Card key={guard.id} className="hover-elevate" data-testid={`card-guard-${guard.id}`}>
<CardHeader className="pb-3">
<div className="flex items-start gap-3">
<Avatar>
<AvatarImage src={guard.user?.profileImageUrl || undefined} />
<AvatarFallback>
{guard.user?.firstName?.[0]}{guard.user?.lastName?.[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<CardTitle className="text-lg truncate">
{guard.user?.firstName} {guard.user?.lastName}
</CardTitle>
<CardDescription className="font-mono text-xs">
{guard.badgeNumber}
</CardDescription>
</div>
<Button
size="icon"
variant="ghost"
onClick={() => openEditDialog(guard)}
data-testid={`button-edit-guard-${guard.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-1">
{guard.isArmed && (
<Badge variant="secondary" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
{guard.hasFirstAid && (
<Badge variant="secondary" className="text-xs">
<Check className="h-3 w-3 mr-1" />
Primo Soccorso
</Badge>
)}
{guard.hasFireSafety && (
<Badge variant="secondary" className="text-xs">
<Check className="h-3 w-3 mr-1" />
Antincendio
</Badge>
)}
{guard.hasDriverLicense && (
<Badge variant="secondary" className="text-xs">
<Check className="h-3 w-3 mr-1" />
Patente
</Badge>
)}
</div>
{guard.certifications.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Certificazioni</p>
{guard.certifications.slice(0, 2).map((cert) => (
<div key={cert.id} className="flex items-center justify-between text-xs">
<span className="truncate flex-1">{cert.name}</span>
<StatusBadge
status={cert.status === "valid" ? "active" : cert.status === "expiring_soon" ? "late" : "emergency"}
className="ml-2"
>
{cert.status === "valid" ? "Valido" : cert.status === "expiring_soon" ? "In scadenza" : "Scaduto"}
</StatusBadge>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<Shield className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-lg font-medium mb-2">Nessuna guardia presente</p>
<p className="text-sm text-muted-foreground mb-4">
Inizia aggiungendo la prima guardia al sistema
</p>
</CardContent>
</Card>
)}
</div>
);
}