Compare commits

...

7 Commits

Author SHA1 Message Date
Marco Lanzara
bb50965eba 🚀 Release v1.0.49
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.49_20251023_170434.sql.gz
- Data: 2025-10-23 17:04:52
2025-10-23 17:04:52 +00:00
marco370
cf0c905d0f Add functionality to manage service requests with attachments
Add new API endpoint and controller for managing service requests, including file uploads via multipart/form-data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e0b5b11c-5b75-4389-8ea9-5f3cd9332f88
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e0b5b11c-5b75-4389-8ea9-5f3cd9332f88/rjLU1aT
2025-10-23 16:57:33 +00:00
marco370
00ac8c8415 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
2025-10-23 16:57:03 +00:00
marco370
ab85e8eb03 Restored to '4a2b5fab66e760175f7609180824ca0ac4f08d5a'
Replit-Restored-To: 4a2b5fab66
2025-10-23 16:38:19 +00:00
marco370
1c183a18ec Saved your changes before rolling back
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: a0a13201-ca6f-49fe-8e12-193e1d995c28
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-23 16:38:16 +00:00
marco370
e0504f0a13 Add planning consultation views and reorganize sidebar navigation
Introduce new planning consultation pages for fixed and mobile agents, refactor sidebar navigation into logical groups, and enhance shift assignment logic by preventing double-booking of guards.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/kDVJJUd
2025-10-23 16:34:28 +00:00
marco370
4a2b5fab66 Update patrol route saving to handle existing routes correctly
Modify the `savePatrolRouteMutation` in `planning-mobile.tsx` to use PUT for updating existing patrol routes and POST for creating new ones, addressing a 400 error when modifying patrol sequences.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/kDVJJUd
2025-10-23 16:09:30 +00:00
7 changed files with 479 additions and 56 deletions

View File

@ -19,10 +19,6 @@ externalPort = 80
localPort = 33035
externalPort = 3001
[[ports]]
localPort = 39567
externalPort = 6000
[[ports]]
localPort = 41295
externalPort = 5173

View File

@ -241,9 +241,16 @@ export default function PlanningMobile() {
// Mutation per salvare patrol route
const savePatrolRouteMutation = useMutation({
mutationFn: async (data: any) => {
const response = await apiRequest("POST", "/api/patrol-routes", data);
return response.json();
mutationFn: async ({ data, existingRouteId }: { data: any; existingRouteId?: string }) => {
if (existingRouteId) {
// UPDATE: usa PUT se esiste già un patrol route
const response = await apiRequest("PUT", `/api/patrol-routes/${existingRouteId}`, data);
return response.json();
} else {
// CREATE: usa POST se è un nuovo patrol route
const response = await apiRequest("POST", "/api/patrol-routes", data);
return response.json();
}
},
onSuccess: () => {
toast({
@ -295,7 +302,15 @@ export default function PlanningMobile() {
})),
};
savePatrolRouteMutation.mutate(patrolRouteData);
// Controlla se esiste già un patrol route per questa guardia/data
const existingRoute = existingPatrolRoutes?.find(
(route: any) => route.guardId === selectedGuard.id
);
savePatrolRouteMutation.mutate({
data: patrolRouteData,
existingRouteId: existingRoute?.id,
});
};
// Carica patrol route esistente quando si seleziona una guardia

View File

@ -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="font-medium">{shift.siteName}</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 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>
</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>
<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 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>

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
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,
};
});

View File

@ -1,7 +1,13 @@
{
"version": "1.0.48",
"lastUpdate": "2025-10-23T16:03:23.268Z",
"version": "1.0.49",
"lastUpdate": "2025-10-23T17:04:52.044Z",
"changelog": [
{
"version": "1.0.49",
"date": "2025-10-23",
"type": "patch",
"description": "Deployment automatico v1.0.49"
},
{
"version": "1.0.48",
"date": "2025-10-23",