From e0504f0a137aac14df3b98d033fe4e74eebccfdb Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Thu, 23 Oct 2025 16:34:28 +0000 Subject: [PATCH] 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 --- client/src/App.tsx | 12 +- client/src/components/app-sidebar.tsx | 183 ++++++++--- .../src/pages/planning-view-fixed-agent.tsx | 273 +++++++++++++++++ .../src/pages/planning-view-mobile-agent.tsx | 275 +++++++++++++++++ client/src/pages/shifts.tsx | 35 ++- replit.md | 14 + server/routes.ts | 285 +++++++++++++++++- 7 files changed, 1013 insertions(+), 64 deletions(-) create mode 100644 client/src/pages/planning-view-fixed-agent.tsx create mode 100644 client/src/pages/planning-view-mobile-agent.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 8ac0aa0..59e8849 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -21,15 +21,13 @@ import AdvancedPlanning from "@/pages/advanced-planning"; import Vehicles from "@/pages/vehicles"; import Parameters from "@/pages/parameters"; import Services from "@/pages/services"; -import Planning from "@/pages/planning"; -import OperationalPlanning from "@/pages/operational-planning"; -import GeneralPlanning from "@/pages/general-planning"; -import ServicePlanning from "@/pages/service-planning"; import Customers from "@/pages/customers"; 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 PlanningViewFixedAgent from "@/pages/planning-view-fixed-agent"; +import PlanningViewMobileAgent from "@/pages/planning-view-mobile-agent"; function Router() { const { isAuthenticated, isLoading } = useAuth(); @@ -48,15 +46,13 @@ function Router() { - - - - + + diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index 561e184..0856149 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -31,55 +31,67 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/theme-toggle"; -const menuItems = [ +const dashboardItems = [ { title: "Dashboard", url: "/", icon: Shield, roles: ["admin", "coordinator", "guard", "client"], }, +]; + +const creationItems = [ { - title: "Turni", + title: "Turni Fissi", url: "/shifts", icon: Calendar, - roles: ["admin", "coordinator", "guard"], - }, - { - title: "Pianificazione", - url: "/planning", - icon: ClipboardList, roles: ["admin", "coordinator"], }, { - title: "Pianificazione Operativa", - url: "/operational-planning", - icon: Calendar, - roles: ["admin", "coordinator"], - }, - { - title: "Planning Fissi", - url: "/general-planning", - icon: BarChart3, - roles: ["admin", "coordinator"], - }, - { - title: "Planning Mobile", + title: "Pattuglie Mobile", url: "/planning-mobile", icon: Navigation, roles: ["admin", "coordinator"], }, +]; + +const consultationItems = [ { - title: "Planning di Servizio", - url: "/service-planning", - icon: ClipboardList, + title: "Planning Agente Fisso", + url: "/planning-view-fixed-agent", + icon: Users, roles: ["admin", "coordinator"], }, { - title: "Gestione Pianificazioni", - url: "/advanced-planning", - icon: ClipboardList, + title: "Planning Agente Mobile", + url: "/planning-view-mobile-agent", + icon: Navigation, roles: ["admin", "coordinator"], }, + { + title: "Planning Sito", + url: "/site-planning-view", + icon: MapPin, + roles: ["admin", "coordinator"], + }, +]; + +const personalItems = [ + { + title: "I Miei Turni Fissi", + url: "/my-shifts-fixed", + icon: Calendar, + roles: ["guard"], + }, + { + title: "Le Mie Pattuglie", + url: "/my-shifts-mobile", + icon: Navigation, + roles: ["guard"], + }, +]; + +const registryItems = [ { title: "Guardie", url: "/guards", @@ -110,12 +122,18 @@ const menuItems = [ icon: Car, roles: ["admin", "coordinator"], }, +]; + +const reportingItems = [ { title: "Report", url: "/reports", icon: BarChart3, roles: ["admin", "coordinator", "client"], }, +]; + +const systemItems = [ { title: "Notifiche", url: "/notifications", @@ -140,8 +158,26 @@ export function AppSidebar() { const { user } = useAuth(); const [location] = useLocation(); - const filteredItems = menuItems.filter( - (item) => user && item.roles.includes(user.role) + const filterItems = (items: typeof dashboardItems) => + items.filter((item) => user && item.roles.includes(user.role)); + + const renderMenuItems = (items: typeof dashboardItems) => ( + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + ); return ( @@ -157,27 +193,74 @@ export function AppSidebar() { - - Menu Principale - - - {filteredItems.map((item) => ( - - - - - {item.title} - - - - ))} - - - + {/* Dashboard */} + {filterItems(dashboardItems).length > 0 && ( + + + {renderMenuItems(filterItems(dashboardItems))} + + + )} + + {/* Planning Operativo - Creazione */} + {filterItems(creationItems).length > 0 && ( + + Planning - Creazione + + {renderMenuItems(filterItems(creationItems))} + + + )} + + {/* Planning Operativo - Consultazione */} + {filterItems(consultationItems).length > 0 && ( + + Planning - Consultazione + + {renderMenuItems(filterItems(consultationItems))} + + + )} + + {/* Viste Personali (Guard) */} + {filterItems(personalItems).length > 0 && ( + + I Miei Turni + + {renderMenuItems(filterItems(personalItems))} + + + )} + + {/* Anagrafica */} + {filterItems(registryItems).length > 0 && ( + + Anagrafica + + {renderMenuItems(filterItems(registryItems))} + + + )} + + {/* Report */} + {filterItems(reportingItems).length > 0 && ( + + Reporting + + {renderMenuItems(filterItems(reportingItems))} + + + )} + + {/* Sistema */} + {filterItems(systemItems).length > 0 && ( + + Sistema + + {renderMenuItems(filterItems(systemItems))} + + + )} diff --git a/client/src/pages/planning-view-fixed-agent.tsx b/client/src/pages/planning-view-fixed-agent.tsx new file mode 100644 index 0000000..0c8bec6 --- /dev/null +++ b/client/src/pages/planning-view-fixed-agent.tsx @@ -0,0 +1,273 @@ +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("roccapiemonte"); + const [selectedGuardId, setSelectedGuardId] = useState(""); + const [weekStart, setWeekStart] = useState( + format(startOfWeek(new Date(), { weekStartsOn: 1 }), "yyyy-MM-dd") + ); + + const locationLabels: Record = { + roccapiemonte: "Roccapiemonte", + milano: "Milano", + roma: "Roma", + }; + + // Query guardie per location + const { data: guards } = useQuery({ + 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 = {}; + 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 ( +
+
+
+

Planning Agente Fisso - Consultazione

+

+ Visualizza i turni fissi pianificati per un agente +

+
+
+ + {/* Filtri */} + + + Filtri + + +
+
+ + +
+ +
+ + +
+
+ + {/* Navigazione settimana */} + {selectedGuardId && ( +
+ +
+ {format(new Date(weekStart), "dd MMM", { locale: it })} -{" "} + {format(addDays(new Date(weekStart), 6), "dd MMM yyyy", { locale: it })} +
+ +
+ )} +
+
+ + {/* Planning Data */} + {selectedGuardId && planningData && ( + <> + {/* Info Guardia */} + + + + + {planningData.guard.firstName} {planningData.guard.lastName} + + #{planningData.guard.badgeNumber} + {locationLabels[planningData.guard.location as Location]} + + Turni fissi pianificati per la settimana + + + + {/* Griglia Settimanale */} +
+ {[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 ( + + + + + {format(dayDate, "EEEE dd MMMM", { locale: it })} + + + + {dayAssignments.length > 0 ? ( +
+ {dayAssignments.map((assignment) => ( +
+
+
+
{assignment.siteName}
+
+ + {assignment.siteAddress} +
+
+
+ +
+ + + {format(new Date(assignment.startTime), "HH:mm")} -{" "} + {format(new Date(assignment.endTime), "HH:mm")} + + + {assignment.isArmedOnDuty && ( + + + Armato + + )} + + {assignment.hasVehicle && ( + + + Automezzo + + )} +
+
+ ))} +
+ ) : ( +

Nessun turno pianificato

+ )} +
+
+ ); + })} +
+ + )} + + {selectedGuardId && isLoading && ( + + +

Caricamento planning...

+
+
+ )} + + {!selectedGuardId && ( + + + +

Seleziona una guardia

+

+ Scegli una guardia per visualizzare i turni fissi pianificati +

+
+
+ )} +
+ ); +} diff --git a/client/src/pages/planning-view-mobile-agent.tsx b/client/src/pages/planning-view-mobile-agent.tsx new file mode 100644 index 0000000..5ff2800 --- /dev/null +++ b/client/src/pages/planning-view-mobile-agent.tsx @@ -0,0 +1,275 @@ +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 { Badge } from "@/components/ui/badge"; +import { Calendar, MapPin, Clock, Car, Navigation, ChevronLeft, ChevronRight } from "lucide-react"; +import { format, addDays, startOfWeek, endOfWeek } from "date-fns"; +import { it } from "date-fns/locale"; +import { Button } from "@/components/ui/button"; + +type Location = "roccapiemonte" | "milano" | "roma"; + +export default function PlanningViewMobileAgent() { + const [selectedLocation, setSelectedLocation] = useState("roccapiemonte"); + const [selectedGuardId, setSelectedGuardId] = useState(""); + const [currentWeekStart, setCurrentWeekStart] = useState( + startOfWeek(new Date(), { weekStartsOn: 1 }) + ); + + const locationLabels: Record = { + roccapiemonte: "Roccapiemonte", + milano: "Milano", + roma: "Roma", + }; + + // Query guardie per location + const { data: guards } = useQuery({ + 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 && g.hasDriverLicense); + }, + }); + + // Query planning agente mobile per settimana + const { data: weekData, isLoading } = useQuery({ + queryKey: ["/api/planning/mobile-agent", selectedGuardId, currentWeekStart], + queryFn: async () => { + const startDate = format(currentWeekStart, "yyyy-MM-dd"); + const endDate = format(endOfWeek(currentWeekStart, { weekStartsOn: 1 }), "yyyy-MM-dd"); + const params = new URLSearchParams({ + guardId: selectedGuardId, + startDate, + endDate, + }); + const response = await fetch(`/api/planning/mobile-agent?${params.toString()}`); + if (!response.ok) throw new Error("Failed to fetch planning"); + return response.json(); + }, + enabled: !!selectedGuardId, + }); + + const handlePreviousWeek = () => { + setCurrentWeekStart((prev) => addDays(prev, -7)); + }; + + const handleNextWeek = () => { + setCurrentWeekStart((prev) => addDays(prev, 7)); + }; + + const handleToday = () => { + setCurrentWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 })); + }; + + // Generate 7 days for the week + const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeekStart, i)); + + return ( +
+
+
+

Planning Agente Mobile - Consultazione

+

+ Vista settimanale dei percorsi pattuglia pianificati per un agente +

+
+
+ + {/* Filtri */} + + + Filtri + + +
+
+ + +
+ +
+ + +
+
+ + {/* Navigazione settimana */} + {selectedGuardId && ( +
+ + +
+ {format(currentWeekStart, "dd MMM", { locale: it })} -{" "} + {format(endOfWeek(currentWeekStart, { weekStartsOn: 1 }), "dd MMM yyyy", { locale: it })} +
+ +
+ )} +
+
+ + {/* Planning Data - Vista Settimanale */} + {selectedGuardId && weekData && weekData.guard && ( + <> + {/* Info Guardia */} + + + + + {weekData.guard.firstName} {weekData.guard.lastName} + + #{weekData.guard.badgeNumber} + {locationLabels[weekData.guard.location as Location]} + + Percorsi pattuglia pianificati nella settimana + + + + {/* Griglia Settimanale */} +
+ {weekDays.map((day) => { + const dateStr = format(day, "yyyy-MM-dd"); + const dayData = weekData.days?.find((d: any) => d.date === dateStr); + + return ( + + + + + + {format(day, "EEEE dd/MM", { locale: it })} + + {dayData?.route && ( + + + {dayData.stops?.length || 0} tappe + + )} + + + + {dayData?.route ? ( +
+ {/* Orario e dettagli turno */} +
+ + + {dayData.route.startTime} - {dayData.route.endTime} + + + {dayData.route.hasVehicle && ( + + + Automezzo + + )} +
+ + {/* Lista tappe */} + {dayData.stops && dayData.stops.length > 0 && ( +
+
+ Sequenza Tappe +
+ {dayData.stops.map((stop: any) => ( +
+
+ {stop.sequenceOrder} +
+
+
{stop.siteName}
+
+ + {stop.siteAddress} +
+ {stop.estimatedArrivalTime && ( +
+ Arrivo: {stop.estimatedArrivalTime} +
+ )} +
+
+ ))} +
+ )} + + {/* Note */} + {dayData.route.notes && ( +
+

+ Note: {dayData.route.notes} +

+
+ )} +
+ ) : ( +
+ +

Nessun percorso pianificato

+
+ )} +
+
+ ); + })} +
+ + )} + + {selectedGuardId && isLoading && ( + + +

Caricamento planning...

+
+
+ )} + + {!selectedGuardId && ( + + + +

Seleziona una guardia

+

+ Scegli una guardia per visualizzare i percorsi pattuglia pianificati +

+
+
+ )} +
+ ); +} diff --git a/client/src/pages/shifts.tsx b/client/src/pages/shifts.tsx index 3f478d4..ba7cbf9 100644 --- a/client/src/pages/shifts.tsx +++ b/client/src/pages/shifts.tsx @@ -42,6 +42,25 @@ export default function Shifts() { queryKey: ["/api/guards"], }); + // Query guardie con controllo vincolo esclusività mobile (per assegnazione turni) + const { data: guardsForShift } = useQuery>({ + queryKey: ["/api/guards/for-shift", selectedShift?.startTime, selectedShift?.site.location], + queryFn: async () => { + if (!selectedShift) return []; + + const shiftDate = format(new Date(selectedShift.startTime), "yyyy-MM-dd"); + const params = new URLSearchParams({ + date: shiftDate, + location: selectedShift.site.location, + }); + + const response = await fetch(`/api/guards/for-shift?${params.toString()}`); + if (!response.ok) throw new Error("Failed to fetch guards for shift"); + return response.json(); + }, + enabled: !!selectedShift && isAssignDialogOpen, + }); + // Filter data by location const filteredShifts = selectedLocation === "all" ? shifts @@ -563,11 +582,12 @@ export default function Shifts() { {/* Guards List */}

