Compare commits

..

No commits in common. "4693b782cb64b9e5963992771f283cb2e8f5d320" and "dd468716d9b0734b2cebde74995b13f75b2b6d65" have entirely different histories.

6 changed files with 126 additions and 677 deletions

View File

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

View File

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

View File

@ -1,7 +1,6 @@
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";
@ -36,7 +35,7 @@ export default function PlanningPage() {
<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>
<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>
@ -79,7 +78,7 @@ function TrainingTab() {
const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: courses = [], isLoading } = useQuery<TrainingCourse[]>({
const { data: courses = [], isLoading } = useQuery({
queryKey: ["/api/training-courses"],
});
@ -212,7 +211,7 @@ function AbsencesTab() {
const { toast } = useToast();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const { data: absences = [], isLoading } = useQuery<Absence[]>({
const { data: absences = [], isLoading } = useQuery({
queryKey: ["/api/absences"],
});

View File

@ -1,41 +1,8 @@
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 { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
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";
import { Site } from "@shared/schema";
import { Building2, Shield, Eye, MapPin, Zap } from "lucide-react";
const serviceTypeInfo = {
fixed_post: {
@ -64,124 +31,11 @@ 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);
@ -194,25 +48,18 @@ 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 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;
@ -231,14 +78,6 @@ export default function Services() {
<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>
@ -285,444 +124,66 @@ export default function Services() {
</Badge>
</div>
</div>
{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>
<CardTitle>Informazioni Tipologie Servizio</CardTitle>
<CardDescription>Caratteristiche e utilizzo delle diverse tipologie</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>}
<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>
</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 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>
</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>
</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>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</>
)}
{/* 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>
<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 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>
<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>
<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>
);
}

View File

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

View File

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