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:
parent
ab85e8eb03
commit
00ac8c8415
@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format, addWeeks, addDays, startOfWeek } from "date-fns";
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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";
|
||||
|
||||
interface ShiftDetail {
|
||||
interface FixedShiftDetail {
|
||||
shiftId: string;
|
||||
date: string;
|
||||
from: string;
|
||||
to: string;
|
||||
siteName: string;
|
||||
siteAddress: string;
|
||||
siteId: string;
|
||||
isArmed: boolean;
|
||||
vehicle?: {
|
||||
licensePlate: string;
|
||||
brand: string;
|
||||
@ -27,14 +29,42 @@ interface ShiftDetail {
|
||||
hours: number;
|
||||
}
|
||||
|
||||
interface GuardSchedule {
|
||||
interface FixedGuardSchedule {
|
||||
guardId: string;
|
||||
guardName: string;
|
||||
badgeNumber: string;
|
||||
shifts: ShiftDetail[];
|
||||
shifts: FixedShiftDetail[];
|
||||
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 {
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
@ -48,6 +78,7 @@ interface SiteSchedule {
|
||||
guardName: string;
|
||||
badgeNumber: string;
|
||||
hours: number;
|
||||
isArmed: boolean;
|
||||
}[];
|
||||
vehicle?: {
|
||||
licensePlate: string;
|
||||
@ -64,20 +95,30 @@ interface SiteSchedule {
|
||||
export default function ServicePlanning() {
|
||||
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
|
||||
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 weekEndStr = format(addDays(weekStart, 6), "yyyy-MM-dd");
|
||||
|
||||
// Query per vista Guardie
|
||||
const { data: guardSchedules, isLoading: isLoadingGuards } = useQuery<GuardSchedule[]>({
|
||||
queryKey: ["/api/service-planning/by-guard", weekStartStr, selectedLocation],
|
||||
// Query per vista Agenti Fissi
|
||||
const { data: fixedGuardSchedules, isLoading: isLoadingFixedGuards } = useQuery<FixedGuardSchedule[]>({
|
||||
queryKey: ["/api/service-planning/guards-fixed", weekStartStr, selectedLocation],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/service-planning/by-guard?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch guard schedules");
|
||||
const response = await fetch(`/api/service-planning/guards-fixed?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch fixed guard schedules");
|
||||
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
|
||||
@ -99,9 +140,9 @@ export default function ServicePlanning() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
Visualizza orari e dotazioni per guardia o sito
|
||||
Visualizza orari e dotazioni per agente fisso, agente mobile o per sito
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -145,11 +186,15 @@ export default function ServicePlanning() {
|
||||
</Card>
|
||||
|
||||
{/* Tabs per vista */}
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard" | "site")}>
|
||||
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||
<TabsTrigger value="guard" data-testid="tab-guard-view">
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard-fixed" | "guard-mobile" | "site")}>
|
||||
<TabsList className="grid w-full max-w-2xl grid-cols-3">
|
||||
<TabsTrigger value="guard-fixed" data-testid="tab-guard-fixed-view">
|
||||
<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 value="site" data-testid="tab-site-view">
|
||||
<Building2 className="h-4 w-4 mr-2" />
|
||||
@ -157,18 +202,18 @@ export default function ServicePlanning() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Vista Agente */}
|
||||
<TabsContent value="guard" className="space-y-4 mt-6">
|
||||
{isLoadingGuards ? (
|
||||
{/* Vista Agenti Fissi */}
|
||||
<TabsContent value="guard-fixed" className="space-y-4 mt-6">
|
||||
{isLoadingFixedGuards ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-32 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : guardSchedules && guardSchedules.length > 0 ? (
|
||||
) : fixedGuardSchedules && fixedGuardSchedules.length > 0 ? (
|
||||
<div className="grid gap-4">
|
||||
{guardSchedules.map((guard) => (
|
||||
<Card key={guard.guardId} data-testid={`card-guard-${guard.guardId}`}>
|
||||
{fixedGuardSchedules.map((guard) => (
|
||||
<Card key={guard.guardId} data-testid={`card-guard-fixed-${guard.guardId}`}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">
|
||||
@ -179,25 +224,40 @@ export default function ServicePlanning() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{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">
|
||||
{guard.shifts.map((shift) => (
|
||||
<div
|
||||
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}`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1 flex-1">
|
||||
<div className="font-medium">{shift.siteName}</div>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3" />
|
||||
{shift.siteAddress}
|
||||
</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>
|
||||
{shift.vehicle && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||
</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>
|
||||
))}
|
||||
@ -210,7 +270,101 @@ export default function ServicePlanning() {
|
||||
) : (
|
||||
<Card>
|
||||
<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>
|
||||
</Card>
|
||||
)}
|
||||
@ -252,20 +406,29 @@ export default function ServicePlanning() {
|
||||
<div className="font-medium">
|
||||
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} • {shift.from} - {shift.to}
|
||||
</div>
|
||||
<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 className="space-y-1">
|
||||
{shift.guards.map((guard, idx) => (
|
||||
<div key={idx} className="text-sm text-muted-foreground">
|
||||
👤 {guard.guardName} ({guard.badgeNumber}) - {guard.hours}h
|
||||
<div key={idx} className="text-sm text-muted-foreground flex items-center justify-between">
|
||||
<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>
|
||||
{shift.vehicle && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
247
server/routes.ts
247
server/routes.ts
@ -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
|
||||
const guardsWithAvailability = await Promise.all(
|
||||
locationGuards.map(async (guard) => {
|
||||
@ -686,6 +697,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
(assignment: any) => assignment.shift_assignments.guardId === guard.id
|
||||
);
|
||||
|
||||
const assignedPatrolRoute = dayPatrolRoutes.find(
|
||||
(route: any) => route.guardId === guard.id
|
||||
);
|
||||
|
||||
// Calcola report disponibilità CCNL
|
||||
const availabilityReport = await getGuardAvailabilityReport(
|
||||
guard.id,
|
||||
@ -695,13 +710,18 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
return {
|
||||
...guard,
|
||||
isAvailable: !assignedShift,
|
||||
isAvailable: !assignedShift && !assignedPatrolRoute,
|
||||
assignedShift: assignedShift ? {
|
||||
id: assignedShift.shifts.id,
|
||||
startTime: assignedShift.shifts.startTime,
|
||||
endTime: assignedShift.shifts.endTime,
|
||||
siteId: assignedShift.shifts.siteId
|
||||
} : null,
|
||||
assignedPatrolRoute: assignedPatrolRoute ? {
|
||||
id: assignedPatrolRoute.id,
|
||||
startTime: assignedPatrolRoute.startTime,
|
||||
endTime: assignedPatrolRoute.endTime,
|
||||
} : null,
|
||||
availability: {
|
||||
weeklyHours: availabilityReport.weeklyHours.current,
|
||||
remainingWeeklyHours: availabilityReport.remainingWeeklyHours,
|
||||
@ -1455,7 +1475,229 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
// ============= 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) => {
|
||||
try {
|
||||
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,
|
||||
badgeNumber: a.guard.badgeNumber,
|
||||
hours,
|
||||
isArmed: a.guard.isArmed,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user