Implement new routes and UI components for guards to view fixed and mobile shifts, and for coordinators to view site-specific guard assignments. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/WbUtQAg
235 lines
8.3 KiB
TypeScript
235 lines
8.3 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Calendar, MapPin, Clock, Shield, Car, ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { format, addDays, startOfWeek, parseISO, isValid } from "date-fns";
|
|
import { it } from "date-fns/locale";
|
|
|
|
interface ShiftAssignment {
|
|
id: string;
|
|
shiftId: string;
|
|
plannedStartTime: string;
|
|
plannedEndTime: string;
|
|
armed: boolean;
|
|
vehicleId: string | null;
|
|
vehiclePlate: string | null;
|
|
site: {
|
|
id: string;
|
|
name: string;
|
|
address: string;
|
|
location: string;
|
|
};
|
|
shift: {
|
|
shiftDate: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
};
|
|
}
|
|
|
|
export default function MyShiftsFixed() {
|
|
// Data iniziale: inizio settimana corrente
|
|
const [currentWeekStart, setCurrentWeekStart] = useState(() => {
|
|
const today = new Date();
|
|
return startOfWeek(today, { weekStartsOn: 1 });
|
|
});
|
|
|
|
// Query per recuperare i turni fissi della guardia loggata
|
|
const { data: user } = useQuery<any>({
|
|
queryKey: ["/api/auth/user"],
|
|
});
|
|
|
|
const { data: myShifts, isLoading } = useQuery<ShiftAssignment[]>({
|
|
queryKey: ["/api/my-shifts/fixed", currentWeekStart.toISOString()],
|
|
queryFn: async () => {
|
|
const weekEnd = addDays(currentWeekStart, 6);
|
|
const response = await fetch(
|
|
`/api/my-shifts/fixed?startDate=${currentWeekStart.toISOString()}&endDate=${weekEnd.toISOString()}`
|
|
);
|
|
if (!response.ok) throw new Error("Failed to fetch shifts");
|
|
return response.json();
|
|
},
|
|
enabled: !!user,
|
|
});
|
|
|
|
// Naviga settimana precedente
|
|
const handlePreviousWeek = () => {
|
|
setCurrentWeekStart(addDays(currentWeekStart, -7));
|
|
};
|
|
|
|
// Naviga settimana successiva
|
|
const handleNextWeek = () => {
|
|
setCurrentWeekStart(addDays(currentWeekStart, 7));
|
|
};
|
|
|
|
// Raggruppa i turni per giorno
|
|
const shiftsByDay = myShifts?.reduce((acc, shift) => {
|
|
const date = shift.shift.shiftDate;
|
|
if (!acc[date]) acc[date] = [];
|
|
acc[date].push(shift);
|
|
return acc;
|
|
}, {} as Record<string, ShiftAssignment[]>) || {};
|
|
|
|
// Genera array di 7 giorni della settimana
|
|
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeekStart, i));
|
|
|
|
const locationLabels: Record<string, string> = {
|
|
roccapiemonte: "Roccapiemonte",
|
|
milano: "Milano",
|
|
roma: "Roma",
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col p-6 space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold" data-testid="title-my-shifts-fixed">
|
|
I Miei Turni Fissi
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Visualizza i tuoi turni con orari e dotazioni operative
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigazione settimana */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handlePreviousWeek}
|
|
data-testid="button-prev-week"
|
|
>
|
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
Settimana Precedente
|
|
</Button>
|
|
<CardTitle className="text-center">
|
|
{format(currentWeekStart, "dd MMMM", { locale: it })} -{" "}
|
|
{format(addDays(currentWeekStart, 6), "dd MMMM yyyy", { locale: it })}
|
|
</CardTitle>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleNextWeek}
|
|
data-testid="button-next-week"
|
|
>
|
|
Settimana Successiva
|
|
<ChevronRight className="h-4 w-4 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
{/* Loading state */}
|
|
{isLoading && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
Caricamento turni...
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Griglia giorni settimana */}
|
|
{!isLoading && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{weekDays.map((day) => {
|
|
const dateStr = format(day, "yyyy-MM-dd");
|
|
const dayShifts = shiftsByDay[dateStr] || [];
|
|
|
|
return (
|
|
<Card key={dateStr} data-testid={`day-card-${dateStr}`}>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg">
|
|
{format(day, "EEEE dd/MM", { locale: it })}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{dayShifts.length === 0
|
|
? "Nessun turno"
|
|
: `${dayShifts.length} turno${dayShifts.length > 1 ? "i" : ""}`}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{dayShifts.length === 0 ? (
|
|
<div className="text-center py-4 text-sm text-muted-foreground">
|
|
Riposo
|
|
</div>
|
|
) : (
|
|
dayShifts.map((shift) => {
|
|
// Parsing sicuro orari
|
|
let startTime = "N/A";
|
|
let endTime = "N/A";
|
|
|
|
if (shift.plannedStartTime) {
|
|
const parsedStart = parseISO(shift.plannedStartTime);
|
|
if (isValid(parsedStart)) {
|
|
startTime = format(parsedStart, "HH:mm");
|
|
}
|
|
}
|
|
|
|
if (shift.plannedEndTime) {
|
|
const parsedEnd = parseISO(shift.plannedEndTime);
|
|
if (isValid(parsedEnd)) {
|
|
endTime = format(parsedEnd, "HH:mm");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={shift.id}
|
|
className="border rounded-lg p-3 space-y-2"
|
|
data-testid={`shift-${shift.id}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 space-y-1">
|
|
<p className="font-semibold text-sm">{shift.site.name}</p>
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<MapPin className="h-3 w-3" />
|
|
<span>{locationLabels[shift.site.location] || shift.site.location}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 text-sm">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">
|
|
{startTime} - {endTime}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Dotazioni */}
|
|
<div className="flex gap-2 flex-wrap">
|
|
{shift.armed && (
|
|
<Badge variant="outline" className="text-xs">
|
|
<Shield className="h-3 w-3 mr-1" />
|
|
Armato
|
|
</Badge>
|
|
)}
|
|
{shift.vehicleId && (
|
|
<Badge variant="outline" className="text-xs">
|
|
<Car className="h-3 w-3 mr-1" />
|
|
{shift.vehiclePlate || "Automezzo"}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="pt-1 border-t text-xs text-muted-foreground">
|
|
{shift.site.address}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|