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
|
localPort = 33035
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 36589
|
||||||
|
externalPort = 3003
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41343
|
localPort = 41343
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
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