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
This commit is contained in:
parent
2b53650f1b
commit
5c15e6ad27
4
.replit
4
.replit
@ -27,6 +27,10 @@ externalPort = 3000
|
||||
localPort = 42175
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 42403
|
||||
externalPort = 3003
|
||||
|
||||
[env]
|
||||
PORT = "5000"
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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";
|
||||
@ -78,7 +79,7 @@ function TrainingTab() {
|
||||
const { toast } = useToast();
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
|
||||
const { data: courses = [], isLoading } = useQuery({
|
||||
const { data: courses = [], isLoading } = useQuery<TrainingCourse[]>({
|
||||
queryKey: ["/api/training-courses"],
|
||||
});
|
||||
|
||||
@ -211,7 +212,7 @@ function AbsencesTab() {
|
||||
const { toast } = useToast();
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
|
||||
const { data: absences = [], isLoading } = useQuery({
|
||||
const { data: absences = [], isLoading } = useQuery<Absence[]>({
|
||||
queryKey: ["/api/absences"],
|
||||
});
|
||||
|
||||
|
||||
@ -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 { Badge } from "@/components/ui/badge";
|
||||
import { Site } from "@shared/schema";
|
||||
import { Building2, Shield, Eye, MapPin, Zap } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 = {
|
||||
fixed_post: {
|
||||
@ -31,11 +64,124 @@ const serviceTypeInfo = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
type SiteForm = z.infer<typeof insertSiteSchema>;
|
||||
|
||||
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[]>({
|
||||
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
|
||||
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
|
||||
const sitesForType = sites.filter(s => s.shiftType === type);
|
||||
@ -48,142 +194,535 @@ export default function Services() {
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
const filteredSites = selectedServiceType
|
||||
? sites.filter(s => s.shiftType === selectedServiceType)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Panoramica tipologie di servizio e relative configurazioni
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Panoramica tipologie di servizio e relative configurazioni
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<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]) => {
|
||||
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}`}>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${info.color}`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
<>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{Object.entries(serviceTypeInfo).map(([type, info]) => {
|
||||
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}`}>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<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>
|
||||
<CardTitle className="text-xl">{info.label}</CardTitle>
|
||||
<CardDescription className="mt-1">{info.description}</CardDescription>
|
||||
<p className="text-sm text-muted-foreground">Attivi</p>
|
||||
<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>
|
||||
</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 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>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Attivi</p>
|
||||
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
|
||||
{stat.active}
|
||||
</p>
|
||||
|
||||
{stat.total > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
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>
|
||||
<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 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>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informazioni Tipologie Servizio</CardTitle>
|
||||
<CardDescription>Caratteristiche e utilizzo delle diverse tipologie</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<Building2 className="h-5 w-5 text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Presidio Fisso</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Utilizzato per siti che richiedono sorveglianza continua con presenza fissa delle guardie.
|
||||
Ideale per banche, musei, uffici pubblici.
|
||||
</p>
|
||||
{/* Create Site Dialog */}
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Aggiungi Nuovo Sito</DialogTitle>
|
||||
<DialogDescription>
|
||||
Crea un nuovo sito di servizio
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...createForm}>
|
||||
<form onSubmit={createForm.handleSubmit((data) => createSiteMutation.mutate(data))} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
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 className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<Eye className="h-5 w-5 text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Pattugliamento</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Servizio di ronde mobili su area estesa. Le guardie effettuano controlli periodici
|
||||
seguendo percorsi predefiniti. Richiede spesso patente di guida.
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Indirizzo</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ""} data-testid="input-site-address" />
|
||||
</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 className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<Shield className="h-5 w-5 text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Ispettorato Notturno</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Controlli specifici durante le ore notturne. Prevede verifiche programmate
|
||||
di sicurezza e aperture/chiusure di strutture.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={createForm.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-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 className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<Zap className="h-5 w-5 text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Pronto Intervento</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Servizio di intervento rapido su chiamata. Le guardie devono essere disponibili
|
||||
per interventi urgenti, spesso armati e con veicolo dedicato.
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
control={createForm.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-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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<FormField
|
||||
control={editForm.control}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user