Restored to '4a2b5fab66e760175f7609180824ca0ac4f08d5a'
Replit-Restored-To: 4a2b5fab66
This commit is contained in:
parent
1c183a18ec
commit
ab85e8eb03
@ -21,13 +21,15 @@ import AdvancedPlanning from "@/pages/advanced-planning";
|
||||
import Vehicles from "@/pages/vehicles";
|
||||
import Parameters from "@/pages/parameters";
|
||||
import Services from "@/pages/services";
|
||||
import Planning from "@/pages/planning";
|
||||
import OperationalPlanning from "@/pages/operational-planning";
|
||||
import GeneralPlanning from "@/pages/general-planning";
|
||||
import ServicePlanning from "@/pages/service-planning";
|
||||
import Customers from "@/pages/customers";
|
||||
import PlanningMobile from "@/pages/planning-mobile";
|
||||
import MyShiftsFixed from "@/pages/my-shifts-fixed";
|
||||
import MyShiftsMobile from "@/pages/my-shifts-mobile";
|
||||
import SitePlanningView from "@/pages/site-planning-view";
|
||||
import PlanningViewFixedAgent from "@/pages/planning-view-fixed-agent";
|
||||
import PlanningViewMobileAgent from "@/pages/planning-view-mobile-agent";
|
||||
|
||||
function Router() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@ -46,13 +48,15 @@ function Router() {
|
||||
<Route path="/services" component={Services} />
|
||||
<Route path="/vehicles" component={Vehicles} />
|
||||
<Route path="/shifts" component={Shifts} />
|
||||
<Route path="/planning" component={Planning} />
|
||||
<Route path="/operational-planning" component={OperationalPlanning} />
|
||||
<Route path="/general-planning" component={GeneralPlanning} />
|
||||
<Route path="/service-planning" component={ServicePlanning} />
|
||||
<Route path="/planning-mobile" component={PlanningMobile} />
|
||||
<Route path="/advanced-planning" component={AdvancedPlanning} />
|
||||
<Route path="/my-shifts-fixed" component={MyShiftsFixed} />
|
||||
<Route path="/my-shifts-mobile" component={MyShiftsMobile} />
|
||||
<Route path="/site-planning-view" component={SitePlanningView} />
|
||||
<Route path="/planning-view-fixed-agent" component={PlanningViewFixedAgent} />
|
||||
<Route path="/planning-view-mobile-agent" component={PlanningViewMobileAgent} />
|
||||
<Route path="/reports" component={Reports} />
|
||||
<Route path="/notifications" component={Notifications} />
|
||||
<Route path="/users" component={Users} />
|
||||
|
||||
@ -31,67 +31,55 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
const dashboardItems = [
|
||||
const menuItems = [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/",
|
||||
icon: Shield,
|
||||
roles: ["admin", "coordinator", "guard", "client"],
|
||||
},
|
||||
];
|
||||
|
||||
const creationItems = [
|
||||
{
|
||||
title: "Turni Fissi",
|
||||
title: "Turni",
|
||||
url: "/shifts",
|
||||
icon: Calendar,
|
||||
roles: ["admin", "coordinator", "guard"],
|
||||
},
|
||||
{
|
||||
title: "Pianificazione",
|
||||
url: "/planning",
|
||||
icon: ClipboardList,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
{
|
||||
title: "Pianificazione Operativa",
|
||||
url: "/operational-planning",
|
||||
icon: Calendar,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
{
|
||||
title: "Pattuglie Mobile",
|
||||
title: "Planning Fissi",
|
||||
url: "/general-planning",
|
||||
icon: BarChart3,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
{
|
||||
title: "Planning Mobile",
|
||||
url: "/planning-mobile",
|
||||
icon: Navigation,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
];
|
||||
|
||||
const consultationItems = [
|
||||
{
|
||||
title: "Planning Agente Fisso",
|
||||
url: "/planning-view-fixed-agent",
|
||||
icon: Users,
|
||||
title: "Planning di Servizio",
|
||||
url: "/service-planning",
|
||||
icon: ClipboardList,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
{
|
||||
title: "Planning Agente Mobile",
|
||||
url: "/planning-view-mobile-agent",
|
||||
icon: Navigation,
|
||||
title: "Gestione Pianificazioni",
|
||||
url: "/advanced-planning",
|
||||
icon: ClipboardList,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
{
|
||||
title: "Planning Sito",
|
||||
url: "/site-planning-view",
|
||||
icon: MapPin,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
];
|
||||
|
||||
const personalItems = [
|
||||
{
|
||||
title: "I Miei Turni Fissi",
|
||||
url: "/my-shifts-fixed",
|
||||
icon: Calendar,
|
||||
roles: ["guard"],
|
||||
},
|
||||
{
|
||||
title: "Le Mie Pattuglie",
|
||||
url: "/my-shifts-mobile",
|
||||
icon: Navigation,
|
||||
roles: ["guard"],
|
||||
},
|
||||
];
|
||||
|
||||
const registryItems = [
|
||||
{
|
||||
title: "Guardie",
|
||||
url: "/guards",
|
||||
@ -122,18 +110,12 @@ const registryItems = [
|
||||
icon: Car,
|
||||
roles: ["admin", "coordinator"],
|
||||
},
|
||||
];
|
||||
|
||||
const reportingItems = [
|
||||
{
|
||||
title: "Report",
|
||||
url: "/reports",
|
||||
icon: BarChart3,
|
||||
roles: ["admin", "coordinator", "client"],
|
||||
},
|
||||
];
|
||||
|
||||
const systemItems = [
|
||||
{
|
||||
title: "Notifiche",
|
||||
url: "/notifications",
|
||||
@ -158,26 +140,8 @@ export function AppSidebar() {
|
||||
const { user } = useAuth();
|
||||
const [location] = useLocation();
|
||||
|
||||
const filterItems = (items: typeof dashboardItems) =>
|
||||
items.filter((item) => user && item.roles.includes(user.role));
|
||||
|
||||
const renderMenuItems = (items: typeof dashboardItems) => (
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={location === item.url}
|
||||
data-testid={`link-${item.title.toLowerCase().replace(/\s+/g, "-")}`}
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
const filteredItems = menuItems.filter(
|
||||
(item) => user && item.roles.includes(user.role)
|
||||
);
|
||||
|
||||
return (
|
||||
@ -193,74 +157,27 @@ export function AppSidebar() {
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
{/* Dashboard */}
|
||||
{filterItems(dashboardItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Menu Principale</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(dashboardItems))}
|
||||
<SidebarMenu>
|
||||
{filteredItems.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={location === item.url}
|
||||
data-testid={`link-${item.title.toLowerCase()}`}
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{/* Planning Operativo - Creazione */}
|
||||
{filterItems(creationItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Planning - Creazione</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(creationItems))}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{/* Planning Operativo - Consultazione */}
|
||||
{filterItems(consultationItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Planning - Consultazione</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(consultationItems))}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{/* Viste Personali (Guard) */}
|
||||
{filterItems(personalItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>I Miei Turni</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(personalItems))}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{/* Anagrafica */}
|
||||
{filterItems(registryItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Anagrafica</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(registryItems))}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{/* Report */}
|
||||
{filterItems(reportingItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Reporting</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(reportingItems))}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
||||
{/* Sistema */}
|
||||
{filterItems(systemItems).length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Sistema</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{renderMenuItems(filterItems(systemItems))}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter className="p-4 border-t space-y-4">
|
||||
|
||||
@ -1,273 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Calendar, Shield, Car, MapPin, Clock, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { format, addDays, startOfWeek } from "date-fns";
|
||||
import { it } from "date-fns/locale";
|
||||
|
||||
type Location = "roccapiemonte" | "milano" | "roma";
|
||||
|
||||
export default function PlanningViewFixedAgent() {
|
||||
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
|
||||
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
||||
const [weekStart, setWeekStart] = useState<string>(
|
||||
format(startOfWeek(new Date(), { weekStartsOn: 1 }), "yyyy-MM-dd")
|
||||
);
|
||||
|
||||
const locationLabels: Record<Location, string> = {
|
||||
roccapiemonte: "Roccapiemonte",
|
||||
milano: "Milano",
|
||||
roma: "Roma",
|
||||
};
|
||||
|
||||
// Query guardie per location
|
||||
const { data: guards } = useQuery<any[]>({
|
||||
queryKey: ["/api/guards", selectedLocation],
|
||||
queryFn: async () => {
|
||||
const response = await fetch("/api/guards");
|
||||
if (!response.ok) throw new Error("Failed to fetch guards");
|
||||
const allGuards = await response.json();
|
||||
return allGuards.filter((g: any) => g.location === selectedLocation && g.isActive);
|
||||
},
|
||||
});
|
||||
|
||||
// Query planning agente fisso
|
||||
const { data: planningData, isLoading } = useQuery<{
|
||||
guard: {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
badgeNumber: string;
|
||||
location: string;
|
||||
};
|
||||
weekStart: string;
|
||||
assignments: Array<{
|
||||
id: string;
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
siteAddress: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
isArmedOnDuty: boolean;
|
||||
hasVehicle: boolean;
|
||||
vehicleId: string | null;
|
||||
location: string;
|
||||
}>;
|
||||
}>({
|
||||
queryKey: ["/api/planning/fixed-agent", selectedGuardId, weekStart],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
guardId: selectedGuardId,
|
||||
weekStart,
|
||||
});
|
||||
const response = await fetch(`/api/planning/fixed-agent?${params.toString()}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch planning");
|
||||
return response.json();
|
||||
},
|
||||
enabled: !!selectedGuardId,
|
||||
});
|
||||
|
||||
const handlePreviousWeek = () => {
|
||||
const prevWeek = new Date(weekStart);
|
||||
prevWeek.setDate(prevWeek.getDate() - 7);
|
||||
setWeekStart(format(prevWeek, "yyyy-MM-dd"));
|
||||
};
|
||||
|
||||
const handleNextWeek = () => {
|
||||
const nextWeek = new Date(weekStart);
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
setWeekStart(format(nextWeek, "yyyy-MM-dd"));
|
||||
};
|
||||
|
||||
// Raggruppa assignments per giorno
|
||||
const assignmentsByDay: Record<string, typeof planningData.assignments> = {};
|
||||
if (planningData) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const dayDate = format(addDays(new Date(weekStart), i), "yyyy-MM-dd");
|
||||
assignmentsByDay[dayDate] = planningData.assignments.filter((a) => {
|
||||
const assignmentDate = format(new Date(a.startTime), "yyyy-MM-dd");
|
||||
return assignmentDate === dayDate;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Planning Agente Fisso - Consultazione</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Visualizza i turni fissi pianificati per un agente
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtri */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Filtri</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Sede</label>
|
||||
<Select value={selectedLocation} onValueChange={(v) => setSelectedLocation(v as Location)}>
|
||||
<SelectTrigger data-testid="select-location">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
||||
<SelectItem value="milano">Milano</SelectItem>
|
||||
<SelectItem value="roma">Roma</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Guardia</label>
|
||||
<Select value={selectedGuardId} onValueChange={setSelectedGuardId}>
|
||||
<SelectTrigger data-testid="select-guard">
|
||||
<SelectValue placeholder="Seleziona guardia" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{guards?.map((guard) => (
|
||||
<SelectItem key={guard.id} value={guard.id}>
|
||||
{guard.firstName} {guard.lastName} - #{guard.badgeNumber}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigazione settimana */}
|
||||
{selectedGuardId && (
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<Button variant="outline" size="sm" onClick={handlePreviousWeek} data-testid="button-prev-week">
|
||||
<ChevronLeft className="h-4 w-4 mr-2" />
|
||||
Settimana Precedente
|
||||
</Button>
|
||||
<div className="text-sm font-medium">
|
||||
{format(new Date(weekStart), "dd MMM", { locale: it })} -{" "}
|
||||
{format(addDays(new Date(weekStart), 6), "dd MMM yyyy", { locale: it })}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleNextWeek} data-testid="button-next-week">
|
||||
Settimana Successiva
|
||||
<ChevronRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Planning Data */}
|
||||
{selectedGuardId && planningData && (
|
||||
<>
|
||||
{/* Info Guardia */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span>
|
||||
{planningData.guard.firstName} {planningData.guard.lastName}
|
||||
</span>
|
||||
<Badge variant="outline">#{planningData.guard.badgeNumber}</Badge>
|
||||
<Badge variant="secondary">{locationLabels[planningData.guard.location as Location]}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>Turni fissi pianificati per la settimana</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Griglia Settimanale */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{[0, 1, 2, 3, 4, 5, 6].map((dayOffset) => {
|
||||
const dayDate = addDays(new Date(weekStart), dayOffset);
|
||||
const dayKey = format(dayDate, "yyyy-MM-dd");
|
||||
const dayAssignments = assignmentsByDay[dayKey] || [];
|
||||
|
||||
return (
|
||||
<Card key={dayKey}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{format(dayDate, "EEEE dd MMMM", { locale: it })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{dayAssignments.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{dayAssignments.map((assignment) => (
|
||||
<div
|
||||
key={assignment.id}
|
||||
className="p-4 border rounded-lg space-y-2"
|
||||
data-testid={`assignment-${assignment.id}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{assignment.siteName}</div>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-1 mt-1">
|
||||
<MapPin className="h-3 w-3" />
|
||||
{assignment.siteAddress}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{format(new Date(assignment.startTime), "HH:mm")} -{" "}
|
||||
{format(new Date(assignment.endTime), "HH:mm")}
|
||||
</Badge>
|
||||
|
||||
{assignment.isArmedOnDuty && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Armato
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{assignment.hasVehicle && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Car className="h-3 w-3 mr-1" />
|
||||
Automezzo
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground py-4 text-center">Nessun turno pianificato</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedGuardId && isLoading && (
|
||||
<Card>
|
||||
<CardContent className="py-16 text-center">
|
||||
<p className="text-muted-foreground">Caricamento planning...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!selectedGuardId && (
|
||||
<Card>
|
||||
<CardContent className="py-16 text-center">
|
||||
<Calendar className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<p className="text-lg font-medium">Seleziona una guardia</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Scegli una guardia per visualizzare i turni fissi pianificati
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,275 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Calendar, MapPin, Clock, Car, Navigation, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { format, addDays, startOfWeek, endOfWeek } from "date-fns";
|
||||
import { it } from "date-fns/locale";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Location = "roccapiemonte" | "milano" | "roma";
|
||||
|
||||
export default function PlanningViewMobileAgent() {
|
||||
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
|
||||
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
||||
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(
|
||||
startOfWeek(new Date(), { weekStartsOn: 1 })
|
||||
);
|
||||
|
||||
const locationLabels: Record<Location, string> = {
|
||||
roccapiemonte: "Roccapiemonte",
|
||||
milano: "Milano",
|
||||
roma: "Roma",
|
||||
};
|
||||
|
||||
// Query guardie per location
|
||||
const { data: guards } = useQuery<any[]>({
|
||||
queryKey: ["/api/guards", selectedLocation],
|
||||
queryFn: async () => {
|
||||
const response = await fetch("/api/guards");
|
||||
if (!response.ok) throw new Error("Failed to fetch guards");
|
||||
const allGuards = await response.json();
|
||||
return allGuards.filter((g: any) => g.location === selectedLocation && g.isActive && g.hasDriverLicense);
|
||||
},
|
||||
});
|
||||
|
||||
// Query planning agente mobile per settimana
|
||||
const { data: weekData, isLoading } = useQuery<any>({
|
||||
queryKey: ["/api/planning/mobile-agent", selectedGuardId, currentWeekStart],
|
||||
queryFn: async () => {
|
||||
const startDate = format(currentWeekStart, "yyyy-MM-dd");
|
||||
const endDate = format(endOfWeek(currentWeekStart, { weekStartsOn: 1 }), "yyyy-MM-dd");
|
||||
const params = new URLSearchParams({
|
||||
guardId: selectedGuardId,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
const response = await fetch(`/api/planning/mobile-agent?${params.toString()}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch planning");
|
||||
return response.json();
|
||||
},
|
||||
enabled: !!selectedGuardId,
|
||||
});
|
||||
|
||||
const handlePreviousWeek = () => {
|
||||
setCurrentWeekStart((prev) => addDays(prev, -7));
|
||||
};
|
||||
|
||||
const handleNextWeek = () => {
|
||||
setCurrentWeekStart((prev) => addDays(prev, 7));
|
||||
};
|
||||
|
||||
const handleToday = () => {
|
||||
setCurrentWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 }));
|
||||
};
|
||||
|
||||
// Generate 7 days for the week
|
||||
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeekStart, i));
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Planning Agente Mobile - Consultazione</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Vista settimanale dei percorsi pattuglia pianificati per un agente
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtri */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Filtri</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Sede</label>
|
||||
<Select value={selectedLocation} onValueChange={(v) => setSelectedLocation(v as Location)}>
|
||||
<SelectTrigger data-testid="select-location">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
||||
<SelectItem value="milano">Milano</SelectItem>
|
||||
<SelectItem value="roma">Roma</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Guardia</label>
|
||||
<Select value={selectedGuardId} onValueChange={setSelectedGuardId}>
|
||||
<SelectTrigger data-testid="select-guard">
|
||||
<SelectValue placeholder="Seleziona guardia" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{guards?.map((guard) => (
|
||||
<SelectItem key={guard.id} value={guard.id}>
|
||||
{guard.firstName} {guard.lastName} - #{guard.badgeNumber}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigazione settimana */}
|
||||
{selectedGuardId && (
|
||||
<div className="flex items-center justify-between gap-2 pt-4">
|
||||
<Button variant="outline" size="sm" onClick={handlePreviousWeek} data-testid="button-prev-week">
|
||||
<ChevronLeft className="h-4 w-4 mr-2" />
|
||||
Settimana Precedente
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleToday} data-testid="button-today">
|
||||
Oggi
|
||||
</Button>
|
||||
<div className="text-sm font-medium text-center">
|
||||
{format(currentWeekStart, "dd MMM", { locale: it })} -{" "}
|
||||
{format(endOfWeek(currentWeekStart, { weekStartsOn: 1 }), "dd MMM yyyy", { locale: it })}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleNextWeek} data-testid="button-next-week">
|
||||
Settimana Successiva
|
||||
<ChevronRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Planning Data - Vista Settimanale */}
|
||||
{selectedGuardId && weekData && weekData.guard && (
|
||||
<>
|
||||
{/* Info Guardia */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span>
|
||||
{weekData.guard.firstName} {weekData.guard.lastName}
|
||||
</span>
|
||||
<Badge variant="outline">#{weekData.guard.badgeNumber}</Badge>
|
||||
<Badge variant="secondary">{locationLabels[weekData.guard.location as Location]}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>Percorsi pattuglia pianificati nella settimana</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Griglia Settimanale */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{weekDays.map((day) => {
|
||||
const dateStr = format(day, "yyyy-MM-dd");
|
||||
const dayData = weekData.days?.find((d: any) => d.date === dateStr);
|
||||
|
||||
return (
|
||||
<Card key={dateStr} data-testid={`day-card-${dateStr}`}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{format(day, "EEEE dd/MM", { locale: it })}
|
||||
</span>
|
||||
{dayData?.route && (
|
||||
<Badge variant="secondary">
|
||||
<Navigation className="h-3 w-3 mr-1" />
|
||||
{dayData.stops?.length || 0} tappe
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{dayData?.route ? (
|
||||
<div className="space-y-3">
|
||||
{/* Orario e dettagli turno */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{dayData.route.startTime} - {dayData.route.endTime}
|
||||
</Badge>
|
||||
|
||||
{dayData.route.hasVehicle && (
|
||||
<Badge variant="secondary">
|
||||
<Car className="h-3 w-3 mr-1" />
|
||||
Automezzo
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lista tappe */}
|
||||
{dayData.stops && dayData.stops.length > 0 && (
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase">
|
||||
Sequenza Tappe
|
||||
</div>
|
||||
{dayData.stops.map((stop: any) => (
|
||||
<div
|
||||
key={stop.id}
|
||||
className="flex items-start gap-2 text-sm"
|
||||
data-testid={`stop-${stop.sequenceOrder}`}
|
||||
>
|
||||
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
|
||||
{stop.sequenceOrder}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{stop.siteName}</div>
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate">{stop.siteAddress}</span>
|
||||
</div>
|
||||
{stop.estimatedArrivalTime && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Arrivo: {stop.estimatedArrivalTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note */}
|
||||
{dayData.route.notes && (
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Note:</strong> {dayData.route.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<Navigation className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
||||
<p className="text-sm text-muted-foreground">Nessun percorso pianificato</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedGuardId && isLoading && (
|
||||
<Card>
|
||||
<CardContent className="py-16 text-center">
|
||||
<p className="text-muted-foreground">Caricamento planning...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!selectedGuardId && (
|
||||
<Card>
|
||||
<CardContent className="py-16 text-center">
|
||||
<Navigation className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<p className="text-lg font-medium">Seleziona una guardia</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Scegli una guardia per visualizzare i percorsi pattuglia pianificati
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -42,25 +42,6 @@ export default function Shifts() {
|
||||
queryKey: ["/api/guards"],
|
||||
});
|
||||
|
||||
// Query guardie con controllo vincolo esclusività mobile (per assegnazione turni)
|
||||
const { data: guardsForShift } = useQuery<Array<GuardWithCertifications & { isBookedMobile: boolean }>>({
|
||||
queryKey: ["/api/guards/for-shift", selectedShift?.startTime, selectedShift?.site.location],
|
||||
queryFn: async () => {
|
||||
if (!selectedShift) return [];
|
||||
|
||||
const shiftDate = format(new Date(selectedShift.startTime), "yyyy-MM-dd");
|
||||
const params = new URLSearchParams({
|
||||
date: shiftDate,
|
||||
location: selectedShift.site.location,
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/guards/for-shift?${params.toString()}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch guards for shift");
|
||||
return response.json();
|
||||
},
|
||||
enabled: !!selectedShift && isAssignDialogOpen,
|
||||
});
|
||||
|
||||
// Filter data by location
|
||||
const filteredShifts = selectedLocation === "all"
|
||||
? shifts
|
||||
@ -582,12 +563,11 @@ export default function Shifts() {
|
||||
{/* Guards List */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium">Guardie Disponibili</h3>
|
||||
{guardsForShift && guardsForShift.length > 0 ? (
|
||||
{guards && guards.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{guardsForShift?.map((guard) => {
|
||||
{filteredGuards?.map((guard) => {
|
||||
const assigned = isGuardAssigned(guard.id);
|
||||
const canAssign = canGuardBeAssigned(guard);
|
||||
const bookedOnMobile = guard.isBookedMobile;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -598,16 +578,11 @@ export default function Shifts() {
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium">
|
||||
{guard.firstName} {guard.lastName}
|
||||
{guard.user?.firstName} {guard.user?.lastName}
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
#{guard.badgeNumber}
|
||||
</span>
|
||||
{bookedOnMobile && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Su pattuglia mobile
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{guard.isArmed && (
|
||||
@ -640,10 +615,10 @@ export default function Shifts() {
|
||||
size="sm"
|
||||
variant={assigned ? "secondary" : "default"}
|
||||
onClick={() => handleAssignGuard(guard.id)}
|
||||
disabled={assigned || !canAssign || bookedOnMobile}
|
||||
disabled={assigned || !canAssign}
|
||||
data-testid={`button-assign-guard-${guard.id}`}
|
||||
>
|
||||
{assigned ? "Assegnato" : bookedOnMobile ? "Su pattuglia" : canAssign ? "Assegna" : "Non idoneo"}
|
||||
{assigned ? "Assegnato" : canAssign ? "Assegna" : "Non idoneo"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
14
replit.md
14
replit.md
@ -142,20 +142,6 @@ To prevent timezone-related bugs, especially when assigning shifts, dates should
|
||||
- Backend endpoint: GET `/api/site-planning/:siteId` with date range filters
|
||||
- **Impact**: Complete end-to-end planning system supporting both coordinator and guard roles with database-backed route planning and operational equipment tracking
|
||||
|
||||
### Planning Consultation Pages & Sidebar Reorganization (October 23, 2025)
|
||||
- **Issue**: Coordinators needed separate consultation views to review planned shifts without mixing creation and consultation workflows. Sidebar was cluttered with deprecated planning pages.
|
||||
- **Solution**:
|
||||
- **New Consultation Pages**:
|
||||
- `planning-view-fixed-agent.tsx`: Weekly view of guard's fixed shifts showing orari, dotazioni (armato, automezzo), location, sito
|
||||
- `planning-view-mobile-agent.tsx`: Weekly grid view (7 days) of guard's patrol routes with site addresses, sequenced stops, and equipment
|
||||
- Backend endpoint `/api/planning/mobile-agent` updated to accept startDate/endDate range and return `{ guard, days[] }` structure
|
||||
- **Sidebar Reorganization**:
|
||||
- Removed deprecated routes: `planning`, `operational-planning`, `general-planning`, `service-planning`
|
||||
- Created logical groups: Dashboard, Planning-Creazione (Turni Fissi, Pattuglie Mobile), Planning-Consultazione (Planning Agente Fisso, Planning Agente Mobile, Planning Sito), I Miei Turni, Anagrafica, Reporting, Sistema
|
||||
- Role-based filtering maintained for all groups
|
||||
- **Routes Cleanup**: Removed deprecated planning page imports and routes from App.tsx
|
||||
- **Impact**: Clear separation between creation (shifts.tsx, planning-mobile.tsx) and consultation (planning-view-*) workflows. Coordinators can now efficiently review weekly assignments for guards and sites without navigating through creation interfaces.
|
||||
|
||||
## External Dependencies
|
||||
- **Replit Auth**: For OpenID Connect (OIDC) based authentication.
|
||||
- **Neon**: Managed PostgreSQL database service.
|
||||
|
||||
285
server/routes.ts
285
server/routes.ts
@ -332,290 +332,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Get guards for shift assignment with mobile exclusivity check
|
||||
app.get("/api/guards/for-shift", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { date, location } = req.query;
|
||||
|
||||
if (!date || typeof date !== "string") {
|
||||
return res.status(400).json({ message: "Date parameter required (YYYY-MM-DD)" });
|
||||
}
|
||||
|
||||
if (!location || !["roccapiemonte", "milano", "roma"].includes(location as string)) {
|
||||
return res.status(400).json({ message: "Valid location parameter required" });
|
||||
}
|
||||
|
||||
// Valida formato data
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(date)) {
|
||||
return res.status(400).json({ message: "Invalid date format, use YYYY-MM-DD" });
|
||||
}
|
||||
|
||||
// Ottieni tutte le guardie per la location
|
||||
const allGuards = await db
|
||||
.select({
|
||||
id: guards.id,
|
||||
userId: guards.userId,
|
||||
firstName: guards.firstName,
|
||||
lastName: guards.lastName,
|
||||
badgeNumber: guards.badgeNumber,
|
||||
location: guards.location,
|
||||
hasDriverLicense: guards.hasDriverLicense,
|
||||
isActive: guards.isActive,
|
||||
createdAt: guards.createdAt,
|
||||
updatedAt: guards.updatedAt,
|
||||
})
|
||||
.from(guards)
|
||||
.where(
|
||||
and(
|
||||
eq(guards.location, location as any),
|
||||
eq(guards.isActive, true)
|
||||
)
|
||||
)
|
||||
.orderBy(guards.lastName, guards.firstName);
|
||||
|
||||
// Verifica quali guardie hanno patrol routes (mobile) per questa data
|
||||
const patrolRoutesForDate = await db
|
||||
.select({
|
||||
guardId: patrolRoutes.guardId,
|
||||
})
|
||||
.from(patrolRoutes)
|
||||
.where(
|
||||
and(
|
||||
eq(patrolRoutes.shiftDate, date),
|
||||
eq(patrolRoutes.location, location as any),
|
||||
ne(patrolRoutes.status, "cancelled")
|
||||
)
|
||||
);
|
||||
|
||||
const guardsOnMobile = new Set(patrolRoutesForDate.map(pr => pr.guardId));
|
||||
|
||||
// Aggiungi flag isBookedMobile per ogni guardia
|
||||
const guardsWithMobileFlag = allGuards.map(guard => ({
|
||||
...guard,
|
||||
isBookedMobile: guardsOnMobile.has(guard.id),
|
||||
}));
|
||||
|
||||
res.json(guardsWithMobileFlag);
|
||||
} catch (error) {
|
||||
console.error("Error fetching guards for shift:", error);
|
||||
res.status(500).json({ message: "Failed to fetch guards for shift" });
|
||||
}
|
||||
});
|
||||
|
||||
// ============= PLANNING CONSULTATION ROUTES =============
|
||||
|
||||
// GET /api/planning/fixed-agent - Vista consultazione planning per agente fisso
|
||||
app.get("/api/planning/fixed-agent", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { guardId, weekStart } = req.query;
|
||||
|
||||
if (!guardId || typeof guardId !== "string") {
|
||||
return res.status(400).json({ message: "GuardId parameter required" });
|
||||
}
|
||||
|
||||
if (!weekStart || typeof weekStart !== "string") {
|
||||
return res.status(400).json({ message: "WeekStart parameter required (YYYY-MM-DD)" });
|
||||
}
|
||||
|
||||
// Valida formato data
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(weekStart)) {
|
||||
return res.status(400).json({ message: "Invalid weekStart format, use YYYY-MM-DD" });
|
||||
}
|
||||
|
||||
// Calcola fine settimana
|
||||
const weekStartDate = new Date(weekStart);
|
||||
weekStartDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const weekEndDate = new Date(weekStart);
|
||||
weekEndDate.setDate(weekEndDate.getDate() + 6);
|
||||
weekEndDate.setHours(23, 59, 59, 999);
|
||||
|
||||
// Ottieni info guardia
|
||||
const guard = await db
|
||||
.select({
|
||||
id: guards.id,
|
||||
firstName: guards.firstName,
|
||||
lastName: guards.lastName,
|
||||
badgeNumber: guards.badgeNumber,
|
||||
location: guards.location,
|
||||
})
|
||||
.from(guards)
|
||||
.where(eq(guards.id, guardId))
|
||||
.limit(1);
|
||||
|
||||
if (guard.length === 0) {
|
||||
return res.status(404).json({ message: "Guard not found" });
|
||||
}
|
||||
|
||||
// Ottieni tutti i turni fissi della guardia per la settimana
|
||||
const assignments = await db
|
||||
.select({
|
||||
id: shiftAssignments.id,
|
||||
shiftId: shifts.id,
|
||||
siteId: sites.id,
|
||||
siteName: sites.name,
|
||||
siteAddress: sites.address,
|
||||
startTime: shifts.startTime,
|
||||
endTime: shifts.endTime,
|
||||
isArmedOnDuty: shiftAssignments.isArmedOnDuty,
|
||||
assignedVehicleId: shiftAssignments.assignedVehicleId,
|
||||
location: sites.location,
|
||||
})
|
||||
.from(shiftAssignments)
|
||||
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
|
||||
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||
.where(
|
||||
and(
|
||||
eq(shiftAssignments.guardId, guardId),
|
||||
gte(shifts.startTime, weekStartDate),
|
||||
lte(shifts.startTime, weekEndDate)
|
||||
)
|
||||
)
|
||||
.orderBy(shifts.startTime);
|
||||
|
||||
res.json({
|
||||
guard: guard[0],
|
||||
weekStart,
|
||||
assignments: assignments.map(a => ({
|
||||
id: a.id,
|
||||
shiftId: a.shiftId,
|
||||
siteId: a.siteId,
|
||||
siteName: a.siteName,
|
||||
siteAddress: a.siteAddress,
|
||||
startTime: a.startTime,
|
||||
endTime: a.endTime,
|
||||
isArmedOnDuty: a.isArmedOnDuty || false,
|
||||
hasVehicle: !!a.assignedVehicleId,
|
||||
vehicleId: a.assignedVehicleId,
|
||||
location: a.location,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching fixed agent planning:", error);
|
||||
res.status(500).json({ message: "Failed to fetch fixed agent planning" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/planning/mobile-agent - Vista consultazione planning per agente mobile
|
||||
app.get("/api/planning/mobile-agent", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { guardId, startDate, endDate } = req.query;
|
||||
|
||||
if (!guardId || typeof guardId !== "string") {
|
||||
return res.status(400).json({ message: "GuardId parameter required" });
|
||||
}
|
||||
|
||||
if (!startDate || typeof startDate !== "string") {
|
||||
return res.status(400).json({ message: "StartDate parameter required (YYYY-MM-DD)" });
|
||||
}
|
||||
|
||||
if (!endDate || typeof endDate !== "string") {
|
||||
return res.status(400).json({ message: "EndDate parameter required (YYYY-MM-DD)" });
|
||||
}
|
||||
|
||||
// Valida formato data
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
|
||||
return res.status(400).json({ message: "Invalid date format, use YYYY-MM-DD" });
|
||||
}
|
||||
|
||||
// Ottieni info guardia
|
||||
const guard = await db
|
||||
.select({
|
||||
id: guards.id,
|
||||
firstName: guards.firstName,
|
||||
lastName: guards.lastName,
|
||||
badgeNumber: guards.badgeNumber,
|
||||
location: guards.location,
|
||||
})
|
||||
.from(guards)
|
||||
.where(eq(guards.id, guardId))
|
||||
.limit(1);
|
||||
|
||||
if (guard.length === 0) {
|
||||
return res.status(404).json({ message: "Guard not found" });
|
||||
}
|
||||
|
||||
// Ottieni tutti i patrol routes per questa guardia nel range di date
|
||||
const routes = await db
|
||||
.select({
|
||||
id: patrolRoutes.id,
|
||||
shiftDate: patrolRoutes.shiftDate,
|
||||
startTime: patrolRoutes.startTime,
|
||||
endTime: patrolRoutes.endTime,
|
||||
location: patrolRoutes.location,
|
||||
status: patrolRoutes.status,
|
||||
notes: patrolRoutes.notes,
|
||||
assignedVehicleId: patrolRoutes.assignedVehicleId,
|
||||
})
|
||||
.from(patrolRoutes)
|
||||
.where(
|
||||
and(
|
||||
eq(patrolRoutes.guardId, guardId),
|
||||
gte(patrolRoutes.shiftDate, startDate),
|
||||
lte(patrolRoutes.shiftDate, endDate)
|
||||
)
|
||||
)
|
||||
.orderBy(patrolRoutes.shiftDate);
|
||||
|
||||
// Per ogni route, ottieni gli stops
|
||||
const days = await Promise.all(
|
||||
routes.map(async (route) => {
|
||||
const stops = await db
|
||||
.select({
|
||||
id: patrolRouteStops.id,
|
||||
siteId: sites.id,
|
||||
siteName: sites.name,
|
||||
siteAddress: sites.address,
|
||||
latitude: sites.latitude,
|
||||
longitude: sites.longitude,
|
||||
sequenceOrder: patrolRouteStops.sequenceOrder,
|
||||
estimatedArrivalTime: patrolRouteStops.estimatedArrivalTime,
|
||||
})
|
||||
.from(patrolRouteStops)
|
||||
.innerJoin(sites, eq(patrolRouteStops.siteId, sites.id))
|
||||
.where(eq(patrolRouteStops.patrolRouteId, route.id))
|
||||
.orderBy(patrolRouteStops.sequenceOrder);
|
||||
|
||||
return {
|
||||
date: route.shiftDate,
|
||||
route: {
|
||||
id: route.id,
|
||||
startTime: route.startTime,
|
||||
endTime: route.endTime,
|
||||
location: route.location,
|
||||
status: route.status,
|
||||
notes: route.notes,
|
||||
hasVehicle: !!route.assignedVehicleId,
|
||||
vehicleId: route.assignedVehicleId,
|
||||
},
|
||||
stops: stops.map(s => ({
|
||||
id: s.id,
|
||||
siteId: s.siteId,
|
||||
siteName: s.siteName,
|
||||
siteAddress: s.siteAddress,
|
||||
latitude: s.latitude,
|
||||
longitude: s.longitude,
|
||||
sequenceOrder: s.sequenceOrder,
|
||||
estimatedArrivalTime: s.estimatedArrivalTime,
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
guard: guard[0],
|
||||
days,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching mobile agent planning:", error);
|
||||
res.status(500).json({ message: "Failed to fetch mobile agent planning" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET vehicles available for a location
|
||||
// Get vehicles available for a location
|
||||
app.get("/api/vehicles/available", isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { location } = req.query;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user