Compare commits

..

4 Commits

Author SHA1 Message Date
Marco Lanzara
4693b782cb 🚀 Release v1.0.8
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.8_20251017_083612.sql.gz
- Data: 2025-10-17 08:36:24
2025-10-17 08:36:24 +00:00
marco370
3f6556392a Improve management of services, schedules, and data exports
Address issues with vehicle data insertion, service management, and database export functionality.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/B8lcojv
2025-10-17 08:33:42 +00:00
marco370
5c15e6ad27 Add ability to manage services and improve planning page
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
2025-10-17 08:33:07 +00:00
marco370
2b53650f1b Rename advanced planning page and set default vehicle location
Update sidebar navigation and page title from "Pianificazione Avanzata" to "Gestione Pianificazioni". Add a default 'roccapiemonte' location to the vehicle model in client/src/pages/vehicles.tsx.

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
2025-10-17 08:25:32 +00:00
6 changed files with 677 additions and 126 deletions

View File

@ -27,6 +27,10 @@ externalPort = 3000
localPort = 42175 localPort = 42175
externalPort = 3002 externalPort = 3002
[[ports]]
localPort = 42403
externalPort = 3003
[env] [env]
PORT = "5000" PORT = "5000"

View File

@ -50,7 +50,7 @@ const menuItems = [
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],
}, },
{ {
title: "Pianificazione Avanzata", title: "Gestione Pianificazioni",
url: "/advanced-planning", url: "/advanced-planning",
icon: ClipboardList, icon: ClipboardList,
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],

View File

@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { queryClient, apiRequest } from "@/lib/queryClient";
import type { TrainingCourse, Absence } from "@shared/schema";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -35,7 +36,7 @@ export default function PlanningPage() {
<div className="flex flex-col gap-6 p-8"> <div className="flex flex-col gap-6 p-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Pianificazione Avanzata</h1> <h1 className="text-3xl font-bold tracking-tight">Gestione Pianificazioni</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni Gestisci formazione, assenze e festività per ottimizzare la pianificazione turni
</p> </p>
@ -78,7 +79,7 @@ function TrainingTab() {
const { toast } = useToast(); const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false); const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: courses = [], isLoading } = useQuery({ const { data: courses = [], isLoading } = useQuery<TrainingCourse[]>({
queryKey: ["/api/training-courses"], queryKey: ["/api/training-courses"],
}); });
@ -211,7 +212,7 @@ function AbsencesTab() {
const { toast } = useToast(); const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false); const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: absences = [], isLoading } = useQuery({ const { data: absences = [], isLoading } = useQuery<Absence[]>({
queryKey: ["/api/absences"], queryKey: ["/api/absences"],
}); });

View File

