diff --git a/.replit b/.replit index 048618b..69df309 100644 --- a/.replit +++ b/.replit @@ -27,6 +27,10 @@ externalPort = 3000 localPort = 42175 externalPort = 3002 +[[ports]] +localPort = 42403 +externalPort = 3003 + [env] PORT = "5000" diff --git a/client/src/pages/advanced-planning.tsx b/client/src/pages/advanced-planning.tsx index 2867cf3..9b626fc 100644 --- a/client/src/pages/advanced-planning.tsx +++ b/client/src/pages/advanced-planning.tsx @@ -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({ 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({ queryKey: ["/api/absences"], }); diff --git a/client/src/pages/services.tsx b/client/src/pages/services.tsx index 687da72..5fd4856 100644 --- a/client/src/pages/services.tsx +++ b/client/src/pages/services.tsx @@ -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; + export default function Services() { + const { toast } = useToast(); + const [selectedServiceType, setSelectedServiceType] = useState(null); + const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [selectedSite, setSelectedSite] = useState(null); + const { data: sites = [], isLoading } = useQuery({ queryKey: ["/api/sites"], }); + const createForm = useForm({ + resolver: zodResolver(insertSiteSchema), + defaultValues: { + name: "", + location: "roccapiemonte", + address: "", + clientId: null, + shiftType: "fixed_post", + minGuards: 1, + requiresArmed: false, + requiresDriverLicense: false, + isActive: true, + }, + }); + + const editForm = useForm({ + 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); + const filteredSites = selectedServiceType + ? sites.filter(s => s.shiftType === selectedServiceType) + : []; + return (
-
-

Gestione Servizi

-

- Panoramica tipologie di servizio e relative configurazioni -

+
+
+

Gestione Servizi

+

+ Panoramica tipologie di servizio e relative configurazioni +

+
{isLoading ? (
Caricamento servizi...
) : ( -
- {Object.entries(serviceTypeInfo).map(([type, info]) => { - const Icon = info.icon; - const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 }; - - return ( - - -
-
-
- + <> +
+ {Object.entries(serviceTypeInfo).map(([type, info]) => { + const Icon = info.icon; + const stat = stats[type] || { total: 0, active: 0, requiresArmed: 0, requiresDriver: 0 }; + + return ( + + +
+
+
+ +
+
+ {info.label} + {info.description} +
+
+ +
+
+ +
+
+

Siti Totali

+

+ {stat.total} +

- {info.label} - {info.description} +

Attivi

+

+ {stat.active} +

+
+
+

Richiedono Armati

+

+ {stat.requiresArmed} +

+
+
+

Richiedono Patente

+

+ {stat.requiresDriver} +

-
- - -
-
-

Siti Totali

-

- {stat.total} -

+ +
+
+ + + {sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte + + + + {sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano + + + + {sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma + +
-
-

Attivi

-

- {stat.active} -

+ + {stat.total > 0 && ( + + )} + + + ); + })} +
+ + {selectedServiceType && filteredSites.length > 0 && ( + + + + Siti {serviceTypeInfo[selectedServiceType as keyof typeof serviceTypeInfo].label} + + + Lista completa dei siti con questo tipo di servizio + + + +
+ {filteredSites.map((site) => ( +
+
+
+

{site.name}

+ + {site.isActive ? "Attivo" : "Inattivo"} + +
+
+ ๐Ÿ“ {site.location} + ๐Ÿ‘ฅ Min. {site.minGuards} guardie + {site.requiresArmed && ๐Ÿ”ซ Armato} + {site.requiresDriverLicense && ๐Ÿš— Patente} +
+
+
-
-

Richiedono Armati

-

- {stat.requiresArmed} -

-
-
-

Richiedono Patente

-

- {stat.requiresDriver} -

-
-
- -
-
- - - {sites.filter(s => s.shiftType === type && s.location === 'roccapiemonte').length} Roccapiemonte - - - - {sites.filter(s => s.shiftType === type && s.location === 'milano').length} Milano - - - - {sites.filter(s => s.shiftType === type && s.location === 'roma').length} Roma - -
-
-
-
- ); - })} -
+ ))} +
+
+ + )} + )} - - - Informazioni Tipologie Servizio - Caratteristiche e utilizzo delle diverse tipologie - - -
-
- -
-

Presidio Fisso

-

- Utilizzato per siti che richiedono sorveglianza continua con presenza fissa delle guardie. - Ideale per banche, musei, uffici pubblici. -

+ {/* Create Site Dialog */} + + + + Aggiungi Nuovo Sito + + Crea un nuovo sito di servizio + + +
+ createSiteMutation.mutate(data))} className="space-y-4"> +
+ ( + + Nome Sito* + + + + + + )} + /> + ( + + Sede* + + + + )} + />
-
- -
- -
-

Pattugliamento

-

- Servizio di ronde mobili su area estesa. Le guardie effettuano controlli periodici - seguendo percorsi predefiniti. Richiede spesso patente di guida. -

+ + ( + + Indirizzo + + + + + + )} + /> + +
+ ( + + Tipo Servizio* + + + + )} + /> + ( + + Guardie Minime* + + field.onChange(parseInt(e.target.value) || 1)} + data-testid="input-min-guards" + /> + + + + )} + />
-
- -
- -
-

Ispettorato Notturno

-

- Controlli specifici durante le ore notturne. Prevede verifiche programmate - di sicurezza e aperture/chiusure di strutture. -

+ +
+ ( + + Richiede Armato + + + + + )} + /> + ( + + Richiede Patente + + + + + )} + />
-
- -
- -
-

Pronto Intervento

-

- Servizio di intervento rapido su chiamata. Le guardie devono essere disponibili - per interventi urgenti, spesso armati e con veicolo dedicato. -

+ + ( + + Sito Attivo + + + + + )} + /> + + + + + + + + + + + {/* Edit Site Dialog */} + + + + Modifica Sito + + Aggiorna le informazioni del sito + + +
+ + selectedSite && updateSiteMutation.mutate({ id: selectedSite.id, data }) + )} + className="space-y-4" + > +
+ ( + + Nome Sito* + + + + + + )} + /> + ( + + Sede* + + + + )} + />
-
-
- - + + ( + + Indirizzo + + + + + + )} + /> + +
+ ( + + Tipo Servizio* + + + + )} + /> + ( + + Guardie Minime* + + field.onChange(parseInt(e.target.value) || 1)} + data-testid="input-edit-min-guards" + /> + + + + )} + /> +
+ +
+ ( + + Richiede Armato + + + + + )} + /> + ( + + Richiede Patente + + + + + )} + /> +
+ + ( + + Sito Attivo + + + + + )} + /> + + + + + + + + +
); }