Restored to '4a2b5fab66e760175f7609180824ca0ac4f08d5a'

Replit-Restored-To: 4a2b5fab66
This commit is contained in:
marco370 2025-10-23 16:38:19 +00:00
parent 1c183a18ec
commit ab85e8eb03
7 changed files with 64 additions and 1013 deletions

View File

@ -21,13 +21,15 @@ import AdvancedPlanning from "@/pages/advanced-planning";
import Vehicles from "@/pages/vehicles"; import Vehicles from "@/pages/vehicles";
import Parameters from "@/pages/parameters"; import Parameters from "@/pages/parameters";
import Services from "@/pages/services"; 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 Customers from "@/pages/customers";
import PlanningMobile from "@/pages/planning-mobile"; import PlanningMobile from "@/pages/planning-mobile";
import MyShiftsFixed from "@/pages/my-shifts-fixed"; import MyShiftsFixed from "@/pages/my-shifts-fixed";
import MyShiftsMobile from "@/pages/my-shifts-mobile"; import MyShiftsMobile from "@/pages/my-shifts-mobile";
import SitePlanningView from "@/pages/site-planning-view"; 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() { function Router() {
const { isAuthenticated, isLoading } = useAuth(); const { isAuthenticated, isLoading } = useAuth();
@ -46,13 +48,15 @@ function Router() {
<Route path="/services" component={Services} /> <Route path="/services" component={Services} />
<Route path="/vehicles" component={Vehicles} /> <Route path="/vehicles" component={Vehicles} />
<Route path="/shifts" component={Shifts} /> <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="/planning-mobile" component={PlanningMobile} />
<Route path="/advanced-planning" component={AdvancedPlanning} /> <Route path="/advanced-planning" component={AdvancedPlanning} />
<Route path="/my-shifts-fixed" component={MyShiftsFixed} /> <Route path="/my-shifts-fixed" component={MyShiftsFixed} />
<Route path="/my-shifts-mobile" component={MyShiftsMobile} /> <Route path="/my-shifts-mobile" component={MyShiftsMobile} />
<Route path="/site-planning-view" component={SitePlanningView} /> <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="/reports" component={Reports} />
<Route path="/notifications" component={Notifications} /> <Route path="/notifications" component={Notifications} />
<Route path="/users" component={Users} /> <Route path="/users" component={Users} />

View File

@ -31,67 +31,55 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/theme-toggle"; import { ThemeToggle } from "@/components/theme-toggle";
const dashboardItems = [ const menuItems = [
{ {
title: "Dashboard", title: "Dashboard",
url: "/", url: "/",
icon: Shield, icon: Shield,
roles: ["admin", "coordinator", "guard", "client"], roles: ["admin", "coordinator", "guard", "client"],
}, },
];
const creationItems = [
{ {
title: "Turni Fissi", title: "Turni",
url: "/shifts", url: "/shifts",
icon: Calendar, 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"], roles: ["admin", "coordinator"],
}, },
{ {
title: "Pattuglie Mobile", title: "Planning Fissi",
url: "/general-planning",
icon: BarChart3,
roles: ["admin", "coordinator"],
},
{
title: "Planning Mobile",
url: "/planning-mobile", url: "/planning-mobile",
icon: Navigation, icon: Navigation,
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],
}, },
];
const consultationItems = [
{ {
title: "Planning Agente Fisso", title: "Planning di Servizio",
url: "/planning-view-fixed-agent", url: "/service-planning",
icon: Users, icon: ClipboardList,
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],
}, },
{ {
title: "Planning Agente Mobile", title: "Gestione Pianificazioni",
url: "/planning-view-mobile-agent", url: "/advanced-planning",
icon: Navigation, icon: ClipboardList,
roles: ["admin", "coordinator"], 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", title: "Guardie",
url: "/guards", url: "/guards",
@ -122,18 +110,12 @@ const registryItems = [
icon: Car, icon: Car,
roles: ["admin", "coordinator"], roles: ["admin", "coordinator"],
}, },
];
const reportingItems = [
{ {
title: "Report", title: "Report",
url: "/reports", url: "/reports",
icon: BarChart3, icon: BarChart3,
roles: ["admin", "coordinator", "client"], roles: ["admin", "coordinator", "client"],
}, },
];
const systemItems = [
{ {
title: "Notifiche", title: "Notifiche",
url: "/notifications", url: "/notifications",
@ -158,26 +140,8 @@ export function AppSidebar() {
const { user } = useAuth(); const { user } = useAuth();
const [location] = useLocation(); const [location] = useLocation();
const filterItems = (items: typeof dashboardItems) => const filteredItems = menuItems.filter(
items.filter((item) => user && item.roles.includes(user.role)); (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>
); );
return ( return (
@ -193,74 +157,27 @@ export function AppSidebar() {
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
{/* Dashboard */} <SidebarGroup>
{filterItems(dashboardItems).length > 0 && ( <SidebarGroupLabel>Menu Principale</SidebarGroupLabel>
<SidebarGroup> <SidebarGroupContent>
<SidebarGroupContent> <SidebarMenu>
{renderMenuItems(filterItems(dashboardItems))} {filteredItems.map((item) => (
</SidebarGroupContent> <SidebarMenuItem key={item.title}>
</SidebarGroup> <SidebarMenuButton
)} asChild
isActive={location === item.url}
{/* Planning Operativo - Creazione */} data-testid={`link-${item.title.toLowerCase()}`}
{filterItems(creationItems).length > 0 && ( >
<SidebarGroup> <Link href={item.url}>
<SidebarGroupLabel>Planning - Creazione</SidebarGroupLabel> <item.icon className="h-4 w-4" />
<SidebarGroupContent> <span>{item.title}</span>
{renderMenuItems(filterItems(creationItems))} </Link>
</SidebarGroupContent> </SidebarMenuButton>
</SidebarGroup> </SidebarMenuItem>
)} ))}
</SidebarMenu>
{/* Planning Operativo - Consultazione */} </SidebarGroupContent>
{filterItems(consultationItems).length > 0 && ( </SidebarGroup>
<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> </SidebarContent>
<SidebarFooter className="p-4 border-t space-y-4"> <SidebarFooter className="p-4 border-t space-y-4">

View File

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

View File

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

View File

@ -42,25 +42,6 @@ export default function Shifts() {
queryKey: ["/api/guards"], 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 // Filter data by location
const filteredShifts = selectedLocation === "all" const filteredShifts = selectedLocation === "all"
? shifts ? shifts
@ -582,12 +563,11 @@ export default function Shifts() {
{/* Guards List */} {/* Guards List */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-medium">Guardie Disponibili</h3> <h3 className="font-medium">Guardie Disponibili</h3>
{guardsForShift && guardsForShift.length > 0 ? ( {guards && guards.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{guardsForShift?.map((guard) => { {filteredGuards?.map((guard) => {
const assigned = isGuardAssigned(guard.id); const assigned = isGuardAssigned(guard.id);
const canAssign = canGuardBeAssigned(guard); const canAssign = canGuardBeAssigned(guard);
const bookedOnMobile = guard.isBookedMobile;
return ( return (
<div <div
@ -598,16 +578,11 @@ export default function Shifts() {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="font-medium"> <p className="font-medium">
{guard.firstName} {guard.lastName} {guard.user?.firstName} {guard.user?.lastName}
</p> </p>
<span className="text-xs text-muted-foreground font-mono"> <span className="text-xs text-muted-foreground font-mono">
#{guard.badgeNumber} #{guard.badgeNumber}
</span> </span>
{bookedOnMobile && (
<Badge variant="destructive" className="text-xs">
Su pattuglia mobile
</Badge>
)}
</div> </div>
<div className="flex gap-1 mt-1"> <div className="flex gap-1 mt-1">
{guard.isArmed && ( {guard.isArmed && (
@ -640,10 +615,10 @@ export default function Shifts() {
size="sm" size="sm"
variant={assigned ? "secondary" : "default"} variant={assigned ? "secondary" : "default"}
onClick={() => handleAssignGuard(guard.id)} onClick={() => handleAssignGuard(guard.id)}
disabled={assigned || !canAssign || bookedOnMobile} disabled={assigned || !canAssign}
data-testid={`button-assign-guard-${guard.id}`} data-testid={`button-assign-guard-${guard.id}`}
> >
{assigned ? "Assegnato" : bookedOnMobile ? "Su pattuglia" : canAssign ? "Assegna" : "Non idoneo"} {assigned ? "Assegnato" : canAssign ? "Assegna" : "Non idoneo"}
</Button> </Button>
</div> </div>
); );

View File

@ -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 - 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 - **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 ## External Dependencies
- **Replit Auth**: For OpenID Connect (OIDC) based authentication. - **Replit Auth**: For OpenID Connect (OIDC) based authentication.
- **Neon**: Managed PostgreSQL database service. - **Neon**: Managed PostgreSQL database service.

View File

@ -332,290 +332,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
// Get guards for shift assignment with mobile exclusivity check // Get vehicles available for a location
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
app.get("/api/vehicles/available", isAuthenticated, async (req, res) => { app.get("/api/vehicles/available", isAuthenticated, async (req, res) => {
try { try {
const { location } = req.query; const { location } = req.query;