@ -1,8 +1,41 @@
import { useQuery } from "@tanstack/react-query"; import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertSiteSchema, type Site } from "@shared/schema";
import { z } from "zod";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Site } from "@shared/schema"; import { Button } from "@/components/ui/button";
import { Building2, Shield, Eye, MapPin, Zap } from "lucide-react"; import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import { Building2, Shield, Eye, MapPin, Zap, Plus, Pencil } from "lucide-react";
const serviceTypeInfo = { const serviceTypeInfo = {
fixed_post: { fixed_post: {
@ -31,11 +64,124 @@ const serviceTypeInfo = {
} }
} as const; } as const;
type SiteForm = z.infer<typeof insertSiteSchema>;
export default function Services() { export default function Services() {
const { toast } = useToast();
const [selectedServiceType, setSelectedServiceType] = useState<string | null>(null);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const { data: sites = [], isLoading } = useQuery<Site[]>({ const { data: sites = [], isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"], queryKey: ["/api/sites"],
}); });
const createForm = useForm<SiteForm>({
resolver: zodResolver(insertSiteSchema),
defaultValues: {
name: "",
location: "roccapiemonte",
address: "",
clientId: null,
shiftType: "fixed_post",
minGuards: 1,
requiresArmed: false,
requiresDriverLicense: false,
isActive: true,
},
});
const editForm = useForm<SiteForm>({
resolver: zodResolver(insertSiteSchema),
defaultValues: {
name: "",
location: "roccapiemonte",
address: "",
clientId: null,
shiftType: "fixed_post",
minGuards: 1,
requiresArmed: false,
requiresDriverLicense: false,
isActive: true,
},
});
const createSiteMutation = useMutation({
mutationFn: async (data: SiteForm) => {
return apiRequest("POST", "/api/sites", data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/sites"] });
toast({
title: "Sito creato",
description: "Il sito è stato aggiunto con successo.",
});
setCreateDialogOpen(false);
createForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile creare il sito.",
variant: "destructive",
});
},
});
const updateSiteMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: SiteForm }) => {
return apiRequest("PATCH", `/api/sites/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/sites"] });
toast({
title: "Sito aggiornato",
description: "Il sito è stato modificato con successo.",
});
setEditDialogOpen(false);
setSelectedSite(null);
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile aggiornare il sito.",
variant: "destructive",
});
},
});
const handleCreateSite = (serviceType: string) => {
createForm.reset({
name: "",
location: "roccapiemonte",
address: "",
clientId: null,
shiftType: serviceType as any,
minGuards: 1,
requiresArmed: false,
requiresDriverLicense: false,
isActive: true,
});
setCreateDialogOpen(true);
};
const handleEditSite = (site: Site) => {
setSelectedSite(site);
editForm.reset({
name: site.name,
location: site.location,
address: site.address || "",
clientId: site.clientId,
shiftType: site.shiftType,
minGuards: site.minGuards,
requiresArmed: site.requiresArmed || false,
requiresDriverLicense: site.requiresDriverLicense || false,
isActive: site.isActive,
});
setEditDialogOpen(true);
};
// Calculate statistics per service type // Calculate statistics per service type
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => { const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
const sitesForType = sites.filter(s => s.shiftType === type); const sitesForType = sites.filter(s => s.shiftType === type);
@ -48,142 +194,535 @@ export default function Services() {
return acc; return acc;
}, {} as Record<string, any>); }, {} as Record<string, any>);
const filteredSites = selectedServiceType
? sites.filter(s => s.shiftType === selectedServiceType)
: [];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div className="flex items-center justify-between">
<h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1> <div>
<p className="text-muted-foreground"> <h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1>
Panoramica tipologie di servizio e relative configurazioni <p className="text-muted-foreground">
</p> Panoramica tipologie di servizio e relative configurazioni
</p>
</div>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="text-center py-8 text-muted-foreground">Caricamento servizi...</div> <div className="text-center py-8 text-muted-foreground">Caricamento servizi...</div>
) : ( ) : (
<div className="grid gap-6 md:grid-cols-2"> <>
{Object.entries(serviceTypeInfo).map(([type, info]) => { <div className="grid gap-6 md:grid-cols-2">
const Icon = info.icon; {Object.entries(serviceTypeInfo).map(([type, info]) => {
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 }; const Icon = info.icon;
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
return (
<Card key={type} data-testid={`card-service-${type}`}> return (
<CardHeader> <Card key={type} data-testid={`card-service-${type}`}>
<div className="flex items-start justify-between"> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-start justify-between">
<div className={`p-2 rounded-lg ${info.color}`}> <div className="flex items-center gap-3">
<Icon className="h-6 w-6" /> <div className={`p-2 rounded-lg ${info.color}`}>
<Icon className="h-6 w-6" />
</div>
<div>
<CardTitle className="text-xl">{info.label}</CardTitle>
<CardDescription className="mt-1">{info.description}</CardDescription>
</div>
</div>
<Button
size="sm"
onClick={() => handleCreateSite(type)}
data-testid={`button-add-site-${type}`}
>
<Plus className="h-4 w-4 mr-1" />
Aggiungi Sito
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Siti Totali</p>
<p className="text-2xl font-semibold" data-testid={`text-total-${type}`}>
{stat.total}
</p>
</div> </div>
<div> <div>
<CardTitle className="text-xl">{info.label}</CardTitle> <p className="text-sm text-muted-foreground">Attivi</p>
<CardDescription className="mt-1">{info.description}</CardDescription> <p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
{stat.active}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Richiedono Armati</p>
<p className="text-lg font-semibold" data-testid={`text-armed-${type}`}>
{stat.requiresArmed}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Richiedono Patente</p>
<p className="text-lg font-semibold" data-testid={`text-driver-${type}`}>
{stat.requiresDriver}
</p>
</div> </div>
</div> </div>
</div>
</CardHeader> <div className="mt-4 pt-4 border-t">
<CardContent> <div className="flex flex-wrap gap-2">
<div className="grid grid-cols-2 gap-4"> <Badge variant="outline" className="font-normal">
<div> <MapPin className="h-3 w-3 mr-1" />
<p className="text-sm text-muted-foreground">Siti Totali</p> {sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte
<p className="text-2xl font-semibold" data-testid={`text-total-${type}`}> </Badge>
{stat.total} <Badge variant="outline" className="font-normal">
</p> <MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma
</Badge>
</div>
</div> </div>
<div>
<p className="text-sm text-muted-foreground">Attivi</p> {stat.total > 0 && (
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}> <Button
{stat.active} variant="ghost"
</p> size="sm"
className="w-full mt-4"
onClick={() => setSelectedServiceType(selectedServiceType === type ? null : type)}
data-testid={`button-view-sites-${type}`}
>
{selectedServiceType === type ? "Nascondi" : "Visualizza"} Siti ({stat.total})
</Button>
)}
</CardContent>
</Card>
);
})}
</div>
{selectedServiceType && filteredSites.length > 0 && (
<Card>
<CardHeader>
<CardTitle>
Siti {serviceTypeInfo[selectedServiceType as keyof typeof serviceTypeInfo].label}
</CardTitle>
<CardDescription>
Lista completa dei siti con questo tipo di servizio
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{filteredSites.map((site) => (
<div
key={site.id}
className="flex items-center justify-between p-4 rounded-lg border"
data-testid={`site-item-${site.id}`}
>
<div className="flex-1">
<div className="flex items-center gap-3">
<h4 className="font-semibold">{site.name}</h4>
<Badge variant={site.isActive ? "default" : "secondary"}>
{site.isActive ? "Attivo" : "Inattivo"}
</Badge>
</div>
<div className="flex gap-4 mt-2 text-sm text-muted-foreground">
<span>📍 {site.location}</span>
<span>👥 Min. {site.minGuards} guardie</span>
{site.requiresArmed && <span>🔫 Armato</span>}
{site.requiresDriverLicense && <span>🚗 Patente</span>}
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleEditSite(site)}
data-testid={`button-edit-site-${site.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
</div> </div>
<div> ))}
<p className="text-sm text-muted-foreground">Richiedono Armati</p> </div>
<p className="text-lg font-semibold" data-testid={`text-armed-${type}`}> </CardContent>
{stat.requiresArmed} </Card>
</p> )}
</div> </>
<div>
<p className="text-sm text-muted-foreground">Richiedono Patente</p>
<p className="text-lg font-semibold" data-testid={`text-driver-${type}`}>
{stat.requiresDriver}
</p>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano
</Badge>
<Badge variant="outline" className="font-normal">
<MapPin className="h-3 w-3 mr-1" />
{sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma
</Badge>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)} )}
<Card> {/* Create Site Dialog */}
<CardHeader> <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<CardTitle>Informazioni Tipologie Servizio</CardTitle> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<CardDescription>Caratteristiche e utilizzo delle diverse tipologie</CardDescription> <DialogHeader>
</CardHeader> <DialogTitle>Aggiungi Nuovo Sito</DialogTitle>
<CardContent className="space-y-4"> <DialogDescription>
<div className="grid gap-3"> Crea un nuovo sito di servizio
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> </DialogDescription>
<Building2 className="h-5 w-5 text-blue-500 mt-0.5" /> </DialogHeader>
<div> <Form {...createForm}>
<h4 className="font-semibold">Presidio Fisso</h4> <form onSubmit={createForm.handleSubmit((data) => createSiteMutation.mutate(data))} className="space-y-4">
<p className="text-sm text-muted-foreground mt-1"> <div className="grid grid-cols-2 gap-4">
Utilizzato per siti che richiedono sorveglianza continua con presenza fissa delle guardie. <FormField
Ideale per banche, musei, uffici pubblici. control={createForm.control}
</p> name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome Sito*</FormLabel>
<FormControl>
<Input {...field} data-testid="input-site-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede*</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="select-site-location">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</div>
<FormField
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> control={createForm.control}
<Eye className="h-5 w-5 text-green-500 mt-0.5" /> name="address"
<div> render={({ field }) => (
<h4 className="font-semibold">Pattugliamento</h4> <FormItem>
<p className="text-sm text-muted-foreground mt-1"> <FormLabel>Indirizzo</FormLabel>
Servizio di ronde mobili su area estesa. Le guardie effettuano controlli periodici <FormControl>
seguendo percorsi predefiniti. Richiede spesso patente di guida. <Input {...field} value={field.value || ""} data-testid="input-site-address" />
</p> </FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={createForm.control}
name="shiftType"
render={({ field }) => (
<FormItem>
<FormLabel>Tipo Servizio*</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="select-shift-type">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
<SelectItem value="patrol">Pattugliamento</SelectItem>
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="minGuards"
render={({ field }) => (
<FormItem>
<FormLabel>Guardie Minime*</FormLabel>
<FormControl>
<Input
type="number"
{...field}
value={field.value}
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
data-testid="input-min-guards"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> <FormField
<Shield className="h-5 w-5 text-purple-500 mt-0.5" /> control={createForm.control}
<div> name="requiresArmed"
<h4 className="font-semibold">Ispettorato Notturno</h4> render={({ field }) => (
<p className="text-sm text-muted-foreground mt-1"> <FormItem className="flex items-center justify-between p-3 border rounded-lg">
Controlli specifici durante le ore notturne. Prevede verifiche programmate <FormLabel>Richiede Armato</FormLabel>
di sicurezza e aperture/chiusure di strutture. <FormControl>
</p> <Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
data-testid="switch-requires-armed"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="requiresDriverLicense"
render={({ field }) => (
<FormItem className="flex items-center justify-between p-3 border rounded-lg">
<FormLabel>Richiede Patente</FormLabel>
<FormControl>
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
data-testid="switch-requires-driver"
/>
</FormControl>
</FormItem>
)}
/>
</div> </div>
</div>
<FormField
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> control={createForm.control}
<Zap className="h-5 w-5 text-orange-500 mt-0.5" /> name="isActive"
<div> render={({ field }) => (
<h4 className="font-semibold">Pronto Intervento</h4> <FormItem className="flex items-center justify-between p-3 border rounded-lg">
<p className="text-sm text-muted-foreground mt-1"> <FormLabel>Sito Attivo</FormLabel>
Servizio di intervento rapido su chiamata. Le guardie devono essere disponibili <FormControl>
per interventi urgenti, spesso armati e con veicolo dedicato. <Switch
</p> checked={field.value ?? true}
onCheckedChange={field.onChange}
data-testid="switch-is-active"
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setCreateDialogOpen(false)}
data-testid="button-cancel"
>
Annulla
</Button>
<Button
type="submit"
disabled={createSiteMutation.isPending}
data-testid="button-save-site"
>
{createSiteMutation.isPending ? "Salvataggio..." : "Salva Sito"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Edit Site Dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Modifica Sito</DialogTitle>
<DialogDescription>
Aggiorna le informazioni del sito
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form
onSubmit={editForm.handleSubmit((data) =>
selectedSite && updateSiteMutation.mutate({ id: selectedSite.id, data })
)}
className="space-y-4"
>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome Sito*</FormLabel>
<FormControl>
<Input {...field} data-testid="input-edit-site-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede*</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger data-testid="select-edit-site-location">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</div>
</div> <FormField
</CardContent> control={editForm.control}
</Card> name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Indirizzo</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} data-testid="input-edit-site-address" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="shiftType"
render={({ field }) => (
<FormItem>
<FormLabel>Tipo Servizio*</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger data-testid="select-edit-shift-type">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="fixed_post">Presidio Fisso</SelectItem>
<SelectItem value="patrol">Pattugliamento</SelectItem>
<SelectItem value="night_inspection">Ispettorato Notturno</SelectItem>
<SelectItem value="quick_response">Pronto Intervento</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="minGuards"
render={({ field }) => (
<FormItem>
<FormLabel>Guardie Minime*</FormLabel>
<FormControl>
<Input
type="number"
{...field}
value={field.value}
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
data-testid="input-edit-min-guards"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="requiresArmed"
render={({ field }) => (
<FormItem className="flex items-center justify-between p-3 border rounded-lg">
<FormLabel>Richiede Armato</FormLabel>
<FormControl>
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
data-testid="switch-edit-requires-armed"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="requiresDriverLicense"
render={({ field }) => (
<FormItem className="flex items-center justify-between p-3 border rounded-lg">
<FormLabel>Richiede Patente</FormLabel>
<FormControl>
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
data-testid="switch-edit-requires-driver"
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="isActive"
render={({ field }) => (
<FormItem className="flex items-center justify-between p-3 border rounded-lg">
<FormLabel>Sito Attivo</FormLabel>
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
data-testid="switch-edit-is-active"
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setEditDialogOpen(false);
setSelectedSite(null);
}}
data-testid="button-edit-cancel"
>
Annulla
</Button>
<Button
type="submit"
disabled={updateSiteMutation.isPending}
data-testid="button-update-site"
>
{updateSiteMutation.isPending ? "Salvataggio..." : "Aggiorna Sito"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@ -130,6 +130,7 @@ export default function Vehicles() {
brand: "", brand: "",
model: "", model: "",
vehicleType: "car", vehicleType: "car",
location: "roccapiemonte",
year: undefined, year: undefined,
assignedGuardId: null, assignedGuardId: null,
status: "available", status: "available",

View File

@ -1,7 +1,13 @@
{ {
"version": "1.0.7", "version": "1.0.8",
"lastUpdate": "2025-10-17T08:07:39.479Z", "lastUpdate": "2025-10-17T08:36:23.963Z",
"changelog": [ "changelog": [
{
"version": "1.0.8",
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.8"
},
{ {
"version": "1.0.7", "version": "1.0.7",
"date": "2025-10-17", "date": "2025-10-17",