diff --git a/client/src/App.tsx b/client/src/App.tsx index 8ac0aa0..edb4d8c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -30,6 +30,7 @@ import PlanningMobile from "@/pages/planning-mobile"; import MyShiftsFixed from "@/pages/my-shifts-fixed"; import MyShiftsMobile from "@/pages/my-shifts-mobile"; import SitePlanningView from "@/pages/site-planning-view"; +import WeeklyGuards from "@/pages/weekly-guards"; function Router() { const { isAuthenticated, isLoading } = useAuth(); @@ -57,6 +58,7 @@ function Router() { + diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index e636d2f..7ccab1b 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -82,6 +82,12 @@ const menuItems: MenuItem[] = [ icon: ClipboardList, roles: ["admin", "coordinator"], }, + { + title: "Guardie Settimanale", + url: "/weekly-guards", + icon: Users, + roles: ["admin", "coordinator"], + }, ], }, { diff --git a/client/src/pages/weekly-guards.tsx b/client/src/pages/weekly-guards.tsx new file mode 100644 index 0000000..924fd5b --- /dev/null +++ b/client/src/pages/weekly-guards.tsx @@ -0,0 +1,315 @@ +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 { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Users } from "lucide-react"; +import { format, parseISO, addDays, startOfWeek, addWeeks } from "date-fns"; +import { it } from "date-fns/locale"; + +type AbsenceType = "sick_leave" | "vacation" | "personal_leave" | "injury"; + +interface GuardScheduleData { + guard: { + id: string; + firstName: string; + lastName: string; + badgeNumber: string; + }; + fixedShifts: Array<{ + assignmentId: string; + shiftId: string; + plannedStartTime: Date; + plannedEndTime: Date; + siteName: string; + siteId: string; + }>; + mobileShifts: Array<{ + routeId: string; + shiftDate: string; + startTime: string; + endTime: string; + }>; + absences: Array<{ + id: string; + type: AbsenceType; + startDate: string; + endDate: string; + }>; +} + +interface WeeklyScheduleResponse { + weekStart: string; + weekEnd: string; + location: string; + guards: GuardScheduleData[]; +} + +const ABSENCE_LABELS: Record = { + sick_leave: "Malattia", + vacation: "Ferie", + personal_leave: "Permesso", + injury: "Infortunio", +}; + +export default function WeeklyGuards() { + const [selectedLocation, setSelectedLocation] = useState("roccapiemonte"); + const [currentWeekStart, setCurrentWeekStart] = useState(() => + startOfWeek(new Date(), { weekStartsOn: 1 }) // Inizia lunedì + ); + + const { data: scheduleData, isLoading } = useQuery({ + queryKey: ["/api/weekly-guards-schedule", selectedLocation, format(currentWeekStart, "yyyy-MM-dd")], + enabled: !!selectedLocation, + }); + + // Helper per ottenere i giorni della settimana + const getWeekDays = () => { + const days = []; + for (let i = 0; i < 7; i++) { + days.push(addDays(currentWeekStart, i)); + } + return days; + }; + + const weekDays = getWeekDays(); + + // Helper per trovare l'attività di una guardia in un giorno specifico + const getDayActivity = (guardData: GuardScheduleData, date: Date) => { + const dateStr = format(date, "yyyy-MM-dd"); + + // Controlla assenze + const absence = guardData.absences.find(abs => { + const startDate = abs.startDate; + const endDate = abs.endDate; + return dateStr >= startDate && dateStr <= endDate; + }); + + if (absence) { + return { + type: "absence" as const, + label: ABSENCE_LABELS[absence.type], + data: absence, + }; + } + + // Controlla turni fissi + const fixedShift = guardData.fixedShifts.find(shift => { + const shiftDate = format(new Date(shift.plannedStartTime), "yyyy-MM-dd"); + return shiftDate === dateStr; + }); + + if (fixedShift) { + const startTime = format(new Date(fixedShift.plannedStartTime), "HH:mm"); + const endTime = format(new Date(fixedShift.plannedEndTime), "HH:mm"); + return { + type: "fixed" as const, + label: `${fixedShift.siteName} ${startTime}-${endTime}`, + data: fixedShift, + }; + } + + // Controlla turni mobili + const mobileShift = guardData.mobileShifts.find(shift => shift.shiftDate === dateStr); + + if (mobileShift) { + return { + type: "mobile" as const, + label: `Pattuglia ${mobileShift.startTime}-${mobileShift.endTime}`, + data: mobileShift, + }; + } + + return null; + }; + + const handlePreviousWeek = () => { + setCurrentWeekStart(prev => addWeeks(prev, -1)); + }; + + const handleNextWeek = () => { + setCurrentWeekStart(prev => addWeeks(prev, 1)); + }; + + const handleCellClick = (activity: ReturnType) => { + if (!activity) return; + + if (activity.type === "fixed") { + // TODO: Aprire dialog turno fisso + console.log("Open fixed shift dialog:", activity.data); + } else if (activity.type === "mobile") { + // TODO: Aprire dialog turno mobile + console.log("Open mobile shift dialog:", activity.data); + } + // Se absence, non fare nulla + }; + + return ( +
+
+
+

+ + Guardie Settimanale +

+

+ Vista riepilogativa delle assegnazioni settimanali per sede +

+
+
+ + {/* Filtri */} + + + + + Filtri Visualizzazione + + + Seleziona sede e settimana per visualizzare le assegnazioni + + + +
+
+ + +
+ +
+ + +
+ + {format(currentWeekStart, "d MMM", { locale: it })} - {format(addDays(currentWeekStart, 6), "d MMM yyyy", { locale: it })} + +
+ + +
+
+
+
+ + {/* Griglia Settimanale */} + {isLoading ? ( + + +

Caricamento...

+
+
+ ) : scheduleData && scheduleData.guards.length > 0 ? ( + + +
+ + + + + {weekDays.map((day, index) => ( + + ))} + + + + {scheduleData.guards.map((guardData) => ( + + + {weekDays.map((day, dayIndex) => { + const activity = getDayActivity(guardData, day); + return ( + + ); + })} + + ))} + +
+ Guardia + +
{format(day, "EEE", { locale: it })}
+
+ {format(day, "dd/MM")} +
+
+
+ + {guardData.guard.lastName} {guardData.guard.firstName} + + + #{guardData.guard.badgeNumber} + +
+
+ {activity ? ( + activity.type === "absence" ? ( +
+ {activity.label} +
+ ) : ( + + ) + ) : ( + - + )} +
+
+
+
+ ) : ( + + +

+ Nessuna guardia trovata per la sede selezionata +

+
+
+ )} +
+ ); +}