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
|
localPort = 33035
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 39567
|
||||||
|
externalPort = 6000
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41295
|
localPort = 41295
|
||||||
externalPort = 5173
|
externalPort = 5173
|
||||||
|
|||||||
@ -241,16 +241,9 @@ export default function PlanningMobile() {
|
|||||||
|
|
||||||
// Mutation per salvare patrol route
|
// Mutation per salvare patrol route
|
||||||
const savePatrolRouteMutation = useMutation({
|
const savePatrolRouteMutation = useMutation({
|
||||||
mutationFn: async ({ data, existingRouteId }: { data: any; existingRouteId?: string }) => {
|
mutationFn: async (data: any) => {
|
||||||
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);
|
const response = await apiRequest("POST", "/api/patrol-routes", data);
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@ -302,15 +295,7 @@ export default function PlanningMobile() {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Controlla se esiste già un patrol route per questa guardia/data
|
savePatrolRouteMutation.mutate(patrolRouteData);
|
||||||
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
|
// Carica patrol route esistente quando si seleziona una guardia
|
||||||
|
|||||||
@ -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, Navigation, Shield, Car as CarIcon, MapPin } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Users, Building2 } 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,15 +12,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
|
|
||||||
type Location = "roccapiemonte" | "milano" | "roma";
|
type Location = "roccapiemonte" | "milano" | "roma";
|
||||||
|
|
||||||
interface FixedShiftDetail {
|
interface ShiftDetail {
|
||||||
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;
|
||||||
@ -29,42 +27,14 @@ interface FixedShiftDetail {
|
|||||||
hours: number;
|
hours: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FixedGuardSchedule {
|
interface GuardSchedule {
|
||||||
guardId: string;
|
guardId: string;
|
||||||
guardName: string;
|
guardName: string;
|
||||||
badgeNumber: string;
|
badgeNumber: string;
|
||||||
shifts: FixedShiftDetail[];
|
shifts: ShiftDetail[];
|
||||||
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;
|
||||||
@ -78,7 +48,6 @@ interface SiteSchedule {
|
|||||||
guardName: string;
|
guardName: string;
|
||||||
badgeNumber: string;
|
badgeNumber: string;
|
||||||
hours: number;
|
hours: number;
|
||||||
isArmed: boolean;
|
|
||||||
}[];
|
}[];
|
||||||
vehicle?: {
|
vehicle?: {
|
||||||
licensePlate: string;
|
licensePlate: string;
|
||||||
@ -95,30 +64,20 @@ 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-fixed" | "guard-mobile" | "site">("guard-fixed");
|
const [viewMode, setViewMode] = useState<"guard" | "site">("guard");
|
||||||
|
|
||||||
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 Agenti Fissi
|
// Query per vista Guardie
|
||||||
const { data: fixedGuardSchedules, isLoading: isLoadingFixedGuards } = useQuery<FixedGuardSchedule[]>({
|
const { data: guardSchedules, isLoading: isLoadingGuards } = useQuery<GuardSchedule[]>({
|
||||||
queryKey: ["/api/service-planning/guards-fixed", weekStartStr, selectedLocation],
|
queryKey: ["/api/service-planning/by-guard", weekStartStr, selectedLocation],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await fetch(`/api/service-planning/guards-fixed?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
const response = await fetch(`/api/service-planning/by-guard?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
||||||
if (!response.ok) throw new Error("Failed to fetch fixed guard schedules");
|
if (!response.ok) throw new Error("Failed to fetch guard schedules");
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
enabled: viewMode === "guard-fixed",
|
enabled: viewMode === "guard",
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
||||||
@ -140,9 +99,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">Visione Servizi</h1>
|
<h1 className="text-3xl font-bold">Planning di Servizio</h1>
|
||||||
<p className="text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,15 +145,11 @@ export default function ServicePlanning() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Tabs per vista */}
|
{/* Tabs per vista */}
|
||||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard-fixed" | "guard-mobile" | "site")}>
|
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard" | "site")}>
|
||||||
<TabsList className="grid w-full max-w-2xl grid-cols-3">
|
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||||
<TabsTrigger value="guard-fixed" data-testid="tab-guard-fixed-view">
|
<TabsTrigger value="guard" data-testid="tab-guard-view">
|
||||||
<Users className="h-4 w-4 mr-2" />
|
<Users className="h-4 w-4 mr-2" />
|
||||||
Agenti Fissi
|
Vista Agente
|
||||||
</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" />
|
||||||
@ -202,18 +157,18 @@ export default function ServicePlanning() {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Vista Agenti Fissi */}
|
{/* Vista Agente */}
|
||||||
<TabsContent value="guard-fixed" className="space-y-4 mt-6">
|
<TabsContent value="guard" className="space-y-4 mt-6">
|
||||||
{isLoadingFixedGuards ? (
|
{isLoadingGuards ? (
|
||||||
<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>
|
||||||
) : fixedGuardSchedules && fixedGuardSchedules.length > 0 ? (
|
) : guardSchedules && guardSchedules.length > 0 ? (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{fixedGuardSchedules.map((guard) => (
|
{guardSchedules.map((guard) => (
|
||||||
<Card key={guard.guardId} data-testid={`card-guard-fixed-${guard.guardId}`}>
|
<Card key={guard.guardId} data-testid={`card-guard-${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">
|
||||||
@ -224,40 +179,25 @@ 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 fisso assegnato</p>
|
<p className="text-sm text-muted-foreground">Nessun turno 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="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}`}
|
data-testid={`shift-${shift.shiftId}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="space-y-1">
|
||||||
<div className="space-y-1 flex-1">
|
|
||||||
<div className="font-medium">{shift.siteName}</div>
|
<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">
|
<div className="text-sm text-muted-foreground">
|
||||||
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} • {shift.from} - {shift.to} ({shift.hours}h)
|
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} • {shift.from} - {shift.to} ({shift.hours}h)
|
||||||
</div>
|
</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 && (
|
{shift.vehicle && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<div className="text-xs text-muted-foreground">
|
||||||
<CarIcon className="h-3 w-3 mr-1" />
|
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||||
{shift.vehicle.licensePlate}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -270,101 +210,7 @@ export default function ServicePlanning() {
|
|||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<p className="text-center text-muted-foreground">Nessun agente con turni fissi assegnati</p>
|
<p className="text-center text-muted-foreground">Nessuna guardia con turni 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>
|
||||||
)}
|
)}
|
||||||
@ -406,29 +252,20 @@ 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>
|
||||||
<div className="flex gap-1">
|
|
||||||
<Badge variant="secondary">{shift.totalGuards} {shift.totalGuards === 1 ? "guardia" : "guardie"}</Badge>
|
<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 flex items-center justify-between">
|
<div key={idx} className="text-sm text-muted-foreground">
|
||||||
<span>{guard.guardName} ({guard.badgeNumber}) - {guard.hours}h</span>
|
👤 {guard.guardName} ({guard.badgeNumber}) - {guard.hours}h
|
||||||
{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>
|
||||||
|
|||||||
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
|
// Calcola disponibilità agenti con report CCNL
|
||||||
const guardsWithAvailability = await Promise.all(
|
const guardsWithAvailability = await Promise.all(
|
||||||
locationGuards.map(async (guard) => {
|
locationGuards.map(async (guard) => {
|
||||||
@ -697,10 +686,6 @@ 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,
|
||||||
@ -710,18 +695,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...guard,
|
...guard,
|
||||||
isAvailable: !assignedShift && !assignedPatrolRoute,
|
isAvailable: !assignedShift,
|
||||||
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,
|
||||||
@ -1475,229 +1455,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// ============= SERVICE PLANNING ROUTES =============
|
// ============= SERVICE PLANNING ROUTES =============
|
||||||
|
|
||||||
// Vista per Agente Fisso - mostra orari e dotazioni operative per turni fissi
|
// Vista per Guardia - mostra orari e dotazioni per ogni guardia
|
||||||
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");
|
||||||
@ -1893,7 +1651,6 @@ 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,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.49",
|
"version": "1.0.48",
|
||||||
"lastUpdate": "2025-10-23T17:04:52.044Z",
|
"lastUpdate": "2025-10-23T16:03:23.268Z",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
{
|
|
||||||
"version": "1.0.49",
|
|
||||||
"date": "2025-10-23",
|
|
||||||
"type": "patch",
|
|
||||||
"description": "Deployment automatico v1.0.49"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "1.0.48",
|
"version": "1.0.48",
|
||||||
"date": "2025-10-23",
|
"date": "2025-10-23",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user