Compare commits
No commits in common. "4693b782cb64b9e5963992771f283cb2e8f5d320" and "dd468716d9b0734b2cebde74995b13f75b2b6d65" have entirely different histories.
4693b782cb
...
dd468716d9
4
.replit
4
.replit
@ -27,10 +27,6 @@ externalPort = 3000
|
|||||||
localPort = 42175
|
localPort = 42175
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 42403
|
|
||||||
externalPort = 3003
|
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
PORT = "5000"
|
PORT = "5000"
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const menuItems = [
|
|||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Gestione Pianificazioni",
|
title: "Pianificazione Avanzata",
|
||||||
url: "/advanced-planning",
|
url: "/advanced-planning",
|
||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
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";
|
||||||
@ -36,7 +35,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">Gestione Pianificazioni</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Pianificazione Avanzata</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>
|
||||||
@ -79,7 +78,7 @@ function TrainingTab() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||||
|
|
||||||
const { data: courses = [], isLoading } = useQuery<TrainingCourse[]>({
|
const { data: courses = [], isLoading } = useQuery({
|
||||||
queryKey: ["/api/training-courses"],
|
queryKey: ["/api/training-courses"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -212,7 +211,7 @@ function AbsencesTab() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||||
|
|
||||||
const { data: absences = [], isLoading } = useQuery<Absence[]>({
|
const { data: absences = [], isLoading } = useQuery({
|
||||||
queryKey: ["/api/absences"],
|
queryKey: ["/api/absences"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,41 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
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 { Button } from "@/components/ui/button";
|
import { Site } from "@shared/schema";
|
||||||
import {
|
import { Building2, Shield, Eye, MapPin, Zap } from "lucide-react";
|
||||||
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: {
|
||||||
@ -64,124 +31,11 @@ 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);
|
||||||
@ -194,535 +48,142 @@ 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 className="flex items-center justify-between">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1>
|
||||||
<h1 className="text-3xl font-semibold mb-2">Gestione Servizi</h1>
|
<p className="text-muted-foreground">
|
||||||
<p className="text-muted-foreground">
|
Panoramica tipologie di servizio e relative configurazioni
|
||||||
Panoramica tipologie di servizio e relative configurazioni
|
</p>
|
||||||
</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">
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
{Object.entries(serviceTypeInfo).map(([type, info]) => {
|
||||||
{Object.entries(serviceTypeInfo).map(([type, info]) => {
|
const Icon = info.icon;
|
||||||
const Icon = info.icon;
|
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
|
||||||
const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 };
|
|
||||||
|
return (
|
||||||
return (
|
<Card key={type} data-testid={`card-service-${type}`}>
|
||||||
<Card key={type} data-testid={`card-service-${type}`}>
|
<CardHeader>
|
||||||
<CardHeader>
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className={`p-2 rounded-lg ${info.color}`}>
|
||||||
<div className={`p-2 rounded-lg ${info.color}`}>
|
<Icon className="h-6 w-6" />
|
||||||
<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>
|
||||||
<p className="text-sm text-muted-foreground">Attivi</p>
|
<CardTitle className="text-xl">{info.label}</CardTitle>
|
||||||
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
|
<CardDescription className="mt-1">{info.description}</CardDescription>
|
||||||
{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>
|
||||||
<div className="mt-4 pt-4 border-t">
|
</CardHeader>
|
||||||
<div className="flex flex-wrap gap-2">
|
<CardContent>
|
||||||
<Badge variant="outline" className="font-normal">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<MapPin className="h-3 w-3 mr-1" />
|
<div>
|
||||||
{sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte
|
<p className="text-sm text-muted-foreground">Siti Totali</p>
|
||||||
</Badge>
|
<p className="text-2xl font-semibold" data-testid={`text-total-${type}`}>
|
||||||
<Badge variant="outline" className="font-normal">
|
{stat.total}
|
||||||
<MapPin className="h-3 w-3 mr-1" />
|
</p>
|
||||||
{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>
|
||||||
{stat.total > 0 && (
|
<p className="text-sm text-muted-foreground">Attivi</p>
|
||||||
<Button
|
<p className="text-2xl font-semibold text-green-500" data-testid={`text-active-${type}`}>
|
||||||
variant="ghost"
|
{stat.active}
|
||||||
size="sm"
|
</p>
|
||||||
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>
|
||||||
</div>
|
<p className="text-sm text-muted-foreground">Richiedono Armati</p>
|
||||||
</CardContent>
|
<p className="text-lg font-semibold" data-testid={`text-armed-${type}`}>
|
||||||
</Card>
|
{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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create Site Dialog */}
|
<Card>
|
||||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
<CardHeader>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<CardTitle>Informazioni Tipologie Servizio</CardTitle>
|
||||||
<DialogHeader>
|
<CardDescription>Caratteristiche e utilizzo delle diverse tipologie</CardDescription>
|
||||||
<DialogTitle>Aggiungi Nuovo Sito</DialogTitle>
|
</CardHeader>
|
||||||
<DialogDescription>
|
<CardContent className="space-y-4">
|
||||||
Crea un nuovo sito di servizio
|
<div className="grid gap-3">
|
||||||
</DialogDescription>
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||||
</DialogHeader>
|
<Building2 className="h-5 w-5 text-blue-500 mt-0.5" />
|
||||||
<Form {...createForm}>
|
<div>
|
||||||
<form onSubmit={createForm.handleSubmit((data) => createSiteMutation.mutate(data))} className="space-y-4">
|
<h4 className="font-semibold">Presidio Fisso</h4>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
<FormField
|
Utilizzato per siti che richiedono sorveglianza continua con presenza fissa delle guardie.
|
||||||
control={createForm.control}
|
Ideale per banche, musei, uffici pubblici.
|
||||||
name="name"
|
</p>
|
||||||
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
|
|
||||||
control={createForm.control}
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||||
name="address"
|
<Eye className="h-5 w-5 text-green-500 mt-0.5" />
|
||||||
render={({ field }) => (
|
<div>
|
||||||
<FormItem>
|
<h4 className="font-semibold">Pattugliamento</h4>
|
||||||
<FormLabel>Indirizzo</FormLabel>
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
<FormControl>
|
Servizio di ronde mobili su area estesa. Le guardie effettuano controlli periodici
|
||||||
<Input {...field} value={field.value || ""} data-testid="input-site-address" />
|
seguendo percorsi predefiniti. Richiede spesso patente di guida.
|
||||||
</FormControl>
|
</p>
|
||||||
<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">
|
|
||||||
<FormField
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||||
control={createForm.control}
|
<Shield className="h-5 w-5 text-purple-500 mt-0.5" />
|
||||||
name="requiresArmed"
|
<div>
|
||||||
render={({ field }) => (
|
<h4 className="font-semibold">Ispettorato Notturno</h4>
|
||||||
<FormItem className="flex items-center justify-between p-3 border rounded-lg">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
<FormLabel>Richiede Armato</FormLabel>
|
Controlli specifici durante le ore notturne. Prevede verifiche programmate
|
||||||
<FormControl>
|
di sicurezza e aperture/chiusure di strutture.
|
||||||
<Switch
|
</p>
|
||||||
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
|
|
||||||
control={createForm.control}
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||||
name="isActive"
|
<Zap className="h-5 w-5 text-orange-500 mt-0.5" />
|
||||||
render={({ field }) => (
|
<div>
|
||||||
<FormItem className="flex items-center justify-between p-3 border rounded-lg">
|
<h4 className="font-semibold">Pronto Intervento</h4>
|
||||||
<FormLabel>Sito Attivo</FormLabel>
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
<FormControl>
|
Servizio di intervento rapido su chiamata. Le guardie devono essere disponibili
|
||||||
<Switch
|
per interventi urgenti, spesso armati e con veicolo dedicato.
|
||||||
checked={field.value ?? true}
|
</p>
|
||||||
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>
|
||||||
<FormField
|
</div>
|
||||||
control={editForm.control}
|
</CardContent>
|
||||||
name="address"
|
</Card>
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,7 +130,6 @@ 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",
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.8",
|
"version": "1.0.7",
|
||||||
"lastUpdate": "2025-10-17T08:36:23.963Z",
|
"lastUpdate": "2025-10-17T08:07:39.479Z",
|
||||||
"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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user