Compare commits
No commits in common. "bb50965ebaa9d4ab0c2032483e550fadb59ebc2f" and "1cdae8c4b315c97e0a34338fac027678655fb8e2" have entirely different histories.
bb50965eba
...
1cdae8c4b3
4
.replit
4
.replit
@ -19,6 +19,10 @@ externalPort = 80
|
||||
localPort = 33035
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 39567
|
||||
externalPort = 6000
|
||||
|
||||
[[ports]]
|
||||
localPort = 41295
|
||||
externalPort = 5173
|
||||
|
||||
@ -241,16 +241,9 @@ export default function PlanningMobile() {
|
||||
|
||||
// Mutation per salvare patrol route
|
||||
const savePatrolRouteMutation = useMutation({
|
||||
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
|
||||
mutationFn: async (data: any) => {
|
||||
const response = await apiRequest("POST", "/api/patrol-routes", data);
|
||||
return response.json();
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@ -302,15 +295,7 @@ export default function PlanningMobile() {
|
||||
})),
|
||||
};
|
||||
|
||||
// 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,
|
||||
});
|
||||
savePatrolRouteMutation.mutate(patrolRouteData);
|
||||
};
|
||||
|
||||
// Carica patrol route esistente quando si seleziona una guardia
|
||||
|
||||
@ -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, Navigation, Shield, Car as CarIcon, MapPin } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, Users, Building2 } 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,15 +12,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
type Location = "roccapiemonte" | "milano" | "roma";
|
||||
|
||||
interface FixedShiftDetail {
|
||||
interface ShiftDetail {
|
||||
shiftId: string;
|
||||
date: string;
|
||||
from: string;
|
||||
to: string;
|
||||
siteName: string;
|
||||
siteAddress: string;
|
||||
siteId: string;
|
||||
isArmed: boolean;
|
||||
vehicle?: {
|
||||
licensePlate: string;
|
||||
brand: string;
|
||||
@ -29,42 +27,14 @@ interface FixedShiftDetail {
|
||||
hours: number;
|
||||
}
|
||||
|
||||
interface FixedGuardSchedule {
|
||||
interface GuardSchedule {
|
||||
guardId: string;
|
||||
guardName: string;
|
||||
badgeNumber: string;
|
||||
shifts: FixedShiftDetail[];
|
||||
shifts: ShiftDetail[];
|
||||
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;
|
||||
@ -78,7 +48,6 @@ interface SiteSchedule {
|
||||
guardName: string;
|
||||
badgeNumber: string;
|
||||
hours: number;
|
||||
isArmed: boolean;
|
||||
}[];
|
||||
vehicle?: {
|
||||
licensePlate: string;
|
||||
@ -95,30 +64,20 @@ 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-fixed" | "guard-mobile" | "site">("guard-fixed");
|
||||
const [viewMode, setViewMode] = useState<"guard" | "site">("guard");
|
||||
|
||||
const weekStartStr = format(weekStart, "yyyy-MM-dd");
|
||||
const weekEndStr = format(addDays(weekStart, 6), "yyyy-MM-dd");
|
||||
|
||||
// Query per vista Agenti Fissi
|
||||
const { data: fixedGuardSchedules, isLoading: isLoadingFixedGuards } = useQuery<FixedGuardSchedule[]>({
|
||||
queryKey: ["/api/service-planning/guards-fixed", weekStartStr, selectedLocation],
|
||||
// Query per vista Guardie
|
||||
const { data: guardSchedules, isLoading: isLoadingGuards } = useQuery<GuardSchedule[]>({
|
||||
queryKey: ["/api/service-planning/by-guard", weekStartStr, selectedLocation],
|
||||
queryFn: async () => {
|
||||
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");
|
||||
const response = await fetch(`/api/service-planning/by-guard?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch guard schedules");
|
||||
return response.json();
|
||||
},
|
||||
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",
|
||||
enabled: viewMode === "guard",
|
||||
});
|
||||
|
||||
// Query per vista Siti
|
||||
@ -140,9 +99,9 @@ export default function ServicePlanning() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Visione Servizi</h1>
|
||||
<h1 className="text-3xl font-bold">Planning di Servizio</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Visualizza orari e dotazioni per agente fisso, agente mobile o per sito
|
||||
Visualizza orari e dotazioni per guardia o sito
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -186,15 +145,11 @@ export default function ServicePlanning() {
|
||||
</Card>
|
||||
|
||||
{/* Tabs per vista */}
|
||||
<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">
|
||||
<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">
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
Agenti Fissi
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="guard-mobile" data-testid="tab-guard-mobile-view">
|
||||
<Navigation className="h-4 w-4 mr-2" />
|
||||
Agenti Mobili
|
||||
Vista Agente
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="site" data-testid="tab-site-view">
|
||||
<Building2 className="h-4 w-4 mr-2" />
|
||||
@ -202,18 +157,18 @@ export default function ServicePlanning() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Vista Agenti Fissi */}
|
||||
<TabsContent value="guard-fixed" className="space-y-4 mt-6">
|
||||
{isLoadingFixedGuards ? (
|
||||
{/* Vista Agente */}
|
||||
<TabsContent value="guard" className="space-y-4 mt-6">
|
||||
{isLoadingGuards ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-32 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : fixedGuardSchedules && fixedGuardSchedules.length > 0 ? (
|
||||
) : guardSchedules && guardSchedules.length > 0 ? (
|
||||
<div className="grid gap-4">
|
||||
{fixedGuardSchedules.map((guard) => (
|
||||
<Card key={guard.guardId} data-testid={`card-guard-fixed-${guard.guardId}`}>
|
||||
{guardSchedules.map((guard) => (
|
||||
<Card key={guard.guardId} data-testid={`card-guard-${guard.guardId}`}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">
|
||||
@ -224,40 +179,25 @@ export default function ServicePlanning() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{guard.shifts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Nessun turno fisso assegnato</p>
|
||||
<p className="text-sm text-muted-foreground">Nessun turno assegnato</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{guard.shifts.map((shift) => (
|
||||
<div
|
||||
key={shift.shiftId}
|
||||
className="p-3 rounded-md bg-muted/50 space-y-2"
|
||||
className="flex items-start justify-between p-3 rounded-md bg-muted/50"
|
||||
data-testid={`shift-${shift.shiftId}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1 flex-1">
|
||||
<div className="space-y-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 className="text-xs text-muted-foreground">
|
||||
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -270,101 +210,7 @@ export default function ServicePlanning() {
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<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>
|
||||
<p className="text-center text-muted-foreground">Nessuna guardia con turni assegnati</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@ -406,29 +252,20 @@ 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 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 key={idx} className="text-sm text-muted-foreground">
|
||||
👤 {guard.guardName} ({guard.badgeNumber}) - {guard.hours}h
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{shift.vehicle && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
BIN
database-backups/vigilanzaturni_v1.0.39_20251023_101800.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.39_20251023_101800.sql.gz
Normal file
Binary file not shown.
Binary file not shown.
247
server/routes.ts
247
server/routes.ts
@ -679,17 +679,6 @@ 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) => {
|
||||
@ -697,10 +686,6 @@ 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,
|
||||
@ -710,18 +695,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
return {
|
||||
...guard,
|
||||
isAvailable: !assignedShift && !assignedPatrolRoute,
|
||||
isAvailable: !assignedShift,
|
||||
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,
|
||||
@ -1475,229 +1455,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
// ============= SERVICE PLANNING ROUTES =============
|
||||
|
||||
// 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à)
|
||||
// Vista per Guardia - mostra orari e dotazioni per ogni guardia
|
||||
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");
|
||||
@ -1893,7 +1651,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
guardName: a.guard.fullName,
|
||||
badgeNumber: a.guard.badgeNumber,
|
||||
hours,
|
||||
isArmed: a.guard.isArmed,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
||||
{
|
||||
"version": "1.0.49",
|
||||
"lastUpdate": "2025-10-23T17:04:52.044Z",
|
||||
"version": "1.0.48",
|
||||
"lastUpdate": "2025-10-23T16:03:23.268Z",
|
||||
"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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user