Add mobile planning interface and backend endpoints
Introduce a new "Planning Mobile" section to the application, including a frontend page (client/src/pages/planning-mobile.tsx) for managing mobile services (patrols, inspections, interventions) and backend API routes (server/routes.ts) to fetch relevant sites and guard availability based on location and date. This also includes updates to the app sidebar (client/src/components/app-sidebar.tsx) and router (client/src/App.tsx) to integrate the new functionality. 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/1nTItRR
This commit is contained in:
parent
c5e4c66815
commit
bd4a55e001
4
.replit
4
.replit
@ -19,6 +19,10 @@ externalPort = 80
|
|||||||
localPort = 33035
|
localPort = 33035
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 38781
|
||||||
|
externalPort = 5173
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41295
|
localPort = 41295
|
||||||
externalPort = 6000
|
externalPort = 6000
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import OperationalPlanning from "@/pages/operational-planning";
|
|||||||
import GeneralPlanning from "@/pages/general-planning";
|
import GeneralPlanning from "@/pages/general-planning";
|
||||||
import ServicePlanning from "@/pages/service-planning";
|
import ServicePlanning from "@/pages/service-planning";
|
||||||
import Customers from "@/pages/customers";
|
import Customers from "@/pages/customers";
|
||||||
|
import PlanningMobile from "@/pages/planning-mobile";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
@ -48,6 +49,7 @@ function Router() {
|
|||||||
<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="/service-planning" component={ServicePlanning} />
|
||||||
|
<Route path="/planning-mobile" component={PlanningMobile} />
|
||||||
<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} />
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
ClipboardList,
|
ClipboardList,
|
||||||
Car,
|
Car,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
|
Navigation,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import {
|
import {
|
||||||
@ -61,6 +62,12 @@ const menuItems = [
|
|||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Planning Mobile",
|
||||||
|
url: "/planning-mobile",
|
||||||
|
icon: Navigation,
|
||||||
|
roles: ["admin", "coordinator"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Planning di Servizio",
|
title: "Planning di Servizio",
|
||||||
url: "/service-planning",
|
url: "/service-planning",
|
||||||
|
|||||||
284
client/src/pages/planning-mobile.tsx
Normal file
284
client/src/pages/planning-mobile.tsx
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Calendar, MapPin, User, Car, Clock } from "lucide-react";
|
||||||
|
import { format, parseISO, isValid } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
|
||||||
|
type Location = "roccapiemonte" | "milano" | "roma";
|
||||||
|
|
||||||
|
type MobileSite = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
serviceTypeId: string;
|
||||||
|
serviceTypeName: string;
|
||||||
|
location: Location;
|
||||||
|
latitude: number | null;
|
||||||
|
longitude: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AvailableGuard = {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
badgeNumber: string;
|
||||||
|
location: Location;
|
||||||
|
weeklyHours: number;
|
||||||
|
availableHours: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PlanningMobile() {
|
||||||
|
const [selectedDate, setSelectedDate] = useState(format(new Date(), "yyyy-MM-dd"));
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
|
||||||
|
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
||||||
|
|
||||||
|
// Query siti mobile per location
|
||||||
|
const { data: mobileSites, isLoading: sitesLoading } = useQuery<MobileSite[]>({
|
||||||
|
queryKey: ["/api/planning-mobile/sites", selectedLocation],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/planning-mobile/sites?location=${selectedLocation}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch mobile sites");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedLocation,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query guardie disponibili per location e data
|
||||||
|
const { data: availableGuards, isLoading: guardsLoading } = useQuery<AvailableGuard[]>({
|
||||||
|
queryKey: ["/api/planning-mobile/guards", selectedLocation, selectedDate],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/planning-mobile/guards?location=${selectedLocation}&date=${selectedDate}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch available guards");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedLocation && !!selectedDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationLabels: Record<Location, string> = {
|
||||||
|
roccapiemonte: "Roccapiemonte",
|
||||||
|
milano: "Milano",
|
||||||
|
roma: "Roma",
|
||||||
|
};
|
||||||
|
|
||||||
|
const locationColors: Record<Location, string> = {
|
||||||
|
roccapiemonte: "bg-blue-500",
|
||||||
|
milano: "bg-green-500",
|
||||||
|
roma: "bg-purple-500",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-3xl font-bold">Planning Mobile</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Pianificazione ronde, ispezioni e interventi notturni per servizi mobili
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtri */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<MapPin className="h-5 w-5" />
|
||||||
|
Filtri Pianificazione
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Seleziona sede, data e guardia per iniziare</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="location-select">Sede*</Label>
|
||||||
|
<Select value={selectedLocation} onValueChange={(v) => setSelectedLocation(v as Location)}>
|
||||||
|
<SelectTrigger id="location-select" data-testid="select-mobile-location">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
||||||
|
<SelectItem value="milano">Milano</SelectItem>
|
||||||
|
<SelectItem value="roma">Roma</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="date-select">Data*</Label>
|
||||||
|
<Input
|
||||||
|
id="date-select"
|
||||||
|
type="date"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={(e) => setSelectedDate(e.target.value)}
|
||||||
|
data-testid="input-mobile-date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="guard-select">Guardia (opzionale)</Label>
|
||||||
|
<Select value={selectedGuardId} onValueChange={setSelectedGuardId}>
|
||||||
|
<SelectTrigger id="guard-select" data-testid="select-mobile-guard">
|
||||||
|
<SelectValue placeholder="Tutte le guardie" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="">Tutte le guardie</SelectItem>
|
||||||
|
{availableGuards?.map((guard) => (
|
||||||
|
<SelectItem key={guard.id} value={guard.id}>
|
||||||
|
{guard.firstName} {guard.lastName} - #{guard.badgeNumber} ({guard.availableHours}h disponibili)
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Grid: Mappa + Siti */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 flex-1">
|
||||||
|
{/* Mappa Siti */}
|
||||||
|
<Card className="flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<MapPin className="h-5 w-5" />
|
||||||
|
Mappa Siti Mobile
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{mobileSites?.length || 0} siti con servizi mobili in {locationLabels[selectedLocation]}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 flex items-center justify-center bg-muted/20 rounded-lg">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Integrazione mappa in sviluppo
|
||||||
|
<br />
|
||||||
|
(Leaflet/Google Maps)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Lista Siti Mobile */}
|
||||||
|
<Card className="flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Clock className="h-5 w-5" />
|
||||||
|
Siti con Servizi Mobili
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Ronde notturne, ispezioni, interventi programmati
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 overflow-y-auto">
|
||||||
|
{sitesLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Caricamento...</p>
|
||||||
|
) : mobileSites && mobileSites.length > 0 ? (
|
||||||
|
mobileSites.map((site) => (
|
||||||
|
<div
|
||||||
|
key={site.id}
|
||||||
|
className="p-4 border rounded-lg space-y-2 hover-elevate cursor-pointer"
|
||||||
|
data-testid={`site-card-${site.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="space-y-1 flex-1">
|
||||||
|
<h4 className="font-semibold">{site.name}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
|
<MapPin className="h-3 w-3" />
|
||||||
|
{site.address}, {site.city}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge className={locationColors[site.location]} data-testid={`badge-location-${site.id}`}>
|
||||||
|
{locationLabels[site.location]}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Badge variant="outline" data-testid={`badge-service-${site.id}`}>
|
||||||
|
{site.serviceTypeName}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button size="sm" variant="default" data-testid={`button-assign-${site.id}`}>
|
||||||
|
Assegna Guardia
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" data-testid={`button-view-${site.id}`}>
|
||||||
|
Dettagli
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 space-y-2">
|
||||||
|
<MapPin className="h-12 w-12 mx-auto text-muted-foreground" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Nessun sito con servizi mobili in {locationLabels[selectedLocation]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Guardie Disponibili */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<User className="h-5 w-5" />
|
||||||
|
Guardie Disponibili ({availableGuards?.length || 0})
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Guardie con ore disponibili per {format(parseISO(selectedDate), "dd MMMM yyyy", { locale: it })}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{guardsLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground col-span-full">Caricamento...</p>
|
||||||
|
) : availableGuards && availableGuards.length > 0 ? (
|
||||||
|
availableGuards.map((guard) => (
|
||||||
|
<div
|
||||||
|
key={guard.id}
|
||||||
|
className="p-3 border rounded-lg space-y-2"
|
||||||
|
data-testid={`guard-card-${guard.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h5 className="font-semibold text-sm">
|
||||||
|
{guard.firstName} {guard.lastName}
|
||||||
|
</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">#{guard.badgeNumber}</p>
|
||||||
|
</div>
|
||||||
|
<Badge className={locationColors[guard.location]}>
|
||||||
|
{locationLabels[guard.location]}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Ore settimanali:</span>
|
||||||
|
<span className="font-medium">{guard.weeklyHours}h / 45h</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Disponibili:</span>
|
||||||
|
<span className="font-medium text-green-600">{guard.availableHours}h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground col-span-full text-center py-4">
|
||||||
|
Nessuna guardia disponibile per la data selezionata
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
server/routes.ts
129
server/routes.ts
@ -3175,6 +3175,135 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============= PLANNING MOBILE ROUTES =============
|
||||||
|
// GET /api/planning-mobile/sites?location=X - Siti con servizi mobili (ronde/ispezioni/interventi)
|
||||||
|
app.get("/api/planning-mobile/sites", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { location } = req.query;
|
||||||
|
|
||||||
|
if (!location || !["roccapiemonte", "milano", "roma"].includes(location as string)) {
|
||||||
|
return res.status(400).json({ message: "Location parameter required (roccapiemonte|milano|roma)" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query siti con serviceType.classification = 'mobile' e location matching
|
||||||
|
const mobileSites = await db
|
||||||
|
.select({
|
||||||
|
id: sites.id,
|
||||||
|
name: sites.name,
|
||||||
|
address: sites.address,
|
||||||
|
city: sites.city,
|
||||||
|
serviceTypeId: sites.serviceTypeId,
|
||||||
|
serviceTypeName: serviceTypes.label,
|
||||||
|
location: sites.location,
|
||||||
|
latitude: sites.latitude,
|
||||||
|
longitude: sites.longitude,
|
||||||
|
})
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.location, location as "roccapiemonte" | "milano" | "roma"),
|
||||||
|
eq(serviceTypes.classification, "mobile"),
|
||||||
|
eq(sites.isActive, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(sites.name);
|
||||||
|
|
||||||
|
res.json(mobileSites);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching mobile sites:", error);
|
||||||
|
res.status(500).json({ message: "Errore caricamento siti mobili" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/planning-mobile/guards?location=X&date=YYYY-MM-DD - Guardie disponibili per location e data
|
||||||
|
app.get("/api/planning-mobile/guards", isAuthenticated, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { location, date } = req.query;
|
||||||
|
|
||||||
|
if (!location || !["roccapiemonte", "milano", "roma"].includes(location as string)) {
|
||||||
|
return res.status(400).json({ message: "Location parameter required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!date || typeof date !== "string") {
|
||||||
|
return res.status(400).json({ message: "Date parameter required (YYYY-MM-DD)" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 location
|
||||||
|
const allGuards = await db
|
||||||
|
.select()
|
||||||
|
.from(guards)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(guards.location, location as "roccapiemonte" | "milano" | "roma"),
|
||||||
|
eq(guards.isActive, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(guards.lastName, guards.firstName);
|
||||||
|
|
||||||
|
// Calcola settimana corrente per calcolare ore settimanali
|
||||||
|
const [year, month, day] = date.split("-").map(Number);
|
||||||
|
const targetDate = new Date(year, month - 1, day);
|
||||||
|
const weekStart = startOfWeek(targetDate, { weekStartsOn: 1 }); // lunedì
|
||||||
|
const weekEnd = endOfWeek(targetDate, { weekStartsOn: 1 });
|
||||||
|
|
||||||
|
// Per ogni guardia, calcola ore già assegnate nella settimana
|
||||||
|
const guardsWithAvailability = await Promise.all(
|
||||||
|
allGuards.map(async (guard) => {
|
||||||
|
// Query shifts assegnati alla guardia nella settimana
|
||||||
|
const weekShifts = await db
|
||||||
|
.select({
|
||||||
|
shiftId: shifts.id,
|
||||||
|
startTime: shifts.startTime,
|
||||||
|
endTime: shifts.endTime,
|
||||||
|
})
|
||||||
|
.from(shiftAssignments)
|
||||||
|
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(shiftAssignments.guardId, guard.id),
|
||||||
|
gte(shifts.startTime, weekStart),
|
||||||
|
lte(shifts.startTime, weekEnd)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calcola ore totali nella settimana
|
||||||
|
const weeklyHours = weekShifts.reduce((total, shift) => {
|
||||||
|
const hours = differenceInHours(new Date(shift.endTime), new Date(shift.startTime));
|
||||||
|
return total + hours;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const maxWeeklyHours = 45; // CCNL limit
|
||||||
|
const availableHours = Math.max(0, maxWeeklyHours - weeklyHours);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: guard.id,
|
||||||
|
firstName: guard.firstName,
|
||||||
|
lastName: guard.lastName,
|
||||||
|
badgeNumber: guard.badgeNumber,
|
||||||
|
location: guard.location,
|
||||||
|
weeklyHours,
|
||||||
|
availableHours,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filtra solo guardie con ore disponibili
|
||||||
|
const availableGuards = guardsWithAvailability.filter(g => g.availableHours > 0);
|
||||||
|
|
||||||
|
res.json(availableGuards);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching available guards:", error);
|
||||||
|
res.status(500).json({ message: "Errore caricamento guardie disponibili" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user