Add weekly guard schedule view with shift details
Implements the "Guardie Settimanale" page, displaying a summarized weekly schedule per guard and location. Includes a dialog for detailed shift information (fixed or mobile) upon clicking a cell, integrating new components and logic for interactive schedule viewing. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/EPTvOHB
This commit is contained in:
parent
a10b50e7e9
commit
d868c6ee31
@ -3,9 +3,12 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Users } from "lucide-react";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Users, Clock, MapPin, Navigation, ExternalLink } from "lucide-react";
|
||||||
import { format, parseISO, addDays, startOfWeek, addWeeks } from "date-fns";
|
import { format, parseISO, addDays, startOfWeek, addWeeks } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
|
||||||
type AbsenceType = "sick_leave" | "vacation" | "personal_leave" | "injury";
|
type AbsenceType = "sick_leave" | "vacation" | "personal_leave" | "injury";
|
||||||
|
|
||||||
@ -52,11 +55,19 @@ const ABSENCE_LABELS: Record<AbsenceType, string> = {
|
|||||||
injury: "Infortunio",
|
injury: "Infortunio",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DialogData = {
|
||||||
|
type: "fixed" | "mobile";
|
||||||
|
guardName: string;
|
||||||
|
date: string;
|
||||||
|
data: any;
|
||||||
|
} | null;
|
||||||
|
|
||||||
export default function WeeklyGuards() {
|
export default function WeeklyGuards() {
|
||||||
const [selectedLocation, setSelectedLocation] = useState<string>("roccapiemonte");
|
const [selectedLocation, setSelectedLocation] = useState<string>("roccapiemonte");
|
||||||
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(() =>
|
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(() =>
|
||||||
startOfWeek(new Date(), { weekStartsOn: 1 }) // Inizia lunedì
|
startOfWeek(new Date(), { weekStartsOn: 1 }) // Inizia lunedì
|
||||||
);
|
);
|
||||||
|
const [dialogData, setDialogData] = useState<DialogData>(null);
|
||||||
|
|
||||||
const { data: scheduleData, isLoading } = useQuery<WeeklyScheduleResponse>({
|
const { data: scheduleData, isLoading } = useQuery<WeeklyScheduleResponse>({
|
||||||
queryKey: ["/api/weekly-guards-schedule", selectedLocation, format(currentWeekStart, "yyyy-MM-dd")],
|
queryKey: ["/api/weekly-guards-schedule", selectedLocation, format(currentWeekStart, "yyyy-MM-dd")],
|
||||||
@ -131,17 +142,18 @@ export default function WeeklyGuards() {
|
|||||||
setCurrentWeekStart(prev => addWeeks(prev, 1));
|
setCurrentWeekStart(prev => addWeeks(prev, 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCellClick = (activity: ReturnType<typeof getDayActivity>) => {
|
const handleCellClick = (guardData: GuardScheduleData, activity: ReturnType<typeof getDayActivity>, date: Date) => {
|
||||||
if (!activity) return;
|
if (!activity || activity.type === "absence") return;
|
||||||
|
|
||||||
if (activity.type === "fixed") {
|
const guardName = `${guardData.guard.lastName} ${guardData.guard.firstName}`;
|
||||||
// TODO: Aprire dialog turno fisso
|
const dateStr = format(date, "EEEE dd MMMM yyyy", { locale: it });
|
||||||
console.log("Open fixed shift dialog:", activity.data);
|
|
||||||
} else if (activity.type === "mobile") {
|
setDialogData({
|
||||||
// TODO: Aprire dialog turno mobile
|
type: activity.type,
|
||||||
console.log("Open mobile shift dialog:", activity.data);
|
guardName,
|
||||||
}
|
date: dateStr,
|
||||||
// Se absence, non fare nulla
|
data: activity.data,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -282,7 +294,7 @@ export default function WeeklyGuards() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full h-auto text-xs px-2 py-1.5 whitespace-normal hover-elevate"
|
className="w-full h-auto text-xs px-2 py-1.5 whitespace-normal hover-elevate"
|
||||||
onClick={() => handleCellClick(activity)}
|
onClick={() => handleCellClick(guardData, activity, day)}
|
||||||
data-testid={`button-shift-${guardData.guard.id}-${dayIndex}`}
|
data-testid={`button-shift-${guardData.guard.id}-${dayIndex}`}
|
||||||
>
|
>
|
||||||
{activity.label}
|
{activity.label}
|
||||||
@ -310,6 +322,109 @@ export default function WeeklyGuards() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Dialog Dettaglio Turno */}
|
||||||
|
<Dialog open={!!dialogData} onOpenChange={() => setDialogData(null)}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
{dialogData?.type === "fixed" ? (
|
||||||
|
<>
|
||||||
|
<MapPin className="h-5 w-5" />
|
||||||
|
Turno Fisso - {dialogData.guardName}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Navigation className="h-5 w-5" />
|
||||||
|
Turno Mobile - {dialogData.guardName}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{dialogData?.date}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{dialogData && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{dialogData.type === "fixed" ? (
|
||||||
|
// Dettagli turno fisso
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-muted/30 p-3 rounded-md space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Sito</span>
|
||||||
|
<span className="text-sm font-medium">{dialogData.data.siteName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Orario</span>
|
||||||
|
<div className="flex items-center gap-1 text-sm font-medium">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{format(new Date(dialogData.data.plannedStartTime), "HH:mm")} - {format(new Date(dialogData.data.plannedEndTime), "HH:mm")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Durata</span>
|
||||||
|
<span className="text-sm font-bold">
|
||||||
|
{Math.round((new Date(dialogData.data.plannedEndTime).getTime() - new Date(dialogData.data.plannedStartTime).getTime()) / (1000 * 60 * 60))}h
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-md">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Per modificare questo turno, vai alla pagina <Link href="/general-planning" className="text-primary font-medium hover:underline">Planning Fissi</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Dettagli turno mobile
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-muted/30 p-3 rounded-md space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Tipo</span>
|
||||||
|
<Badge variant="outline">Pattuglia</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Orario</span>
|
||||||
|
<div className="flex items-center gap-1 text-sm font-medium">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{dialogData.data.startTime} - {dialogData.data.endTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-md">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Per visualizzare il percorso completo e modificare il turno, vai alla pagina <Link href="/planning-mobile" className="text-primary font-medium hover:underline">Planning Mobile</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDialogData(null)}>
|
||||||
|
Chiudi
|
||||||
|
</Button>
|
||||||
|
{dialogData?.type === "fixed" ? (
|
||||||
|
<Link href="/general-planning">
|
||||||
|
<Button data-testid="button-goto-planning-fissi">
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
Vai a Planning Fissi
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link href="/planning-mobile">
|
||||||
|
<Button data-testid="button-goto-planning-mobile">
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
Vai a Planning Mobile
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user