VigilanzaTurni/client/src/pages/planning-view-fixed-agent.tsx
marco370 e0504f0a13 Add planning consultation views and reorganize sidebar navigation
Introduce new planning consultation pages for fixed and mobile agents, refactor sidebar navigation into logical groups, and enhance shift assignment logic by preventing double-booking of guards.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/kDVJJUd
2025-10-23 16:34:28 +00:00

274 lines
10 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Calendar, Shield, Car, MapPin, Clock, ChevronLeft, ChevronRight } from "lucide-react";
import { format, addDays, startOfWeek } from "date-fns";
import { it } from "date-fns/locale";
type Location = "roccapiemonte" | "milano" | "roma";
export default function PlanningViewFixedAgent() {
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
const [weekStart, setWeekStart] = useState<string>(
format(startOfWeek(new Date(), { weekStartsOn: 1 }), "yyyy-MM-dd")
);
const locationLabels: Record<Location, string> = {
roccapiemonte: "Roccapiemonte",
milano: "Milano",
roma: "Roma",
};
// Query guardie per location
const { data: guards } = useQuery<any[]>({
queryKey: ["/api/guards", selectedLocation],
queryFn: async () => {
const response = await fetch("/api/guards");
if (!response.ok) throw new Error("Failed to fetch guards");
const allGuards = await response.json();
return allGuards.filter((g: any) => g.location === selectedLocation && g.isActive);
},
});
// Query planning agente fisso
const { data: planningData, isLoading } = useQuery<{
guard: {
id: string;
firstName: string;
lastName: string;
badgeNumber: string;
location: string;
};
weekStart: string;
assignments: Array<{
id: string;
siteId: string;
siteName: string;
siteAddress: string;
startTime: Date;
endTime: Date;
isArmedOnDuty: boolean;
hasVehicle: boolean;
vehicleId: string | null;
location: string;
}>;
}>({
queryKey: ["/api/planning/fixed-agent", selectedGuardId, weekStart],
queryFn: async () => {
const params = new URLSearchParams({
guardId: selectedGuardId,
weekStart,
});
const response = await fetch(`/api/planning/fixed-agent?${params.toString()}`);
if (!response.ok) throw new Error("Failed to fetch planning");
return response.json();
},
enabled: !!selectedGuardId,
});
const handlePreviousWeek = () => {
const prevWeek = new Date(weekStart);
prevWeek.setDate(prevWeek.getDate() - 7);
setWeekStart(format(prevWeek, "yyyy-MM-dd"));
};
const handleNextWeek = () => {
const nextWeek = new Date(weekStart);
nextWeek.setDate(nextWeek.getDate() + 7);
setWeekStart(format(nextWeek, "yyyy-MM-dd"));
};
// Raggruppa assignments per giorno
const assignmentsByDay: Record<string, typeof planningData.assignments> = {};
if (planningData) {
for (let i = 0; i < 7; i++) {
const dayDate = format(addDays(new Date(weekStart), i), "yyyy-MM-dd");
assignmentsByDay[dayDate] = planningData.assignments.filter((a) => {
const assignmentDate = format(new Date(a.startTime), "yyyy-MM-dd");
return assignmentDate === dayDate;
});
}
}
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Planning Agente Fisso - Consultazione</h1>
<p className="text-muted-foreground mt-1">
Visualizza i turni fissi pianificati per un agente
</p>
</div>
</div>
{/* Filtri */}
<Card>
<CardHeader>
<CardTitle>Filtri</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium mb-2 block">Sede</label>
<Select value={selectedLocation} onValueChange={(v) => setSelectedLocation(v as Location)}>
<SelectTrigger data-testid="select-location">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Guardia</label>
<Select value={selectedGuardId} onValueChange={setSelectedGuardId}>
<SelectTrigger data-testid="select-guard">
<SelectValue placeholder="Seleziona guardia" />
</SelectTrigger>
<SelectContent>
{guards?.map((guard) => (
<SelectItem key={guard.id} value={guard.id}>
{guard.firstName} {guard.lastName} - #{guard.badgeNumber}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Navigazione settimana */}
{selectedGuardId && (
<div className="flex items-center justify-between pt-4">
<Button variant="outline" size="sm" onClick={handlePreviousWeek} data-testid="button-prev-week">
<ChevronLeft className="h-4 w-4 mr-2" />
Settimana Precedente
</Button>
<div className="text-sm font-medium">
{format(new Date(weekStart), "dd MMM", { locale: it })} -{" "}
{format(addDays(new Date(weekStart), 6), "dd MMM yyyy", { locale: it })}
</div>
<Button variant="outline" size="sm" onClick={handleNextWeek} data-testid="button-next-week">
Settimana Successiva
<ChevronRight className="h-4 w-4 ml-2" />
</Button>
</div>
)}
</CardContent>
</Card>
{/* Planning Data */}
{selectedGuardId && planningData && (
<>
{/* Info Guardia */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>
{planningData.guard.firstName} {planningData.guard.lastName}
</span>
<Badge variant="outline">#{planningData.guard.badgeNumber}</Badge>
<Badge variant="secondary">{locationLabels[planningData.guard.location as Location]}</Badge>
</CardTitle>
<CardDescription>Turni fissi pianificati per la settimana</CardDescription>
</CardHeader>
</Card>
{/* Griglia Settimanale */}
<div className="grid grid-cols-1 gap-4">
{[0, 1, 2, 3, 4, 5, 6].map((dayOffset) => {
const dayDate = addDays(new Date(weekStart), dayOffset);
const dayKey = format(dayDate, "yyyy-MM-dd");
const dayAssignments = assignmentsByDay[dayKey] || [];
return (
<Card key={dayKey}>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Calendar className="h-4 w-4" />
{format(dayDate, "EEEE dd MMMM", { locale: it })}
</CardTitle>
</CardHeader>
<CardContent>
{dayAssignments.length > 0 ? (
<div className="space-y-3">
{dayAssignments.map((assignment) => (
<div
key={assignment.id}
className="p-4 border rounded-lg space-y-2"
data-testid={`assignment-${assignment.id}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="font-medium">{assignment.siteName}</div>
<div className="text-sm text-muted-foreground flex items-center gap-1 mt-1">
<MapPin className="h-3 w-3" />
{assignment.siteAddress}
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="text-xs">
<Clock className="h-3 w-3 mr-1" />
{format(new Date(assignment.startTime), "HH:mm")} -{" "}
{format(new Date(assignment.endTime), "HH:mm")}
</Badge>
{assignment.isArmedOnDuty && (
<Badge variant="secondary" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
{assignment.hasVehicle && (
<Badge variant="secondary" className="text-xs">
<Car className="h-3 w-3 mr-1" />
Automezzo
</Badge>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground py-4 text-center">Nessun turno pianificato</p>
)}
</CardContent>
</Card>
);
})}
</div>
</>
)}
{selectedGuardId && isLoading && (
<Card>
<CardContent className="py-16 text-center">
<p className="text-muted-foreground">Caricamento planning...</p>
</CardContent>
</Card>
)}
{!selectedGuardId && (
<Card>
<CardContent className="py-16 text-center">
<Calendar className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<p className="text-lg font-medium">Seleziona una guardia</p>
<p className="text-sm text-muted-foreground mt-2">
Scegli una guardia per visualizzare i turni fissi pianificati
</p>
</CardContent>
</Card>
)}
</div>
);
}