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
274 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|