Compare commits

..

No commits in common. "f0c0321d1aa94d208a1dc7e7f04eeb669b16761e" and "0ab1a804ebb92d21b810264f2f08f087cce916c3" have entirely different histories.

8 changed files with 57 additions and 233 deletions

View File

@ -19,6 +19,10 @@ externalPort = 80
localPort = 33035 localPort = 33035
externalPort = 3001 externalPort = 3001
[[ports]]
localPort = 37125
externalPort = 4200
[[ports]] [[ports]]
localPort = 41343 localPort = 41343
externalPort = 3000 externalPort = 3000

View File

@ -9,8 +9,7 @@ import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Calendar, AlertCircle, CheckCircle2, Clock, MapPin, Users, Shield, Car as CarIcon, Building2 } from "lucide-react"; import { Calendar, AlertCircle, CheckCircle2, Clock, MapPin, Users, Shield, Car as CarIcon } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { format } from "date-fns"; import { format } from "date-fns";
import { it } from "date-fns/locale"; import { it } from "date-fns/locale";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@ -85,15 +84,8 @@ interface ResourcesData {
guards: Guard[]; guards: Guard[];
} }
const locationLabels: Record<string, string> = {
roccapiemonte: "Roccapiemonte",
milano: "Milano",
roma: "Roma",
};
export default function OperationalPlanning() { export default function OperationalPlanning() {
const { toast } = useToast(); const { toast } = useToast();
const [selectedLocation, setSelectedLocation] = useState<string>("roccapiemonte"); // Sede selezionata (primo step)
const [selectedDate, setSelectedDate] = useState<string>( const [selectedDate, setSelectedDate] = useState<string>(
format(new Date(), "yyyy-MM-dd") format(new Date(), "yyyy-MM-dd")
); );
@ -102,41 +94,18 @@ export default function OperationalPlanning() {
const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null); const [selectedVehicle, setSelectedVehicle] = useState<string | null>(null);
const [createShiftDialogOpen, setCreateShiftDialogOpen] = useState(false); const [createShiftDialogOpen, setCreateShiftDialogOpen] = useState(false);
// Query per siti non coperti (filtrati per sede e data) // Query per siti non coperti
const { data: uncoveredData, isLoading } = useQuery<UncoveredSitesData>({ const { data: uncoveredData, isLoading } = useQuery<UncoveredSitesData>({
queryKey: ['/api/operational-planning/uncovered-sites', selectedDate, selectedLocation], queryKey: [`/api/operational-planning/uncovered-sites?date=${selectedDate}`, selectedDate],
queryFn: async ({ queryKey }) => { enabled: !!selectedDate,
const [, date, location] = queryKey;
const res = await fetch(`/api/operational-planning/uncovered-sites?date=${date}&location=${location}`, {
credentials: 'include'
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return res.json();
},
enabled: !!selectedDate && !!selectedLocation,
}); });
// Query per risorse (veicoli e guardie) - solo quando c'è un sito selezionato // Query per risorse (veicoli e guardie) - solo quando c'è un sito selezionato
const { data: resourcesData, isLoading: isLoadingResources } = useQuery<ResourcesData>({ const { data: resourcesData, isLoading: isLoadingResources } = useQuery<ResourcesData>({
queryKey: ['/api/operational-planning/availability', selectedDate, selectedLocation, selectedSite?.id], queryKey: [`/api/operational-planning/availability?date=${selectedDate}`, selectedDate],
queryFn: async ({ queryKey }) => { enabled: !!selectedDate && !!selectedSite,
const [, date, location] = queryKey;
const res = await fetch(`/api/operational-planning/availability?date=${date}&location=${location}`, {
credentials: 'include'
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return res.json();
},
enabled: !!selectedDate && !!selectedLocation && !!selectedSite,
}); });
const handleLocationChange = (location: string) => {
setSelectedLocation(location);
setSelectedSite(null);
setSelectedGuards([]);
setSelectedVehicle(null);
};
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedDate(e.target.value); setSelectedDate(e.target.value);
setSelectedSite(null); setSelectedSite(null);
@ -257,25 +226,11 @@ export default function OperationalPlanning() {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" /> <Calendar className="h-5 w-5" />
Seleziona Sede e Data Seleziona Data
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex gap-4 items-end"> <div className="flex gap-4 items-end">
<div className="flex-1 max-w-xs">
<Label htmlFor="planning-location">Sede</Label>
<Select value={selectedLocation} onValueChange={handleLocationChange}>
<SelectTrigger id="planning-location" data-testid="select-planning-location" className="mt-1">
<Building2 className="h-4 w-4 mr-2" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex-1 max-w-xs"> <div className="flex-1 max-w-xs">
<Label htmlFor="planning-date">Data</Label> <Label htmlFor="planning-date">Data</Label>
<Input <Input
@ -468,12 +423,10 @@ export default function OperationalPlanning() {
{vehicle.brand} {vehicle.model} {vehicle.brand} {vehicle.model}
</p> </p>
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <Checkbox
<Checkbox checked={selectedVehicle === vehicle.id}
checked={selectedVehicle === vehicle.id} onCheckedChange={() => setSelectedVehicle(vehicle.id)}
onCheckedChange={() => setSelectedVehicle(vehicle.id)} />
/>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -528,12 +481,10 @@ export default function OperationalPlanning() {
Ore sett.: {guard.availability.weeklyHours}h | Rimaste: {guard.availability.remainingWeeklyHours}h Ore sett.: {guard.availability.weeklyHours}h | Rimaste: {guard.availability.remainingWeeklyHours}h
</p> </p>
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <Checkbox
<Checkbox checked={selectedGuards.includes(guard.id)}
checked={selectedGuards.includes(guard.id)} onCheckedChange={() => toggleGuardSelection(guard.id)}
onCheckedChange={() => toggleGuardSelection(guard.id)} />
/>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -11,7 +11,7 @@ import { Switch } from "@/components/ui/switch";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { insertSiteSchema } from "@shared/schema"; import { insertSiteSchema } from "@shared/schema";
import { Plus, MapPin, Shield, Users, Pencil, Building2 } from "lucide-react"; import { Plus, MapPin, Shield, Users, Pencil } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { StatusBadge } from "@/components/status-badge"; import { StatusBadge } from "@/components/status-badge";
@ -25,12 +25,6 @@ const shiftTypeLabels: Record<string, string> = {
quick_response: "Pronto Intervento", quick_response: "Pronto Intervento",
}; };
const locationLabels: Record<string, string> = {
roccapiemonte: "Roccapiemonte",
milano: "Milano",
roma: "Roma",
};
export default function Sites() { export default function Sites() {
const { toast } = useToast(); const { toast } = useToast();
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
@ -135,7 +129,6 @@ export default function Sites() {
editForm.reset({ editForm.reset({
name: site.name, name: site.name,
address: site.address, address: site.address,
location: site.location,
shiftType: site.shiftType, shiftType: site.shiftType,
minGuards: site.minGuards, minGuards: site.minGuards,
requiresArmed: site.requiresArmed, requiresArmed: site.requiresArmed,
@ -228,29 +221,6 @@ export default function Sites() {
)} )}
/> />
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede Gestionale</FormLabel>
<Select onValueChange={field.onChange} value={field.value || "roccapiemonte"}>
<FormControl>
<SelectTrigger data-testid="select-location">
<SelectValue placeholder="Seleziona sede" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="border-t pt-4 space-y-4"> <div className="border-t pt-4 space-y-4">
<p className="text-sm font-medium">Dati Contrattuali</p> <p className="text-sm font-medium">Dati Contrattuali</p>
@ -489,29 +459,6 @@ export default function Sites() {
)} )}
/> />
<FormField
control={editForm.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede Gestionale</FormLabel>
<Select onValueChange={field.onChange} value={field.value || "roccapiemonte"}>
<FormControl>
<SelectTrigger data-testid="select-edit-location">
<SelectValue placeholder="Seleziona sede" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="border-t pt-4 space-y-4"> <div className="border-t pt-4 space-y-4">
<p className="text-sm font-medium">Dati Contrattuali</p> <p className="text-sm font-medium">Dati Contrattuali</p>
@ -737,15 +684,9 @@ export default function Sites() {
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<CardTitle className="text-lg truncate">{site.name}</CardTitle> <CardTitle className="text-lg truncate">{site.name}</CardTitle>
<CardDescription className="text-xs mt-1 space-y-0.5"> <CardDescription className="text-xs mt-1">
<div> <MapPin className="h-3 w-3 inline mr-1" />
<MapPin className="h-3 w-3 inline mr-1" /> {site.address}
{site.address}
</div>
<div>
<Building2 className="h-3 w-3 inline mr-1" />
Sede: {locationLabels[site.location]}
</div>
</CardDescription> </CardDescription>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -35,23 +35,6 @@ The database includes core tables for `users`, `guards`, `certifications`, `site
- Sites include service schedule fields: `serviceStartTime` and `serviceEndTime` (formato HH:MM) - Sites include service schedule fields: `serviceStartTime` and `serviceEndTime` (formato HH:MM)
- **Contract Management**: Sites now include contract fields: `contractReference` (codice contratto), `contractStartDate`, `contractEndDate` (date validità contratto in formato YYYY-MM-DD) - **Contract Management**: Sites now include contract fields: `contractReference` (codice contratto), `contractStartDate`, `contractEndDate` (date validità contratto in formato YYYY-MM-DD)
- Sites now reference service types via `serviceTypeId` foreign key; `shiftType` is optional and can be derived from service type - Sites now reference service types via `serviceTypeId` foreign key; `shiftType` is optional and can be derived from service type
- **Multi-Location Support**: Added `location` field (enum: roccapiemonte, milano, roma) to `sites`, `guards`, and `vehicles` tables for complete multi-sede resource isolation
**Recent Features (October 17, 2025)**:
- **Multi-Sede Operational Planning**: Redesigned operational planning workflow with location-first approach:
1. Select sede (Roccapiemonte/Milano/Roma) - first step with default value
2. Select date
3. View uncovered sites filtered by selected sede
4. Select site → view available resources (guards and vehicles) filtered by sede
5. Assign resources and create shift
- **Location-Based Filtering**: Backend endpoints use INNER JOIN with sites table to ensure complete resource isolation between locations - guards/vehicles in one sede remain available even when assigned to shifts in other sedi
- **Site Management**: Added sede selection in site creation/editing forms with visual badges showing location in site listings
**Recent Bug Fixes (October 17, 2025)**:
- **Operational Planning Date Handling**: Fixed date sanitization in `/api/operational-planning/uncovered-sites` and `/api/operational-planning/availability` endpoints to handle malformed date inputs (e.g., "2025-10-17/2025-10-17"). Both endpoints now validate dates using `parseISO`/`isValid` and return 400 for invalid formats.
- **Checkbox Event Propagation**: Fixed double-toggle bug in operational planning resource selection by wrapping vehicle and guard checkboxes in `<div onClick={e => e.stopPropagation()}>` to prevent Card onClick from firing when clicking checkboxes.
- **Multi-Sede Resource Isolation**: Fixed critical bug where resources from different sedi were incorrectly marked as unavailable due to global shift queries. Now both availability and uncovered-sites endpoints filter shifts by location using JOIN with sites table.
- **QueryKey Cache Invalidation**: Fixed queryKey structure from single-string to hierarchical array with custom queryFn to enable targeted cache invalidation by location and date while preventing URL concatenation errors.
### API Endpoints ### API Endpoints
Comprehensive RESTful API endpoints are provided for Authentication, Users, Guards, Sites, Shifts, and Notifications, supporting full CRUD operations with role-based access control. Comprehensive RESTful API endpoints are provided for Authentication, Users, Guards, Sites, Shifts, and Notifications, supporting full CRUD operations with role-based access control.
@ -68,12 +51,11 @@ Key frontend routes include `/`, `/guards`, `/sites`, `/shifts`, `/reports`, `/n
### Key Features ### Key Features
- **Dashboard Operativa**: Live KPIs (active shifts, total guards, active sites, expiring certifications) and real-time shift status. - **Dashboard Operativa**: Live KPIs (active shifts, total guards, active sites, expiring certifications) and real-time shift status.
- **Gestione Guardie**: Complete profiles with skill matrix (armed, fire safety, first aid, driver's license), certification management with automatic expiry, and unique badge numbers. - **Gestione Guardie**: Complete profiles with skill matrix (armed, fire safety, first aid, driver's license), certification management with automatic expiry, and unique badge numbers.
- **Gestione Siti/Commesse**: Service types with specialized parameters (fixed post hours, patrol passages, inspection frequency, response time) and minimum requirements (guard count, armed, driver's license). Sites include service schedule (start/end time), contract management (reference code, validity period with start/end dates), and location/sede assignment. Contract status is visualized with badges (active/expiring/expired) and enforces shift creation only within active contract periods. - **Gestione Siti/Commesse**: Service types with specialized parameters (fixed post hours, patrol passages, inspection frequency, response time) and minimum requirements (guard count, armed, driver's license). Sites include service schedule (start/end time) and contract management (reference code, validity period with start/end dates). Contract status is visualized with badges (active/expiring/expired) and enforces shift creation only within active contract periods.
- **Pianificazione Operativa Multi-Sede**: Location-aware workflow for shift assignment: - **Pianificazione Operativa Interattiva**: Three-step workflow for shift assignment:
1. Select sede (Roccapiemonte/Milano/Roma) → filters all subsequent data by location 1. Select date → view uncovered sites with coverage status
2. Select date → view uncovered sites with coverage status (sede-filtered) 2. Select site → view filtered resources (guards and vehicles matching requirements)
3. Select site → view available resources (guards and vehicles matching sede and requirements) 3. Assign resources → create shift with atomic guard assignments and vehicle allocation
4. Assign resources → create shift with atomic guard assignments and vehicle allocation
- **Pianificazione Turni**: 24/7 calendar, manual guard assignment, basic constraints, and shift statuses (planned, active, completed, cancelled). - **Pianificazione Turni**: 24/7 calendar, manual guard assignment, basic constraints, and shift statuses (planned, active, completed, cancelled).
- **Reportistica**: Total hours worked, monthly hours per guard, shift statistics, and data export capabilities. - **Reportistica**: Total hours worked, monthly hours per guard, shift statistics, and data export capabilities.
- **Advanced Planning**: Management of guard constraints (preferences, max hours, rest days), site preferences (preferred/blacklisted guards), contract parameters, training courses, holidays, and absences with substitution system. - **Advanced Planning**: Management of guard constraints (preferences, max hours, rest days), site preferences (preferred/blacklisted guards), contract parameters, training courses, holidays, and absences with substitution system.
@ -90,4 +72,4 @@ Key frontend routes include `/`, `/guards`, `/sites`, `/shifts`, `/reports`, `/n
- **PM2**: Production process manager for Node.js applications. - **PM2**: Production process manager for Node.js applications.
- **Nginx**: As a reverse proxy for the production environment. - **Nginx**: As a reverse proxy for the production environment.
- **Let's Encrypt**: For SSL/TLS certificates. - **Let's Encrypt**: For SSL/TLS certificates.
- **GitLab CI/CD**: For continuous integration and deployment. - **GitLab CI/CD**: For continuous integration and deployment.

View File

@ -6,7 +6,7 @@ import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./local
import { db } from "./db"; import { db } from "./db";
import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters } from "@shared/schema"; import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters } from "@shared/schema";
import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm"; import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm";
import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid } from "date-fns"; import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format } from "date-fns";
// Determina quale sistema auth usare basandosi sull'ambiente // Determina quale sistema auth usare basandosi sull'ambiente
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS; const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
@ -538,84 +538,63 @@ export async function registerRoutes(app: Express): Promise<Server> {
app.get("/api/operational-planning/availability", isAuthenticated, async (req, res) => { app.get("/api/operational-planning/availability", isAuthenticated, async (req, res) => {
try { try {
const { getGuardAvailabilityReport } = await import("./ccnlRules"); const { getGuardAvailabilityReport } = await import("./ccnlRules");
const dateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd");
// Sanitizza input: gestisce sia "2025-10-17" che "2025-10-17/2025-10-17" const date = new Date(dateStr + "T00:00:00.000Z");
const rawDateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd");
const normalizedDateStr = rawDateStr.split("/")[0]; // Prende solo la prima parte se c'è uno slash
// Valida la data
const parsedDate = parseISO(normalizedDateStr);
if (!isValid(parsedDate)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
}
const dateStr = format(parsedDate, "yyyy-MM-dd");
// Ottieni location dalla query (default: roccapiemonte)
const location = req.query.location as string || "roccapiemonte";
// Imposta inizio e fine giornata in UTC // Imposta inizio e fine giornata in UTC
const startOfDay = new Date(dateStr + "T00:00:00.000Z"); const startOfDay = new Date(dateStr + "T00:00:00.000Z");
const endOfDay = new Date(dateStr + "T23:59:59.999Z"); const endOfDay = new Date(dateStr + "T23:59:59.999Z");
// Ottieni tutti i veicoli della sede selezionata // Ottieni tutti i veicoli
const allVehicles = await storage.getAllVehicles(); const allVehicles = await storage.getAllVehicles();
const locationVehicles = allVehicles.filter(v => v.location === location);
// Ottieni turni del giorno SOLO della sede selezionata (join con sites per filtrare per location) // Ottieni turni del giorno per trovare veicoli assegnati
const dayShifts = await db const dayShifts = await db
.select({ .select()
shift: shifts
})
.from(shifts) .from(shifts)
.innerJoin(sites, eq(shifts.siteId, sites.id))
.where( .where(
and( and(
gte(shifts.startTime, startOfDay), gte(shifts.startTime, startOfDay),
lte(shifts.startTime, endOfDay), lte(shifts.startTime, endOfDay)
eq(sites.location, location)
) )
); );
// Mappa veicoli con disponibilità // Mappa veicoli con disponibilità
const vehiclesWithAvailability = await Promise.all( const vehiclesWithAvailability = await Promise.all(
locationVehicles.map(async (vehicle) => { allVehicles.map(async (vehicle) => {
const assignedShiftRecord = dayShifts.find((s: any) => s.shift.vehicleId === vehicle.id); const assignedShift = dayShifts.find((shift: any) => shift.vehicleId === vehicle.id);
return { return {
...vehicle, ...vehicle,
isAvailable: !assignedShiftRecord, isAvailable: !assignedShift,
assignedShift: assignedShiftRecord ? { assignedShift: assignedShift ? {
id: assignedShiftRecord.shift.id, id: assignedShift.id,
startTime: assignedShiftRecord.shift.startTime, startTime: assignedShift.startTime,
endTime: assignedShiftRecord.shift.endTime, endTime: assignedShift.endTime,
siteId: assignedShiftRecord.shift.siteId siteId: assignedShift.siteId
} : null } : null
}; };
}) })
); );
// Ottieni tutte le guardie della sede selezionata // Ottieni tutte le guardie
const allGuards = await storage.getAllGuards(); const allGuards = await storage.getAllGuards();
const locationGuards = allGuards.filter(g => g.location === location);
// Ottieni assegnazioni turni del giorno SOLO della sede selezionata (join con sites per filtrare per location) // Ottieni assegnazioni turni del giorno
const dayShiftAssignments = await db const dayShiftAssignments = await db
.select() .select()
.from(shiftAssignments) .from(shiftAssignments)
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
.innerJoin(sites, eq(shifts.siteId, sites.id))
.where( .where(
and( and(
gte(shifts.startTime, startOfDay), gte(shifts.startTime, startOfDay),
lte(shifts.startTime, endOfDay), lte(shifts.startTime, endOfDay)
eq(sites.location, location)
) )
); );
// Calcola disponibilità agenti con report CCNL // Calcola disponibilità agenti con report CCNL
const guardsWithAvailability = await Promise.all( const guardsWithAvailability = await Promise.all(
locationGuards.map(async (guard) => { allGuards.map(async (guard) => {
const assignedShift = dayShiftAssignments.find( const assignedShift = dayShiftAssignments.find(
(assignment: any) => assignment.shift_assignments.guardId === guard.id (assignment: any) => assignment.shift_assignments.guardId === guard.id
); );
@ -675,34 +654,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
// Endpoint per ottenere siti non completamente coperti per una data e sede // Endpoint per ottenere siti non completamente coperti per una data
app.get("/api/operational-planning/uncovered-sites", isAuthenticated, async (req, res) => { app.get("/api/operational-planning/uncovered-sites", isAuthenticated, async (req, res) => {
try { try {
// Sanitizza input: gestisce sia "2025-10-17" che "2025-10-17/2025-10-17" const dateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd");
const rawDateStr = req.query.date as string || format(new Date(), "yyyy-MM-dd");
const normalizedDateStr = rawDateStr.split("/")[0]; // Prende solo la prima parte se c'è uno slash
// Valida la data // Ottieni tutti i siti attivi
const parsedDate = parseISO(normalizedDateStr);
if (!isValid(parsedDate)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
}
const dateStr = format(parsedDate, "yyyy-MM-dd");
// Ottieni location dalla query (default: roccapiemonte)
const location = req.query.location as string || "roccapiemonte";
// Ottieni tutti i siti attivi della sede selezionata
const allSites = await db const allSites = await db
.select() .select()
.from(sites) .from(sites)
.where( .where(eq(sites.isActive, true));
and(
eq(sites.isActive, true),
eq(sites.location, location)
)
);
// Filtra siti con contratto valido per la data selezionata // Filtra siti con contratto valido per la data selezionata
const sitesWithValidContract = allSites.filter((site: any) => { const sitesWithValidContract = allSites.filter((site: any) => {
@ -725,27 +686,18 @@ export async function registerRoutes(app: Express): Promise<Server> {
return selectedDate >= contractStart && selectedDate <= contractEnd; return selectedDate >= contractStart && selectedDate <= contractEnd;
}); });
// Ottieni turni del giorno con assegnazioni SOLO della sede selezionata // Ottieni turni del giorno con assegnazioni (usando SQL date comparison)
const startOfDayDate = new Date(dateStr);
startOfDayDate.setHours(0, 0, 0, 0);
const endOfDayDate = new Date(dateStr);
endOfDayDate.setHours(23, 59, 59, 999);
const dayShifts = await db const dayShifts = await db
.select({ .select({
shift: shifts, shift: shifts,
assignmentCount: sql<number>`count(${shiftAssignments.id})::int` assignmentCount: sql<number>`count(${shiftAssignments.id})::int`
}) })
.from(shifts) .from(shifts)
.innerJoin(sites, eq(shifts.siteId, sites.id))
.leftJoin(shiftAssignments, eq(shifts.id, shiftAssignments.shiftId)) .leftJoin(shiftAssignments, eq(shifts.id, shiftAssignments.shiftId))
.where( .where(
and( and(
gte(shifts.startTime, startOfDayDate), sql`DATE(${shifts.startTime}) = ${dateStr}`,
lte(shifts.startTime, endOfDayDate), ne(shifts.status, "cancelled")
ne(shifts.status, "cancelled"),
eq(sites.location, location)
) )
) )
.groupBy(shifts.id); .groupBy(shifts.id);

View File

@ -1,13 +1,7 @@
{ {
"version": "1.0.19", "version": "1.0.18",
"lastUpdate": "2025-10-17T17:24:45.675Z", "lastUpdate": "2025-10-17T15:56:56.628Z",
"changelog": [ "changelog": [
{
"version": "1.0.19",
"date": "2025-10-17",
"type": "patch",
"description": "Deployment automatico v1.0.19"
},
{ {
"version": "1.0.18", "version": "1.0.18",
"date": "2025-10-17", "date": "2025-10-17",