Add mobile patrol routes and distinguish between fixed and mobile guard duties

Introduce new data structures and API endpoints for mobile patrol routes, differentiating them from fixed guard shifts in the service planning interface.

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/rjLU1aT
This commit is contained in:
marco370 2025-10-23 16:57:03 +00:00
parent ab85e8eb03
commit 00ac8c8415
2 changed files with 452 additions and 46 deletions

View File

@ -2,7 +2,7 @@ import { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { format, addWeeks, addDays, startOfWeek } from "date-fns"; import { format, addWeeks, addDays, startOfWeek } from "date-fns";
import { it } from "date-fns/locale"; import { it } from "date-fns/locale";
import { ChevronLeft, ChevronRight, Users, Building2 } from "lucide-react"; import { ChevronLeft, ChevronRight, Users, Building2, Navigation, Shield, Car as CarIcon, MapPin } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, 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";
@ -12,13 +12,15 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
type Location = "roccapiemonte" | "milano" | "roma"; type Location = "roccapiemonte" | "milano" | "roma";
interface ShiftDetail { interface FixedShiftDetail {
shiftId: string; shiftId: string;
date: string; date: string;
from: string; from: string;
to: string; to: string;
siteName: string; siteName: string;
siteAddress: string;
siteId: string; siteId: string;
isArmed: boolean;
vehicle?: { vehicle?: {
licensePlate: string; licensePlate: string;
brand: string; brand: string;
@ -27,14 +29,42 @@ interface ShiftDetail {
hours: number; hours: number;
} }
interface GuardSchedule { interface FixedGuardSchedule {
guardId: string; guardId: string;
guardName: string; guardName: string;
badgeNumber: string; badgeNumber: string;
shifts: ShiftDetail[]; shifts: FixedShiftDetail[];
totalHours: number; totalHours: number;
} }
interface PatrolRoute {
routeId: string;
guardId: string;
shiftDate: string;
startTime: string;
endTime: string;
isArmedRoute: boolean;
vehicle?: {
licensePlate: string;
brand: string;
model: string;
};
stops: {
siteId: string;
siteName: string;
siteAddress: string;
sequenceOrder: number;
}[];
}
interface MobileGuardSchedule {
guardId: string;
guardName: string;
badgeNumber: string;
routes: PatrolRoute[];
totalRoutes: number;
}
interface SiteSchedule { interface SiteSchedule {
siteId: string; siteId: string;
siteName: string; siteName: string;
@ -48,6 +78,7 @@ interface SiteSchedule {
guardName: string; guardName: string;
badgeNumber: string; badgeNumber: string;
hours: number; hours: number;
isArmed: boolean;
}[]; }[];
vehicle?: { vehicle?: {
licensePlate: string; licensePlate: string;
@ -64,20 +95,30 @@ interface SiteSchedule {
export default function ServicePlanning() { export default function ServicePlanning() {
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte"); const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
const [weekStart, setWeekStart] = useState<Date>(startOfWeek(new Date(), { weekStartsOn: 1 })); const [weekStart, setWeekStart] = useState<Date>(startOfWeek(new Date(), { weekStartsOn: 1 }));
const [viewMode, setViewMode] = useState<"guard" | "site">("guard"); const [viewMode, setViewMode] = useState<"guard-fixed" | "guard-mobile" | "site">("guard-fixed");
const weekStartStr = format(weekStart, "yyyy-MM-dd"); const weekStartStr = format(weekStart, "yyyy-MM-dd");
const weekEndStr = format(addDays(weekStart, 6), "yyyy-MM-dd");
// Query per vista Guardie // Query per vista Agenti Fissi
const { data: guardSchedules, isLoading: isLoadingGuards } = useQuery<GuardSchedule[]>({ const { data: fixedGuardSchedules, isLoading: isLoadingFixedGuards } = useQuery<FixedGuardSchedule[]>({
queryKey: ["/api/service-planning/by-guard", weekStartStr, selectedLocation], queryKey: ["/api/service-planning/guards-fixed", weekStartStr, selectedLocation],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`/api/service-planning/by-guard?weekStart=${weekStartStr}&location=${selectedLocation}`); const response = await fetch(`/api/service-planning/guards-fixed?weekStart=${weekStartStr}&location=${selectedLocation}`);
if (!response.ok) throw new Error("Failed to fetch guard schedules"); if (!response.ok) throw new Error("Failed to fetch fixed guard schedules");
return response.json(); return response.json();
}, },
enabled: viewMode === "guard", enabled: viewMode === "guard-fixed",
});
// Query per vista Agenti Mobili
const { data: mobileGuardSchedules, isLoading: isLoadingMobileGuards } = useQuery<MobileGuardSchedule[]>({
queryKey: ["/api/service-planning/guards-mobile", weekStartStr, selectedLocation],
queryFn: async () => {
const response = await fetch(`/api/service-planning/guards-mobile?weekStart=${weekStartStr}&location=${selectedLocation}`);
if (!response.ok) throw new Error("Failed to fetch mobile guard schedules");
return response.json();
},
enabled: viewMode === "guard-mobile",
}); });
// Query per vista Siti // Query per vista Siti
@ -99,9 +140,9 @@ export default function ServicePlanning() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold">Planning di Servizio</h1> <h1 className="text-3xl font-bold">Visione Servizi</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Visualizza orari e dotazioni per guardia o sito Visualizza orari e dotazioni per agente fisso, agente mobile o per sito
</p> </p>
</div> </div>
</div> </div>
@ -145,11 +186,15 @@ export default function ServicePlanning() {
</Card> </Card>
{/* Tabs per vista */} {/* Tabs per vista */}
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard" | "site")}> <Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard-fixed" | "guard-mobile" | "site")}>
<TabsList className="grid w-full max-w-md grid-cols-2"> <TabsList className="grid w-full max-w-2xl grid-cols-3">
<TabsTrigger value="guard" data-testid="tab-guard-view"> <TabsTrigger value="guard-fixed" data-testid="tab-guard-fixed-view">
<Users className="h-4 w-4 mr-2" /> <Users className="h-4 w-4 mr-2" />
Vista Agente Agenti Fissi
</TabsTrigger>
<TabsTrigger value="guard-mobile" data-testid="tab-guard-mobile-view">
<Navigation className="h-4 w-4 mr-2" />
Agenti Mobili
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="site" data-testid="tab-site-view"> <TabsTrigger value="site" data-testid="tab-site-view">
<Building2 className="h-4 w-4 mr-2" /> <Building2 className="h-4 w-4 mr-2" />
@ -157,18 +202,18 @@ export default function ServicePlanning() {
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
{/* Vista Agente */} {/* Vista Agenti Fissi */}
<TabsContent value="guard" className="space-y-4 mt-6"> <TabsContent value="guard-fixed" className="space-y-4 mt-6">
{isLoadingGuards ? ( {isLoadingFixedGuards ? (
<div className="space-y-4"> <div className="space-y-4">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-32 w-full" /> <Skeleton key={i} className="h-32 w-full" />
))} ))}
</div> </div>
) : guardSchedules && guardSchedules.length > 0 ? ( ) : fixedGuardSchedules && fixedGuardSchedules.length > 0 ? (
<div className="grid gap-4"> <div className="grid gap-4">
{guardSchedules.map((guard) => ( {fixedGuardSchedules.map((guard) => (
<Card key={guard.guardId} data-testid={`card-guard-${guard.guardId}`}> <Card key={guard.guardId} data-testid={`card-guard-fixed-${guard.guardId}`}>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg"> <CardTitle className="text-lg">
@ -179,25 +224,40 @@ export default function ServicePlanning() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{guard.shifts.length === 0 ? ( {guard.shifts.length === 0 ? (
<p className="text-sm text-muted-foreground">Nessun turno assegnato</p> <p className="text-sm text-muted-foreground">Nessun turno fisso assegnato</p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{guard.shifts.map((shift) => ( {guard.shifts.map((shift) => (
<div <div
key={shift.shiftId} key={shift.shiftId}
className="flex items-start justify-between p-3 rounded-md bg-muted/50" className="p-3 rounded-md bg-muted/50 space-y-2"
data-testid={`shift-${shift.shiftId}`} data-testid={`shift-${shift.shiftId}`}
> >
<div className="space-y-1"> <div className="flex items-start justify-between">
<div className="font-medium">{shift.siteName}</div> <div className="space-y-1 flex-1">
<div className="text-sm text-muted-foreground"> <div className="font-medium">{shift.siteName}</div>
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} {shift.from} - {shift.to} ({shift.hours}h) <div className="text-sm text-muted-foreground flex items-center gap-1">
</div> <MapPin className="h-3 w-3" />
{shift.vehicle && ( {shift.siteAddress}
<div className="text-xs text-muted-foreground">
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
</div> </div>
)} <div className="text-sm text-muted-foreground">
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} {shift.from} - {shift.to} ({shift.hours}h)
</div>
</div>
<div className="flex flex-col items-end gap-1">
{shift.isArmed && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
{shift.vehicle && (
<Badge variant="outline" className="text-xs">
<CarIcon className="h-3 w-3 mr-1" />
{shift.vehicle.licensePlate}
</Badge>
)}
</div>
</div> </div>
</div> </div>
))} ))}
@ -210,7 +270,101 @@ export default function ServicePlanning() {
) : ( ) : (
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-center text-muted-foreground">Nessuna guardia con turni assegnati</p> <p className="text-center text-muted-foreground">Nessun agente con turni fissi assegnati</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* Vista Agenti Mobili */}
<TabsContent value="guard-mobile" className="space-y-4 mt-6">
{isLoadingMobileGuards ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
) : mobileGuardSchedules && mobileGuardSchedules.length > 0 ? (
<div className="grid gap-4">
{mobileGuardSchedules.map((guard) => (
<Card key={guard.guardId} data-testid={`card-guard-mobile-${guard.guardId}`}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">
{guard.guardName} <Badge variant="outline">{guard.badgeNumber}</Badge>
</CardTitle>
<Badge>{guard.totalRoutes} {guard.totalRoutes === 1 ? 'percorso' : 'percorsi'}</Badge>
</div>
</CardHeader>
<CardContent>
{guard.routes.length === 0 ? (
<p className="text-sm text-muted-foreground">Nessun percorso pattuglia assegnato</p>
) : (
<div className="space-y-4">
{guard.routes.map((route) => (
<div
key={route.routeId}
className="p-3 rounded-md bg-muted/50 space-y-3"
data-testid={`route-${route.routeId}`}
>
<div className="flex items-center justify-between">
<div className="font-medium">
{format(new Date(route.shiftDate), "EEEE d MMM yyyy", { locale: it })}
</div>
<div className="text-sm text-muted-foreground">
{route.startTime} - {route.endTime}
</div>
</div>
<div className="flex gap-2">
{route.isArmedRoute && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
{route.vehicle && (
<Badge variant="outline" className="text-xs">
<CarIcon className="h-3 w-3 mr-1" />
{route.vehicle.licensePlate}
</Badge>
)}
</div>
<div className="space-y-2">
<div className="text-sm font-medium flex items-center gap-1">
<Navigation className="h-4 w-4" />
Percorso ({route.stops.length} {route.stops.length === 1 ? 'tappa' : 'tappe'}):
</div>
<div className="space-y-1 pl-5">
{route.stops.map((stop) => (
<div key={stop.siteId} className="text-sm text-muted-foreground flex items-start gap-2">
<Badge variant="secondary" className="text-xs">
{stop.sequenceOrder}
</Badge>
<div className="flex-1">
<div className="font-medium text-foreground">{stop.siteName}</div>
<div className="text-xs flex items-center gap-1">
<MapPin className="h-3 w-3" />
{stop.siteAddress}
</div>
</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Nessun agente con percorsi pattuglia assegnati</p>
</CardContent> </CardContent>
</Card> </Card>
)} )}
@ -252,20 +406,29 @@ export default function ServicePlanning() {
<div className="font-medium"> <div className="font-medium">
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} {shift.from} - {shift.to} {format(new Date(shift.date), "EEEE d MMM", { locale: it })} {shift.from} - {shift.to}
</div> </div>
<Badge variant="secondary">{shift.totalGuards} {shift.totalGuards === 1 ? "guardia" : "guardie"}</Badge> <div className="flex gap-1">
<Badge variant="secondary">{shift.totalGuards} {shift.totalGuards === 1 ? "guardia" : "guardie"}</Badge>
{shift.vehicle && (
<Badge variant="outline" className="text-xs">
<CarIcon className="h-3 w-3 mr-1" />
{shift.vehicle.licensePlate}
</Badge>
)}
</div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{shift.guards.map((guard, idx) => ( {shift.guards.map((guard, idx) => (
<div key={idx} className="text-sm text-muted-foreground"> <div key={idx} className="text-sm text-muted-foreground flex items-center justify-between">
👤 {guard.guardName} ({guard.badgeNumber}) - {guard.hours}h <span>{guard.guardName} ({guard.badgeNumber}) - {guard.hours}h</span>
{guard.isArmed && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
</div> </div>
))} ))}
</div> </div>
{shift.vehicle && (
<div className="text-xs text-muted-foreground">
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
</div>
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -679,6 +679,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
) )
); );
// Ottieni patrol routes del giorno SOLO della sede selezionata
const dayPatrolRoutes = await db
.select()
.from(patrolRoutes)
.where(
and(
eq(patrolRoutes.shiftDate, dateStr),
eq(patrolRoutes.location, location as any)
)
);
// Calcola disponibilità agenti con report CCNL // Calcola disponibilità agenti con report CCNL
const guardsWithAvailability = await Promise.all( const guardsWithAvailability = await Promise.all(
locationGuards.map(async (guard) => { locationGuards.map(async (guard) => {
@ -686,6 +697,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
(assignment: any) => assignment.shift_assignments.guardId === guard.id (assignment: any) => assignment.shift_assignments.guardId === guard.id
); );
const assignedPatrolRoute = dayPatrolRoutes.find(
(route: any) => route.guardId === guard.id
);
// Calcola report disponibilità CCNL // Calcola report disponibilità CCNL
const availabilityReport = await getGuardAvailabilityReport( const availabilityReport = await getGuardAvailabilityReport(
guard.id, guard.id,
@ -695,13 +710,18 @@ export async function registerRoutes(app: Express): Promise<Server> {
return { return {
...guard, ...guard,
isAvailable: !assignedShift, isAvailable: !assignedShift && !assignedPatrolRoute,
assignedShift: assignedShift ? { assignedShift: assignedShift ? {
id: assignedShift.shifts.id, id: assignedShift.shifts.id,
startTime: assignedShift.shifts.startTime, startTime: assignedShift.shifts.startTime,
endTime: assignedShift.shifts.endTime, endTime: assignedShift.shifts.endTime,
siteId: assignedShift.shifts.siteId siteId: assignedShift.shifts.siteId
} : null, } : null,
assignedPatrolRoute: assignedPatrolRoute ? {
id: assignedPatrolRoute.id,
startTime: assignedPatrolRoute.startTime,
endTime: assignedPatrolRoute.endTime,
} : null,
availability: { availability: {
weeklyHours: availabilityReport.weeklyHours.current, weeklyHours: availabilityReport.weeklyHours.current,
remainingWeeklyHours: availabilityReport.remainingWeeklyHours, remainingWeeklyHours: availabilityReport.remainingWeeklyHours,
@ -1455,7 +1475,229 @@ export async function registerRoutes(app: Express): Promise<Server> {
// ============= SERVICE PLANNING ROUTES ============= // ============= SERVICE PLANNING ROUTES =============
// Vista per Guardia - mostra orari e dotazioni per ogni guardia // Vista per Agente Fisso - mostra orari e dotazioni operative per turni fissi
app.get("/api/service-planning/guards-fixed", isAuthenticated, async (req, res) => {
try {
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
const normalizedWeekStart = rawWeekStart.split("/")[0];
const parsedWeekStart = parseISO(normalizedWeekStart);
if (!isValid(parsedWeekStart)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
}
const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd");
const location = req.query.location as string || "roccapiemonte";
const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd");
const weekStartTimestamp = new Date(weekStartDate);
weekStartTimestamp.setHours(0, 0, 0, 0);
const weekEndTimestamp = new Date(weekEndDate);
weekEndTimestamp.setHours(23, 59, 59, 999);
// Ottieni tutte le guardie della sede
const allGuards = await db
.select()
.from(guards)
.where(eq(guards.location, location as any))
.orderBy(guards.fullName);
// Ottieni tutti i turni della settimana
const allWeekShifts = await db
.select({
shift: shifts,
site: sites,
vehicle: vehicles,
serviceType: serviceTypes,
})
.from(shifts)
.innerJoin(sites, eq(shifts.siteId, sites.id))
.leftJoin(vehicles, eq(shifts.vehicleId, vehicles.id))
.leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
.where(
and(
gte(shifts.startTime, weekStartTimestamp),
lte(shifts.startTime, weekEndTimestamp),
ne(shifts.status, "cancelled"),
eq(sites.location, location as any)
)
);
// Filtra solo turni FISSI in base alla classificazione del serviceType
const weekShifts = allWeekShifts.filter((s: any) =>
s.serviceType && s.serviceType.classification === "fisso"
);
// Ottieni tutte le assegnazioni per i turni della settimana
const shiftIds = weekShifts.map((s: any) => s.shift.id);
const assignments = shiftIds.length > 0 ? await db
.select({
assignment: shiftAssignments,
guard: guards,
})
.from(shiftAssignments)
.innerJoin(guards, eq(shiftAssignments.guardId, guards.id))
.where(
sql`${shiftAssignments.shiftId} IN (${sql.join(shiftIds.map((id: string) => sql`${id}`), sql`, `)})`
) : [];
// Costruisci dati per ogni guardia
const guardSchedules = allGuards.map((guard: any) => {
// Trova assegnazioni della guardia
const guardAssignments = assignments.filter((a: any) => a.guard.id === guard.id);
// Costruisci lista turni con dettagli
const shifts = guardAssignments.map((a: any) => {
const shiftData = weekShifts.find((s: any) => s.shift.id === a.assignment.shiftId);
if (!shiftData) return null;
const plannedStart = new Date(a.assignment.plannedStartTime);
const plannedEnd = new Date(a.assignment.plannedEndTime);
const minutes = differenceInMinutes(plannedEnd, plannedStart);
const hours = Math.round((minutes / 60) * 10) / 10;
return {
shiftId: shiftData.shift.id,
date: format(plannedStart, "yyyy-MM-dd"),
from: format(plannedStart, "HH:mm"),
to: format(plannedEnd, "HH:mm"),
siteName: shiftData.site.name,
siteAddress: shiftData.site.address,
siteId: shiftData.site.id,
isArmed: guard.isArmed,
vehicle: shiftData.vehicle ? {
licensePlate: shiftData.vehicle.licensePlate,
brand: shiftData.vehicle.brand,
model: shiftData.vehicle.model,
} : undefined,
hours,
};
}).filter(Boolean);
const totalHours = Math.round(shifts.reduce((sum: number, s: any) => sum + s.hours, 0) * 10) / 10;
return {
guardId: guard.id,
guardName: guard.fullName,
badgeNumber: guard.badgeNumber,
shifts,
totalHours,
};
});
// Filtra solo guardie con turni assegnati
const guardsWithShifts = guardSchedules.filter((g: any) => g.shifts.length > 0);
res.json(guardsWithShifts);
} catch (error) {
console.error("Error fetching fixed guard schedules:", error);
res.status(500).json({ message: "Failed to fetch fixed guard schedules", error: String(error) });
}
});
// Vista per Agente Mobile - mostra percorsi pattuglia con siti e indirizzi
app.get("/api/service-planning/guards-mobile", isAuthenticated, async (req, res) => {
try {
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
const normalizedWeekStart = rawWeekStart.split("/")[0];
const parsedWeekStart = parseISO(normalizedWeekStart);
if (!isValid(parsedWeekStart)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
}
const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd");
const location = req.query.location as string || "roccapiemonte";
const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd");
// Ottieni tutte le guardie della sede
const allGuards = await db
.select()
.from(guards)
.where(eq(guards.location, location as any))
.orderBy(guards.fullName);
// Ottieni tutte le patrol routes della settimana per la sede
const weekRoutes = await db
.select({
route: patrolRoutes,
guard: guards,
vehicle: vehicles,
})
.from(patrolRoutes)
.innerJoin(guards, eq(patrolRoutes.guardId, guards.id))
.leftJoin(vehicles, eq(patrolRoutes.vehicleId, vehicles.id))
.where(
and(
gte(sql`${patrolRoutes.shiftDate}::date`, weekStartDate),
lte(sql`${patrolRoutes.shiftDate}::date`, weekEndDate),
eq(patrolRoutes.location, location as any)
)
);
// Per ogni route, ottieni le stops
const routesWithStops = await Promise.all(
weekRoutes.map(async (routeData: any) => {
const stops = await db
.select({
stop: patrolRouteStops,
site: sites,
})
.from(patrolRouteStops)
.innerJoin(sites, eq(patrolRouteStops.siteId, sites.id))
.where(eq(patrolRouteStops.routeId, routeData.route.id))
.orderBy(asc(patrolRouteStops.sequenceOrder));
return {
routeId: routeData.route.id,
guardId: routeData.guard.id,
shiftDate: routeData.route.shiftDate,
startTime: routeData.route.startTime,
endTime: routeData.route.endTime,
isArmedRoute: routeData.route.isArmedRoute,
vehicle: routeData.vehicle ? {
licensePlate: routeData.vehicle.licensePlate,
brand: routeData.vehicle.brand,
model: routeData.vehicle.model,
} : undefined,
stops: stops.map((s: any) => ({
siteId: s.site.id,
siteName: s.site.name,
siteAddress: s.site.address,
sequenceOrder: s.stop.sequenceOrder,
})),
};
})
);
// Costruisci dati per ogni guardia
const guardSchedules = allGuards.map((guard: any) => {
// Trova routes della guardia
const guardRoutes = routesWithStops.filter((r: any) => r.guardId === guard.id);
const totalRoutes = guardRoutes.length;
return {
guardId: guard.id,
guardName: guard.fullName,
badgeNumber: guard.badgeNumber,
routes: guardRoutes,
totalRoutes,
};
});
// Filtra solo guardie con routes assegnate
const guardsWithRoutes = guardSchedules.filter((g: any) => g.routes.length > 0);
res.json(guardsWithRoutes);
} catch (error) {
console.error("Error fetching mobile guard schedules:", error);
res.status(500).json({ message: "Failed to fetch mobile guard schedules", error: String(error) });
}
});
// Vista per Guardia - mostra orari e dotazioni per ogni guardia (LEGACY - manteniamo per compatibilità)
app.get("/api/service-planning/by-guard", isAuthenticated, async (req, res) => { app.get("/api/service-planning/by-guard", isAuthenticated, async (req, res) => {
try { try {
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd"); const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
@ -1651,6 +1893,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
guardName: a.guard.fullName, guardName: a.guard.fullName,
badgeNumber: a.guard.badgeNumber, badgeNumber: a.guard.badgeNumber,
hours, hours,
isArmed: a.guard.isArmed,
}; };
}); });