Guardie Disponibili

- {guards && guards.length > 0 ? ( + {guardsForShift && guardsForShift.length > 0 ? (
- {filteredGuards?.map((guard) => { + {guardsForShift?.map((guard) => { const assigned = isGuardAssigned(guard.id); const canAssign = canGuardBeAssigned(guard); + const bookedOnMobile = guard.isBookedMobile; return (

- {guard.user?.firstName} {guard.user?.lastName} + {guard.firstName} {guard.lastName}

#{guard.badgeNumber} + {bookedOnMobile && ( + + Su pattuglia mobile + + )}
{guard.isArmed && ( @@ -615,10 +640,10 @@ export default function Shifts() { size="sm" variant={assigned ? "secondary" : "default"} onClick={() => handleAssignGuard(guard.id)} - disabled={assigned || !canAssign} + disabled={assigned || !canAssign || bookedOnMobile} data-testid={`button-assign-guard-${guard.id}`} > - {assigned ? "Assegnato" : canAssign ? "Assegna" : "Non idoneo"} + {assigned ? "Assegnato" : bookedOnMobile ? "Su pattuglia" : canAssign ? "Assegna" : "Non idoneo"}
); diff --git a/replit.md b/replit.md index 467b9da..e15f60b 100644 --- a/replit.md +++ b/replit.md @@ -142,6 +142,20 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should - Backend endpoint: GET `/api/site-planning/:siteId` with date range filters - **Impact**: Complete end-to-end planning system supporting both coordinator and guard roles with database-backed route planning and operational equipment tracking +### Planning Consultation Pages & Sidebar Reorganization (October 23, 2025) +- **Issue**: Coordinators needed separate consultation views to review planned shifts without mixing creation and consultation workflows. Sidebar was cluttered with deprecated planning pages. +- **Solution**: + - **New Consultation Pages**: + - `planning-view-fixed-agent.tsx`: Weekly view of guard's fixed shifts showing orari, dotazioni (armato, automezzo), location, sito + - `planning-view-mobile-agent.tsx`: Weekly grid view (7 days) of guard's patrol routes with site addresses, sequenced stops, and equipment + - Backend endpoint `/api/planning/mobile-agent` updated to accept startDate/endDate range and return `{ guard, days[] }` structure + - **Sidebar Reorganization**: + - Removed deprecated routes: `planning`, `operational-planning`, `general-planning`, `service-planning` + - Created logical groups: Dashboard, Planning-Creazione (Turni Fissi, Pattuglie Mobile), Planning-Consultazione (Planning Agente Fisso, Planning Agente Mobile, Planning Sito), I Miei Turni, Anagrafica, Reporting, Sistema + - Role-based filtering maintained for all groups + - **Routes Cleanup**: Removed deprecated planning page imports and routes from App.tsx +- **Impact**: Clear separation between creation (shifts.tsx, planning-mobile.tsx) and consultation (planning-view-*) workflows. Coordinators can now efficiently review weekly assignments for guards and sites without navigating through creation interfaces. + ## External Dependencies - **Replit Auth**: For OpenID Connect (OIDC) based authentication. - **Neon**: Managed PostgreSQL database service. diff --git a/server/routes.ts b/server/routes.ts index e563a53..a1c28ef 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -331,8 +331,291 @@ export async function registerRoutes(app: Express): Promise { res.status(500).json({ message: "Failed to fetch guards availability" }); } }); + + // Get guards for shift assignment with mobile exclusivity check + app.get("/api/guards/for-shift", isAuthenticated, async (req, res) => { + try { + const { date, location } = req.query; + + if (!date || typeof date !== "string") { + return res.status(400).json({ message: "Date parameter required (YYYY-MM-DD)" }); + } + + if (!location || !["roccapiemonte", "milano", "roma"].includes(location as string)) { + return res.status(400).json({ message: "Valid location parameter required" }); + } + + // Valida formato data + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(date)) { + return res.status(400).json({ message: "Invalid date format, use YYYY-MM-DD" }); + } + + // Ottieni tutte le guardie per la location + const allGuards = await db + .select({ + id: guards.id, + userId: guards.userId, + firstName: guards.firstName, + lastName: guards.lastName, + badgeNumber: guards.badgeNumber, + location: guards.location, + hasDriverLicense: guards.hasDriverLicense, + isActive: guards.isActive, + createdAt: guards.createdAt, + updatedAt: guards.updatedAt, + }) + .from(guards) + .where( + and( + eq(guards.location, location as any), + eq(guards.isActive, true) + ) + ) + .orderBy(guards.lastName, guards.firstName); + + // Verifica quali guardie hanno patrol routes (mobile) per questa data + const patrolRoutesForDate = await db + .select({ + guardId: patrolRoutes.guardId, + }) + .from(patrolRoutes) + .where( + and( + eq(patrolRoutes.shiftDate, date), + eq(patrolRoutes.location, location as any), + ne(patrolRoutes.status, "cancelled") + ) + ); + + const guardsOnMobile = new Set(patrolRoutesForDate.map(pr => pr.guardId)); + + // Aggiungi flag isBookedMobile per ogni guardia + const guardsWithMobileFlag = allGuards.map(guard => ({ + ...guard, + isBookedMobile: guardsOnMobile.has(guard.id), + })); + + res.json(guardsWithMobileFlag); + } catch (error) { + console.error("Error fetching guards for shift:", error); + res.status(500).json({ message: "Failed to fetch guards for shift" }); + } + }); + + // ============= PLANNING CONSULTATION ROUTES ============= - // Get vehicles available for a location + // GET /api/planning/fixed-agent - Vista consultazione planning per agente fisso + app.get("/api/planning/fixed-agent", isAuthenticated, async (req, res) => { + try { + const { guardId, weekStart } = req.query; + + if (!guardId || typeof guardId !== "string") { + return res.status(400).json({ message: "GuardId parameter required" }); + } + + if (!weekStart || typeof weekStart !== "string") { + return res.status(400).json({ message: "WeekStart parameter required (YYYY-MM-DD)" }); + } + + // Valida formato data + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(weekStart)) { + return res.status(400).json({ message: "Invalid weekStart format, use YYYY-MM-DD" }); + } + + // Calcola fine settimana + const weekStartDate = new Date(weekStart); + weekStartDate.setHours(0, 0, 0, 0); + + const weekEndDate = new Date(weekStart); + weekEndDate.setDate(weekEndDate.getDate() + 6); + weekEndDate.setHours(23, 59, 59, 999); + + // Ottieni info guardia + const guard = await db + .select({ + id: guards.id, + firstName: guards.firstName, + lastName: guards.lastName, + badgeNumber: guards.badgeNumber, + location: guards.location, + }) + .from(guards) + .where(eq(guards.id, guardId)) + .limit(1); + + if (guard.length === 0) { + return res.status(404).json({ message: "Guard not found" }); + } + + // Ottieni tutti i turni fissi della guardia per la settimana + const assignments = await db + .select({ + id: shiftAssignments.id, + shiftId: shifts.id, + siteId: sites.id, + siteName: sites.name, + siteAddress: sites.address, + startTime: shifts.startTime, + endTime: shifts.endTime, + isArmedOnDuty: shiftAssignments.isArmedOnDuty, + assignedVehicleId: shiftAssignments.assignedVehicleId, + location: sites.location, + }) + .from(shiftAssignments) + .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) + .innerJoin(sites, eq(shifts.siteId, sites.id)) + .where( + and( + eq(shiftAssignments.guardId, guardId), + gte(shifts.startTime, weekStartDate), + lte(shifts.startTime, weekEndDate) + ) + ) + .orderBy(shifts.startTime); + + res.json({ + guard: guard[0], + weekStart, + assignments: assignments.map(a => ({ + id: a.id, + shiftId: a.shiftId, + siteId: a.siteId, + siteName: a.siteName, + siteAddress: a.siteAddress, + startTime: a.startTime, + endTime: a.endTime, + isArmedOnDuty: a.isArmedOnDuty || false, + hasVehicle: !!a.assignedVehicleId, + vehicleId: a.assignedVehicleId, + location: a.location, + })), + }); + } catch (error) { + console.error("Error fetching fixed agent planning:", error); + res.status(500).json({ message: "Failed to fetch fixed agent planning" }); + } + }); + + // GET /api/planning/mobile-agent - Vista consultazione planning per agente mobile + app.get("/api/planning/mobile-agent", isAuthenticated, async (req, res) => { + try { + const { guardId, startDate, endDate } = req.query; + + if (!guardId || typeof guardId !== "string") { + return res.status(400).json({ message: "GuardId parameter required" }); + } + + if (!startDate || typeof startDate !== "string") { + return res.status(400).json({ message: "StartDate parameter required (YYYY-MM-DD)" }); + } + + if (!endDate || typeof endDate !== "string") { + return res.status(400).json({ message: "EndDate parameter required (YYYY-MM-DD)" }); + } + + // Valida formato data + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { + return res.status(400).json({ message: "Invalid date format, use YYYY-MM-DD" }); + } + + // Ottieni info guardia + const guard = await db + .select({ + id: guards.id, + firstName: guards.firstName, + lastName: guards.lastName, + badgeNumber: guards.badgeNumber, + location: guards.location, + }) + .from(guards) + .where(eq(guards.id, guardId)) + .limit(1); + + if (guard.length === 0) { + return res.status(404).json({ message: "Guard not found" }); + } + + // Ottieni tutti i patrol routes per questa guardia nel range di date + const routes = await db + .select({ + id: patrolRoutes.id, + shiftDate: patrolRoutes.shiftDate, + startTime: patrolRoutes.startTime, + endTime: patrolRoutes.endTime, + location: patrolRoutes.location, + status: patrolRoutes.status, + notes: patrolRoutes.notes, + assignedVehicleId: patrolRoutes.assignedVehicleId, + }) + .from(patrolRoutes) + .where( + and( + eq(patrolRoutes.guardId, guardId), + gte(patrolRoutes.shiftDate, startDate), + lte(patrolRoutes.shiftDate, endDate) + ) + ) + .orderBy(patrolRoutes.shiftDate); + + // Per ogni route, ottieni gli stops + const days = await Promise.all( + routes.map(async (route) => { + const stops = await db + .select({ + id: patrolRouteStops.id, + siteId: sites.id, + siteName: sites.name, + siteAddress: sites.address, + latitude: sites.latitude, + longitude: sites.longitude, + sequenceOrder: patrolRouteStops.sequenceOrder, + estimatedArrivalTime: patrolRouteStops.estimatedArrivalTime, + }) + .from(patrolRouteStops) + .innerJoin(sites, eq(patrolRouteStops.siteId, sites.id)) + .where(eq(patrolRouteStops.patrolRouteId, route.id)) + .orderBy(patrolRouteStops.sequenceOrder); + + return { + date: route.shiftDate, + route: { + id: route.id, + startTime: route.startTime, + endTime: route.endTime, + location: route.location, + status: route.status, + notes: route.notes, + hasVehicle: !!route.assignedVehicleId, + vehicleId: route.assignedVehicleId, + }, + stops: stops.map(s => ({ + id: s.id, + siteId: s.siteId, + siteName: s.siteName, + siteAddress: s.siteAddress, + latitude: s.latitude, + longitude: s.longitude, + sequenceOrder: s.sequenceOrder, + estimatedArrivalTime: s.estimatedArrivalTime, + })), + }; + }) + ); + + res.json({ + guard: guard[0], + days, + }); + } catch (error) { + console.error("Error fetching mobile agent planning:", error); + res.status(500).json({ message: "Failed to fetch mobile agent planning" }); + } + }); + + // GET vehicles available for a location app.get("/api/vehicles/available", isAuthenticated, async (req, res) => { try { const { location } = req.query;