diff --git a/client/src/App.tsx b/client/src/App.tsx index 59e8849..8ac0aa0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -21,13 +21,15 @@ 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(); @@ -46,13 +48,15 @@ function Router() { + + + + - - diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index 0856149..561e184 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -31,67 +31,55 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/theme-toggle"; -const dashboardItems = [ +const menuItems = [ { title: "Dashboard", url: "/", icon: Shield, roles: ["admin", "coordinator", "guard", "client"], }, -]; - -const creationItems = [ { - title: "Turni Fissi", + title: "Turni", 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: "Pattuglie Mobile", + title: "Planning Fissi", + url: "/general-planning", + icon: BarChart3, + roles: ["admin", "coordinator"], + }, + { + title: "Planning Mobile", url: "/planning-mobile", icon: Navigation, roles: ["admin", "coordinator"], }, -]; - -const consultationItems = [ { - title: "Planning Agente Fisso", - url: "/planning-view-fixed-agent", - icon: Users, + title: "Planning di Servizio", + url: "/service-planning", + icon: ClipboardList, roles: ["admin", "coordinator"], }, { - title: "Planning Agente Mobile", - url: "/planning-view-mobile-agent", - icon: Navigation, + title: "Gestione Pianificazioni", + url: "/advanced-planning", + icon: ClipboardList, 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", @@ -122,18 +110,12 @@ const registryItems = [ icon: Car, roles: ["admin", "coordinator"], }, -]; - -const reportingItems = [ { title: "Report", url: "/reports", icon: BarChart3, roles: ["admin", "coordinator", "client"], }, -]; - -const systemItems = [ { title: "Notifiche", url: "/notifications", @@ -158,26 +140,8 @@ export function AppSidebar() { const { user } = useAuth(); const [location] = useLocation(); - const filterItems = (items: typeof dashboardItems) => - items.filter((item) => user && item.roles.includes(user.role)); - - const renderMenuItems = (items: typeof dashboardItems) => ( - - {items.map((item) => ( - - - - - {item.title} - - - - ))} - + const filteredItems = menuItems.filter( + (item) => user && item.roles.includes(user.role) ); return ( @@ -193,74 +157,27 @@ export function AppSidebar() { - {/* 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))} - - - )} + + Menu Principale + + + {filteredItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + diff --git a/client/src/pages/planning-view-fixed-agent.tsx b/client/src/pages/planning-view-fixed-agent.tsx deleted file mode 100644 index 0c8bec6..0000000 --- a/client/src/pages/planning-view-fixed-agent.tsx +++ /dev/null @@ -1,273 +0,0 @@ -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 deleted file mode 100644 index 5ff2800..0000000 --- a/client/src/pages/planning-view-mobile-agent.tsx +++ /dev/null @@ -1,275 +0,0 @@ -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 ba7cbf9..3f478d4 100644 --- a/client/src/pages/shifts.tsx +++ b/client/src/pages/shifts.tsx @@ -42,25 +42,6 @@ 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 @@ -582,12 +563,11 @@ export default function Shifts() { {/* Guards List */}

Guardie Disponibili

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

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

#{guard.badgeNumber} - {bookedOnMobile && ( - - Su pattuglia mobile - - )}
{guard.isArmed && ( @@ -640,10 +615,10 @@ export default function Shifts() { size="sm" variant={assigned ? "secondary" : "default"} onClick={() => handleAssignGuard(guard.id)} - disabled={assigned || !canAssign || bookedOnMobile} + disabled={assigned || !canAssign} data-testid={`button-assign-guard-${guard.id}`} > - {assigned ? "Assegnato" : bookedOnMobile ? "Su pattuglia" : canAssign ? "Assegna" : "Non idoneo"} + {assigned ? "Assegnato" : canAssign ? "Assegna" : "Non idoneo"}
); diff --git a/replit.md b/replit.md index e15f60b..467b9da 100644 --- a/replit.md +++ b/replit.md @@ -142,20 +142,6 @@ 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 a1c28ef..e563a53 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -331,291 +331,8 @@ 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 /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 + // Get vehicles available for a location app.get("/api/vehicles/available", isAuthenticated, async (req, res) => { try { const { location } = req.query;