Add a new section for viewing and managing service planning details
Implement the "Service Planning" page with backend API routes and frontend components for displaying guard and site schedules. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/KiuJzNf
This commit is contained in:
parent
a945abdb5d
commit
efcaca356a
@ -24,6 +24,7 @@ import Services from "@/pages/services";
|
|||||||
import Planning from "@/pages/planning";
|
import Planning from "@/pages/planning";
|
||||||
import OperationalPlanning from "@/pages/operational-planning";
|
import OperationalPlanning from "@/pages/operational-planning";
|
||||||
import GeneralPlanning from "@/pages/general-planning";
|
import GeneralPlanning from "@/pages/general-planning";
|
||||||
|
import ServicePlanning from "@/pages/service-planning";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
@ -44,6 +45,7 @@ function Router() {
|
|||||||
<Route path="/planning" component={Planning} />
|
<Route path="/planning" component={Planning} />
|
||||||
<Route path="/operational-planning" component={OperationalPlanning} />
|
<Route path="/operational-planning" component={OperationalPlanning} />
|
||||||
<Route path="/general-planning" component={GeneralPlanning} />
|
<Route path="/general-planning" component={GeneralPlanning} />
|
||||||
|
<Route path="/service-planning" component={ServicePlanning} />
|
||||||
<Route path="/advanced-planning" component={AdvancedPlanning} />
|
<Route path="/advanced-planning" component={AdvancedPlanning} />
|
||||||
<Route path="/reports" component={Reports} />
|
<Route path="/reports" component={Reports} />
|
||||||
<Route path="/notifications" component={Notifications} />
|
<Route path="/notifications" component={Notifications} />
|
||||||
|
|||||||
@ -61,6 +61,12 @@ const menuItems = [
|
|||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Planning di Servizio",
|
||||||
|
url: "/service-planning",
|
||||||
|
icon: ClipboardList,
|
||||||
|
roles: ["admin", "coordinator"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Gestione Pianificazioni",
|
title: "Gestione Pianificazioni",
|
||||||
url: "/advanced-planning",
|
url: "/advanced-planning",
|
||||||
|
|||||||
288
client/src/pages/service-planning.tsx
Normal file
288
client/src/pages/service-planning.tsx
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { format, addWeeks, addDays, startOfWeek } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
import { ChevronLeft, ChevronRight, Users, Building2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
type Location = "roccapiemonte" | "milano" | "roma";
|
||||||
|
|
||||||
|
interface ShiftDetail {
|
||||||
|
shiftId: string;
|
||||||
|
date: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
siteName: string;
|
||||||
|
siteId: string;
|
||||||
|
vehicle?: {
|
||||||
|
licensePlate: string;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
hours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuardSchedule {
|
||||||
|
guardId: string;
|
||||||
|
guardName: string;
|
||||||
|
badgeNumber: string;
|
||||||
|
shifts: ShiftDetail[];
|
||||||
|
totalHours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SiteSchedule {
|
||||||
|
siteId: string;
|
||||||
|
siteName: string;
|
||||||
|
location: string;
|
||||||
|
shifts: {
|
||||||
|
shiftId: string;
|
||||||
|
date: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
guards: {
|
||||||
|
guardName: string;
|
||||||
|
badgeNumber: string;
|
||||||
|
hours: number;
|
||||||
|
}[];
|
||||||
|
vehicle?: {
|
||||||
|
licensePlate: string;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
totalGuards: number;
|
||||||
|
totalHours: number;
|
||||||
|
}[];
|
||||||
|
totalShifts: number;
|
||||||
|
totalHours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServicePlanning() {
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
|
||||||
|
const [weekStart, setWeekStart] = useState<Date>(startOfWeek(new Date(), { weekStartsOn: 1 }));
|
||||||
|
const [viewMode, setViewMode] = useState<"guard" | "site">("guard");
|
||||||
|
|
||||||
|
const weekStartStr = format(weekStart, "yyyy-MM-dd");
|
||||||
|
const weekEndStr = format(addDays(weekStart, 6), "yyyy-MM-dd");
|
||||||
|
|
||||||
|
// Query per vista Guardie
|
||||||
|
const { data: guardSchedules, isLoading: isLoadingGuards } = useQuery<GuardSchedule[]>({
|
||||||
|
queryKey: ["/api/service-planning/by-guard", weekStartStr, selectedLocation],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/service-planning/by-guard?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch guard schedules");
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: viewMode === "guard",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query per vista Siti
|
||||||
|
const { data: siteSchedules, isLoading: isLoadingSites } = useQuery<SiteSchedule[]>({
|
||||||
|
queryKey: ["/api/service-planning/by-site", weekStartStr, selectedLocation],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/service-planning/by-site?weekStart=${weekStartStr}&location=${selectedLocation}`);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch site schedules");
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: viewMode === "site",
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToPreviousWeek = () => setWeekStart(addWeeks(weekStart, -1));
|
||||||
|
const goToNextWeek = () => setWeekStart(addWeeks(weekStart, 1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-auto p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Planning di Servizio</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Visualizza orari e dotazioni per guardia o sito
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controlli */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
|
{/* Selezione sede */}
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Navigazione settimana */}
|
||||||
|
<div className="flex-1 min-w-[300px]">
|
||||||
|
<label className="text-sm font-medium mb-2 block">Settimana</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="icon" onClick={goToPreviousWeek} data-testid="button-prev-week">
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div className="flex-1 text-center font-medium">
|
||||||
|
{format(weekStart, "d MMM", { locale: it })} - {format(addDays(weekStart, 6), "d MMM yyyy", { locale: it })}
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="icon" onClick={goToNextWeek} data-testid="button-next-week">
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Tabs per vista */}
|
||||||
|
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard" | "site")}>
|
||||||
|
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||||
|
<TabsTrigger value="guard" data-testid="tab-guard-view">
|
||||||
|
<Users className="h-4 w-4 mr-2" />
|
||||||
|
Vista Agente
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="site" data-testid="tab-site-view">
|
||||||
|
<Building2 className="h-4 w-4 mr-2" />
|
||||||
|
Vista Sito
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Vista Agente */}
|
||||||
|
<TabsContent value="guard" className="space-y-4 mt-6">
|
||||||
|
{isLoadingGuards ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-32 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : guardSchedules && guardSchedules.length > 0 ? (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{guardSchedules.map((guard) => (
|
||||||
|
<Card key={guard.guardId} data-testid={`card-guard-${guard.guardId}`}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg">
|
||||||
|
{guard.guardName} <Badge variant="outline">{guard.badgeNumber}</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
<Badge>{guard.totalHours}h totali</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{guard.shifts.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Nessun turno assegnato</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{guard.shifts.map((shift) => (
|
||||||
|
<div
|
||||||
|
key={shift.shiftId}
|
||||||
|
className="flex items-start justify-between p-3 rounded-md bg-muted/50"
|
||||||
|
data-testid={`shift-${shift.shiftId}`}
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-medium">{shift.siteName}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} • {shift.from} - {shift.to} ({shift.hours}h)
|
||||||
|
</div>
|
||||||
|
{shift.vehicle && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<p className="text-center text-muted-foreground">Nessuna guardia con turni assegnati</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Vista Sito */}
|
||||||
|
<TabsContent value="site" className="space-y-4 mt-6">
|
||||||
|
{isLoadingSites ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-32 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : siteSchedules && siteSchedules.length > 0 ? (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{siteSchedules.map((site) => (
|
||||||
|
<Card key={site.siteId} data-testid={`card-site-${site.siteId}`}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg">{site.siteName}</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline">{site.totalShifts} turni</Badge>
|
||||||
|
<Badge>{site.totalHours}h totali</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{site.shifts.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Nessun turno programmato</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{site.shifts.map((shift) => (
|
||||||
|
<div
|
||||||
|
key={shift.shiftId}
|
||||||
|
className="p-3 rounded-md bg-muted/50 space-y-2"
|
||||||
|
data-testid={`shift-${shift.shiftId}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="font-medium">
|
||||||
|
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} • {shift.from} - {shift.to}
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">{shift.totalGuards} {shift.totalGuards === 1 ? "guardia" : "guardie"}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{shift.guards.map((guard, idx) => (
|
||||||
|
<div key={idx} className="text-sm text-muted-foreground">
|
||||||
|
👤 {guard.guardName} ({guard.badgeNumber}) - {guard.hours}h
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{shift.vehicle && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<p className="text-center text-muted-foreground">Nessun sito con turni programmati</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
244
server/routes.ts
244
server/routes.ts
@ -1429,6 +1429,250 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============= SERVICE PLANNING ROUTES =============
|
||||||
|
|
||||||
|
// Vista per Guardia - mostra orari e dotazioni per ogni guardia
|
||||||
|
app.get("/api/service-planning/by-guard", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
|
||||||
|
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 per la sede (con JOIN su sites per filtrare location)
|
||||||
|
const weekShifts = await db
|
||||||
|
.select({
|
||||||
|
shift: shifts,
|
||||||
|
site: sites,
|
||||||
|
vehicle: vehicles,
|
||||||
|
})
|
||||||
|
.from(shifts)
|
||||||
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||||
|
.leftJoin(vehicles, eq(shifts.vehicleId, vehicles.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(shifts.startTime, weekStartTimestamp),
|
||||||
|
lte(shifts.startTime, weekEndTimestamp),
|
||||||
|
ne(shifts.status, "cancelled"),
|
||||||
|
eq(sites.location, location as any)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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; // Arrotonda a 1 decimale
|
||||||
|
|
||||||
|
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,
|
||||||
|
siteId: shiftData.site.id,
|
||||||
|
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 guard schedules:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch guard schedules", error: String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vista per Sito - mostra agenti e dotazioni per ogni sito
|
||||||
|
app.get("/api/service-planning/by-site", 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 tutti i siti attivi della sede
|
||||||
|
const activeSites = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.isActive, true),
|
||||||
|
eq(sites.location, location as any)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(sites.name);
|
||||||
|
|
||||||
|
// Ottieni tutti i turni della settimana per la sede
|
||||||
|
const weekShifts = await db
|
||||||
|
.select({
|
||||||
|
shift: shifts,
|
||||||
|
site: sites,
|
||||||
|
vehicle: vehicles,
|
||||||
|
})
|
||||||
|
.from(shifts)
|
||||||
|
.innerJoin(sites, eq(shifts.siteId, sites.id))
|
||||||
|
.leftJoin(vehicles, eq(shifts.vehicleId, vehicles.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(shifts.startTime, weekStartTimestamp),
|
||||||
|
lte(shifts.startTime, weekEndTimestamp),
|
||||||
|
ne(shifts.status, "cancelled"),
|
||||||
|
eq(sites.location, location as any)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 sito
|
||||||
|
const siteSchedules = activeSites.map((site: any) => {
|
||||||
|
// Trova turni del sito
|
||||||
|
const siteShifts = weekShifts.filter((s: any) => s.site.id === site.id);
|
||||||
|
|
||||||
|
// Costruisci lista turni con guardie e veicoli
|
||||||
|
const shifts = siteShifts.map((shiftData: any) => {
|
||||||
|
const shiftAssignments = assignments.filter((a: any) => a.assignment.shiftId === shiftData.shift.id);
|
||||||
|
|
||||||
|
const guards = shiftAssignments.map((a: any) => {
|
||||||
|
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; // Arrotonda a 1 decimale
|
||||||
|
|
||||||
|
return {
|
||||||
|
guardName: a.guard.fullName,
|
||||||
|
badgeNumber: a.guard.badgeNumber,
|
||||||
|
hours,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const shiftStart = new Date(shiftData.shift.startTime);
|
||||||
|
const shiftEnd = new Date(shiftData.shift.endTime);
|
||||||
|
const minutes = differenceInMinutes(shiftEnd, shiftStart);
|
||||||
|
const totalHours = Math.round((minutes / 60) * 10) / 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shiftId: shiftData.shift.id,
|
||||||
|
date: format(shiftStart, "yyyy-MM-dd"),
|
||||||
|
from: format(shiftStart, "HH:mm"),
|
||||||
|
to: format(shiftEnd, "HH:mm"),
|
||||||
|
guards,
|
||||||
|
vehicle: shiftData.vehicle ? {
|
||||||
|
licensePlate: shiftData.vehicle.licensePlate,
|
||||||
|
brand: shiftData.vehicle.brand,
|
||||||
|
model: shiftData.vehicle.model,
|
||||||
|
} : undefined,
|
||||||
|
totalGuards: guards.length,
|
||||||
|
totalHours,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalHours = Math.round(shifts.reduce((sum: number, s: any) => sum + s.totalHours, 0) * 10) / 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
siteId: site.id,
|
||||||
|
siteName: site.name,
|
||||||
|
location: site.location,
|
||||||
|
shifts,
|
||||||
|
totalShifts: shifts.length,
|
||||||
|
totalHours,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtra solo siti con turni programmati
|
||||||
|
const sitesWithShifts = siteSchedules.filter((s: any) => s.shifts.length > 0);
|
||||||
|
|
||||||
|
res.json(sitesWithShifts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching site schedules:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch site schedules", error: String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============= CERTIFICATION ROUTES =============
|
// ============= CERTIFICATION ROUTES =============
|
||||||
app.post("/api/certifications", isAuthenticated, async (req, res) => {
|
app.post("/api/certifications", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user