VigilanzaTurni/client/src/pages/dashboard.tsx
marco370 34221555d8 Add location filtering and meal voucher settings
Implements multi-location filtering on Dashboard and Shifts pages, adds meal voucher configuration options in Parameters, and introduces multi-location seeding in server/seed.ts.

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:25:08 +00:00

235 lines
8.8 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";
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-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>
);
}