VigilanzaTurni/client/src/pages/dashboard.tsx
marco370 8237234fad Add current version number to the dashboard and specify testing environment
Add current version number to the dashboard UI and document that all tests are performed on the external server with local authentication.

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/IdDfihe
2025-10-17 07:53:11 +00:00

239 lines
9.0 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/useAuth";
import { KPICard } from "@/components/kpi-card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { StatusBadge } from "@/components/status-badge";
import { Users, Calendar, MapPin, AlertTriangle, Clock, CheckCircle, Building2 } from "lucide-react";
import { ShiftWithDetails, GuardWithCertifications, Site } from "@shared/schema";
import { formatDistanceToNow, format } from "date-fns";
import { it } from "date-fns/locale";
import { Skeleton } from "@/components/ui/skeleton";
import versionInfo from "../../../version.json";
export default function Dashboard() {
const { user } = useAuth();
const [selectedLocation, setSelectedLocation] = useState<string>("all");
const { data: shifts, isLoading: shiftsLoading } = useQuery<ShiftWithDetails[]>({
queryKey: ["/api/shifts/active"],
});
const { data: guards, isLoading: guardsLoading } = useQuery<GuardWithCertifications[]>({
queryKey: ["/api/guards"],
});
const { data: sites, isLoading: sitesLoading } = useQuery<Site[]>({
queryKey: ["/api/sites"],
});
// Filter data by location
const filteredGuards = selectedLocation === "all"
? guards
: guards?.filter(g => g.location === selectedLocation);
const filteredSites = selectedLocation === "all"
? sites
: sites?.filter(s => s.location === selectedLocation);
const filteredShifts = selectedLocation === "all"
? shifts
: shifts?.filter(s => {
const site = sites?.find(site => site.id === s.siteId);
return site?.location === selectedLocation;
});
// Calculate KPIs
const activeShifts = filteredShifts?.filter(s => s.status === "active").length || 0;
const totalGuards = filteredGuards?.length || 0;
const activeSites = filteredSites?.filter(s => s.isActive).length || 0;
// Expiring certifications (next 30 days)
const expiringCerts = filteredGuards?.flatMap(g =>
g.certifications.filter(c => c.status === "expiring_soon")
).length || 0;
const isLoading = shiftsLoading || guardsLoading || sitesLoading;
const locationLabels: Record<string, string> = {
all: "Tutte le Sedi",
roccapiemonte: "Roccapiemonte",
milano: "Milano",
roma: "Roma"
};
return (
<div className="space-y-6">
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-semibold mb-2">Dashboard Operativa</h1>
<p className="text-sm text-muted-foreground/70 mb-2" data-testid="text-version">
v{versionInfo.version}
</p>
<p className="text-muted-foreground">
Benvenuto, {user?.firstName} {user?.lastName}
</p>
</div>
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-muted-foreground" />
<Select value={selectedLocation} onValueChange={setSelectedLocation}>
<SelectTrigger className="w-[200px]" data-testid="select-location">
<SelectValue placeholder="Seleziona sede" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tutte le Sedi</SelectItem>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* KPI Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{isLoading ? (
<>
<Skeleton className="h-32" />
<Skeleton className="h-32" />
<Skeleton className="h-32" />
<Skeleton className="h-32" />
</>
) : (
<>
<KPICard
title="Turni Attivi"
value={activeShifts}
icon={Calendar}
data-testid="kpi-active-shifts"
/>
<KPICard
title="Guardie Totali"
value={totalGuards}
icon={Users}
data-testid="kpi-total-guards"
/>
<KPICard
title="Siti Attivi"
value={activeSites}
icon={MapPin}
data-testid="kpi-active-sites"
/>
<KPICard
title="Certificazioni in Scadenza"
value={expiringCerts}
icon={AlertTriangle}
className={expiringCerts > 0 ? "border-[hsl(25,90%,55%)]" : ""}
data-testid="kpi-expiring-certs"
/>
</>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Active Shifts */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Turni in Corso
</CardTitle>
<CardDescription>
Servizi attualmente attivi
</CardDescription>
</CardHeader>
<CardContent>
{shiftsLoading ? (
<div className="space-y-3">
<Skeleton className="h-16" />
<Skeleton className="h-16" />
</div>
) : filteredShifts && filteredShifts.length > 0 ? (
<div className="space-y-3">
{filteredShifts.slice(0, 5).map((shift) => (
<div
key={shift.id}
className="flex items-center justify-between p-3 rounded-md border hover-elevate"
data-testid={`shift-${shift.id}`}
>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{shift.site.name}</p>
<p className="text-sm text-muted-foreground">
{shift.assignments.length} guardie assegnate
</p>
</div>
<StatusBadge status={shift.status === "active" ? "active" : "pending"}>
{shift.status === "active" ? "Attivo" : "Pianificato"}
</StatusBadge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-8">
Nessun turno attivo al momento
</p>
)}
</CardContent>
</Card>
{/* Alerts & Notifications */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Alert e Scadenze
</CardTitle>
<CardDescription>
Certificazioni in scadenza prossimi 30 giorni
</CardDescription>
</CardHeader>
<CardContent>
{guardsLoading ? (
<div className="space-y-3">
<Skeleton className="h-16" />
<Skeleton className="h-16" />
</div>
) : filteredGuards ? (
<div className="space-y-3">
{filteredGuards
.flatMap(guard =>
guard.certifications
.filter(c => c.status === "expiring_soon" || c.status === "expired")
.map(cert => ({ guard, cert }))
)
.slice(0, 5)
.map(({ guard, cert }) => (
<div
key={cert.id}
className="flex items-center justify-between p-3 rounded-md border border-[hsl(25,90%,55%)]/30 bg-[hsl(25,90%,55%)]/5"
data-testid={`alert-${cert.id}`}
>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">
{guard.user?.firstName} {guard.user?.lastName}
</p>
<p className="text-sm text-muted-foreground">
{cert.name} - Scade {format(new Date(cert.expiryDate), "dd/MM/yyyy")}
</p>
</div>
<StatusBadge status={cert.status === "expired" ? "emergency" : "late"}>
{cert.status === "expired" ? "Scaduto" : "In scadenza"}
</StatusBadge>
</div>
))}
{filteredGuards.flatMap(g => g.certifications.filter(c => c.status !== "valid")).length === 0 && (
<div className="flex items-center gap-2 text-sm text-muted-foreground justify-center py-8">
<CheckCircle className="h-4 w-4 text-[hsl(140,60%,45%)]" />
Tutte le certificazioni sono valide
</div>
)}
</div>
) : null}
</CardContent>
</Card>
</div>
</div>
);
}