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;