Add a new page for managing different types of security services

Implement the Services page with routing, navigation, and data fetching for service statistics.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/kxc8yZp
This commit is contained in:
marco370 2025-10-17 07:55:17 +00:00
parent 8237234fad
commit 500da807cf
4 changed files with 202 additions and 0 deletions

View File

@ -19,6 +19,10 @@ externalPort = 80
localPort = 33035 localPort = 33035
externalPort = 3001 externalPort = 3001
[[ports]]
localPort = 36589
externalPort = 3003
[[ports]] [[ports]]
localPort = 41343 localPort = 41343
externalPort = 3000 externalPort = 3000

View File

@ -20,6 +20,7 @@ import Users from "@/pages/users";
import Planning from "@/pages/planning"; import Planning from "@/pages/planning";
import Vehicles from "@/pages/vehicles"; import Vehicles from "@/pages/vehicles";
import Parameters from "@/pages/parameters"; import Parameters from "@/pages/parameters";
import Services from "@/pages/services";
function Router() { function Router() {
const { isAuthenticated, isLoading } = useAuth(); const { isAuthenticated, isLoading } = useAuth();
@ -34,6 +35,7 @@ function Router() {
<Route path="/" component={Dashboard} /> <Route path="/" component={Dashboard} />
<Route path="/guards" component={Guards} /> <Route path="/guards" component={Guards} />
<Route path="/sites" component={Sites} /> <Route path="/sites" component={Sites} />
<Route path="/services" component={Services} />
<Route path="/vehicles" component={Vehicles} /> <Route path="/vehicles" component={Vehicles} />
<Route path="/shifts" component={Shifts} /> <Route path="/shifts" component={Shifts} />
<Route path="/planning" component={Planning} /> <Route path="/planning" component={Planning} />

View File

@ -10,6 +10,7 @@ import {
UserCog, UserCog,
ClipboardList, ClipboardList,
Car, Car,
Briefcase,
} from "lucide-react"; } from "lucide-react";
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
import { import {
@ -60,6 +61,12 @@ const menuItems = [
icon: MapPin, icon: MapPin,
roles: ["admin", "coordinator", "client"], roles: ["admin", "coordinator", "client"],
}, },
{
title: "Servizi",
url: "/services",
icon: Briefcase,
roles: ["admin", "coordinator"],
},
{ {
title: "Parco Automezzi", title: "Parco Automezzi",
url: "/vehicles", url: "/vehicles",

View File

@ -0,0 +1,189 @@
import { useQuery } from "@tanstack/react-query";
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";
const serviceTypeInfo = {
fixed_post: {
label: "Presidio Fisso",
description: "Guardia fissa presso una struttura",
icon: Building2,
color: "bg-blue-500/10 text-blue-500 border-blue-500/20"
},
patrol: {
label: "Pattugliamento",
description: "Ronde e controlli su area",
icon: Eye,
color: "bg-green-500/10 text-green-500 border-green-500/20"
},
night_inspection: {
label: "Ispettorato Notturno",
description: "Controlli notturni programmati",
icon: Shield,
color: "bg-purple-500/10 text-purple-500 border-purple-500/20"
},
quick_response: {
label: "Pronto Intervento",
description: "Intervento rapido su chiamata",
icon: Zap,
color: "bg-orange-500/10 text-orange-500 border-orange-500/20"
}
} as const;
export default function Services() {
const { data: sites = [], isLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"],
});
// Calculate statistics per service type
const stats = Object.keys(serviceTypeInfo).reduce((acc, type) => {
const sitesForType = sites.filter(s => s.shiftType === type);
acc[type] = {
total: sitesForType.length,
active: sitesForType.filter(s => s.isActive).length,
requiresArmed: sitesForType.filter(s => s.requiresArmed).length,
requiresDriver: sitesForType.filter(s => s.requiresDriverLicense).length,
};
return acc;
}, {} as Record<string, any>);
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>
{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>
<div>
<CardTitle className="text-xl">{info.label}</CardTitle>
<CardDescription className="mt-1">{info.description}</CardDescription>
</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>
<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>
</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>
)}
<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>
</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>
</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>
</div>
);
}