VigilanzaTurni/client/src/pages/reports.tsx
marco370 abe4041cd1 Add basic UI components and structure for the application
Initial commit adds core UI components, including layout elements, form controls, and navigation elements, along with the main application structure and routing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/nGJAldO
2025-10-11 09:36:55 +00:00

228 lines
8.5 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { ShiftWithDetails, Guard } from "@shared/schema";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { BarChart3, Users, Clock, Calendar, TrendingUp } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { differenceInHours, format, startOfMonth, endOfMonth } from "date-fns";
import { it } from "date-fns/locale";
export default function Reports() {
const { data: shifts, isLoading: shiftsLoading } = useQuery<ShiftWithDetails[]>({
queryKey: ["/api/shifts"],
});
const { data: guards, isLoading: guardsLoading } = useQuery<Guard[]>({
queryKey: ["/api/guards"],
});
const isLoading = shiftsLoading || guardsLoading;
// Calculate statistics
const completedShifts = shifts?.filter(s => s.status === "completed") || [];
const totalHours = completedShifts.reduce((acc, shift) => {
return acc + differenceInHours(new Date(shift.endTime), new Date(shift.startTime));
}, 0);
// Hours per guard
const hoursPerGuard: Record<string, { name: string; hours: number }> = {};
completedShifts.forEach(shift => {
shift.assignments.forEach(assignment => {
const guardId = assignment.guardId;
const guardName = `${assignment.guard.user?.firstName || ""} ${assignment.guard.user?.lastName || ""}`.trim();
const hours = differenceInHours(new Date(shift.endTime), new Date(shift.startTime));
if (!hoursPerGuard[guardId]) {
hoursPerGuard[guardId] = { name: guardName, hours: 0 };
}
hoursPerGuard[guardId].hours += hours;
});
});
const guardStats = Object.values(hoursPerGuard).sort((a, b) => b.hours - a.hours);
// Monthly statistics
const currentMonth = new Date();
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const monthlyShifts = completedShifts.filter(s => {
const shiftDate = new Date(s.startTime);
return shiftDate >= monthStart && shiftDate <= monthEnd;
});
const monthlyHours = monthlyShifts.reduce((acc, shift) => {
return acc + differenceInHours(new Date(shift.endTime), new Date(shift.startTime));
}, 0);
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-semibold mb-2">Report e Statistiche</h1>
<p className="text-muted-foreground">
Ore lavorate e copertura servizi
</p>
</div>
{isLoading ? (
<div className="grid gap-4 md:grid-cols-3">
<Skeleton className="h-32" />
<Skeleton className="h-32" />
<Skeleton className="h-32" />
</div>
) : (
<>
{/* Summary Cards */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="pb-3">
<CardDescription className="flex items-center gap-2">
<Clock className="h-4 w-4" />
Ore Totali Lavorate
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-semibold" data-testid="text-total-hours">
{totalHours}h
</p>
<p className="text-xs text-muted-foreground mt-1">
{completedShifts.length} turni completati
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Ore Mese Corrente
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-semibold" data-testid="text-monthly-hours">
{monthlyHours}h
</p>
<p className="text-xs text-muted-foreground mt-1">
{format(currentMonth, "MMMM yyyy", { locale: it })}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription className="flex items-center gap-2">
<Users className="h-4 w-4" />
Guardie Attive
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-semibold" data-testid="text-active-guards">
{guardStats.length}
</p>
<p className="text-xs text-muted-foreground mt-1">
Con turni completati
</p>
</CardContent>
</Card>
</div>
{/* Hours per Guard */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Ore per Guardia
</CardTitle>
<CardDescription>
Ore totali lavorate per ogni guardia
</CardDescription>
</CardHeader>
<CardContent>
{guardStats.length > 0 ? (
<div className="space-y-3">
{guardStats.map((stat, index) => (
<div
key={index}
className="flex items-center gap-4"
data-testid={`guard-stat-${index}`}
>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{stat.name}</p>
<div className="mt-1 h-2 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-primary"
style={{
width: `${(stat.hours / (guardStats[0]?.hours || 1)) * 100}%`,
}}
/>
</div>
</div>
<div className="text-right">
<p className="text-lg font-semibold font-mono">{stat.hours}h</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<BarChart3 className="h-12 w-12 text-muted-foreground mx-auto mb-3" />
<p className="text-sm text-muted-foreground">
Nessun dato disponibile
</p>
</div>
)}
</CardContent>
</Card>
{/* Recent Shifts Summary */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Turni Recenti
</CardTitle>
<CardDescription>
Ultimi turni completati
</CardDescription>
</CardHeader>
<CardContent>
{completedShifts.length > 0 ? (
<div className="space-y-3">
{completedShifts.slice(0, 5).map((shift) => (
<div
key={shift.id}
className="flex items-center justify-between p-3 rounded-md border"
data-testid={`recent-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">
{format(new Date(shift.startTime), "dd MMM yyyy", { locale: it })}
</p>
</div>
<div className="text-right">
<p className="font-mono text-sm">
{differenceInHours(new Date(shift.endTime), new Date(shift.startTime))}h
</p>
<p className="text-xs text-muted-foreground">
{shift.assignments.length} guardie
</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Calendar className="h-12 w-12 text-muted-foreground mx-auto mb-3" />
<p className="text-sm text-muted-foreground">
Nessun turno completato
</p>
</div>
)}
</CardContent>
</Card>
</>
)}
</div>
);
}