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:
parent
8237234fad
commit
500da807cf
4
.replit
4
.replit
@ -19,6 +19,10 @@ externalPort = 80
|
||||
localPort = 33035
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 36589
|
||||
externalPort = 3003
|
||||
|
||||
[[ports]]
|
||||
localPort = 41343
|
||||
externalPort = 3000
|
||||
|
||||
@ -20,6 +20,7 @@ import Users from "@/pages/users";
|
||||
import Planning from "@/pages/planning";
|
||||
import Vehicles from "@/pages/vehicles";
|
||||
import Parameters from "@/pages/parameters";
|
||||
import Services from "@/pages/services";
|
||||
|
||||
function Router() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@ -34,6 +35,7 @@ function Router() {
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route path="/guards" component={Guards} />
|
||||
<Route path="/sites" component={Sites} />
|
||||
<Route path="/services" component={Services} />
|
||||
<Route path="/vehicles" component={Vehicles} />
|
||||
<Route path="/shifts" component={Shifts} />
|
||||
<Route path="/planning" component={Planning} />
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
UserCog,
|
||||
ClipboardList,
|
||||
Car,
|
||||
Briefcase,
|
||||
} from "lucide-react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import {
|
||||
@ -60,6 +61,12 @@ const menuItems = [
|
||||
icon: MapPin,
|
||||
roles: ["admin", "coordinator", "client"],
|
||||
},
|
||||
{
|
||||
title: "Servizi",
|
||||
url: "/services",
|
||||
icon: Briefcase,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
{
|
||||
title: "Parco Automezzi",
|
||||
url: "/vehicles",
|
||||
|
||||
189
client/src/pages/services.tsx
Normal file
189
client/src/pages/services.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user