Improve planning accuracy and real-time updates for guard shifts

Fixes an issue where dates were incorrectly shifted due to timezone handling and ensures the guard assignment dialog updates in real-time after modifications. Additionally, refactors the calculation of needed guards to accurately reflect site service hours and minimum guard requirements.

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/TWQ52cO
This commit is contained in:
marco370 2025-10-22 09:12:20 +00:00
parent d8a6ec9c49
commit 3b7c55b55b
3 changed files with 54 additions and 32 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { format, startOfWeek, addWeeks } from "date-fns"; import { format, startOfWeek, addWeeks } from "date-fns";
import { it } from "date-fns/locale"; import { it } from "date-fns/locale";
@ -161,14 +161,35 @@ export default function GeneralPlanning() {
staleTime: 0, staleTime: 0,
}); });
// Effect per aggiornare selectedCell quando planningData cambia (per real-time update del dialog)
useEffect(() => {
if (selectedCell && planningData) {
// Trova la cella aggiornata nei nuovi dati
const day = planningData.days.find(d => d.date === selectedCell.date);
if (day) {
const updatedSite = day.sites.find(s => s.siteId === selectedCell.siteId);
if (updatedSite) {
setSelectedCell({
siteId: selectedCell.siteId,
siteName: selectedCell.siteName,
date: selectedCell.date,
data: updatedSite,
});
}
}
}
}, [planningData]);
// Mutation per eliminare assegnazione guardia // Mutation per eliminare assegnazione guardia
const deleteAssignmentMutation = useMutation({ const deleteAssignmentMutation = useMutation({
mutationFn: async (assignmentId: string) => { mutationFn: async (assignmentId: string) => {
return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, undefined); return apiRequest("DELETE", `/api/shift-assignments/${assignmentId}`, undefined);
}, },
onSuccess: () => { onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] }); // Invalida e refetch planning generale per aggiornare dialog
queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] }); await queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
await queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] });
await queryClient.refetchQueries({ queryKey: ["/api/general-planning"] });
toast({ toast({
title: "Guardia rimossa", title: "Guardia rimossa",

View File

@ -1026,32 +1026,33 @@ export async function registerRoutes(app: Express): Promise<Server> {
const maxOreGuardia = 9; // Max ore per guardia const maxOreGuardia = 9; // Max ore per guardia
const minGuardie = site.minGuards || 1; const minGuardie = site.minGuards || 1;
// Somma ore totali dei turni del giorno // Calcola ore servizio del sito (per calcolo corretto anche senza turni)
const serviceStart = site.serviceStartTime || "00:00";
const serviceEnd = site.serviceEndTime || "23:59";
const [startH, startM] = serviceStart.split(":").map(Number);
const [endH, endM] = serviceEnd.split(":").map(Number);
let serviceHours = (endH + endM/60) - (startH + startM/60);
if (serviceHours <= 0) serviceHours += 24; // Servizio notturno (es. 22:00-06:00)
// Somma ore totali dei turni del giorno (se esistono)
const totalShiftHours = dayShifts.reduce((sum: number, ds: any) => { const totalShiftHours = dayShifts.reduce((sum: number, ds: any) => {
const start = new Date(ds.shift.startTime); const start = new Date(ds.shift.startTime);
const end = new Date(ds.shift.endTime); const end = new Date(ds.shift.endTime);
return sum + differenceInHours(end, start); return sum + differenceInHours(end, start);
}, 0); }, 0);
// Usa ore servizio o ore turni (se già creati)
const effectiveHours = totalShiftHours > 0 ? totalShiftHours : serviceHours;
// Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni) // Guardie uniche assegnate (conta ogni guardia una volta anche se ha più turni)
const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size; const uniqueGuardsAssigned = new Set(guardsWithHours.map((g: any) => g.guardId)).size;
// Calcolo guardie necessarie e mancanti // Calcolo guardie necessarie basato su ore servizio
let totalGuardsNeeded: number; // Slot necessari per coprire le ore (ogni guardia max 9h)
let missingGuards: number; const slotsNeeded = Math.ceil(effectiveHours / maxOreGuardia);
// Guardie totali necessarie (slot × min guardie contemporanee)
if (totalShiftHours > 0) { const totalGuardsNeeded = slotsNeeded * minGuardie;
// Se ci sono turni: calcola basandosi sulle ore const missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
// Slot necessari per coprire le ore totali
const slotsNeeded = Math.ceil(totalShiftHours / maxOreGuardia);
// Guardie totali necessarie (slot × min guardie contemporanee)
totalGuardsNeeded = slotsNeeded * minGuardie;
missingGuards = Math.max(0, totalGuardsNeeded - uniqueGuardsAssigned);
} else {
// Se NON ci sono turni: serve almeno la copertura minima
totalGuardsNeeded = minGuardie;
missingGuards = minGuardie; // Tutte mancanti perché non ci sono turni
}
return { return {
siteId: site.id, siteId: site.id,
@ -1287,8 +1288,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
const actualMonth = baseDate.getMonth(); const actualMonth = baseDate.getMonth();
const actualDay = baseDate.getDate(); const actualDay = baseDate.getDate();
// Build dates in UTC to avoid timezone shifts // Build dates in LOCAL timezone to match user's selection
const shiftDate = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0)); const shiftDate = new Date(actualYear, actualMonth, actualDay, 0, 0, 0, 0);
// Check contract validity for this date // Check contract validity for this date
if (site.contractStartDate && site.contractEndDate) { if (site.contractStartDate && site.contractEndDate) {
@ -1301,13 +1302,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
} }
// Calculate planned start and end times in UTC // Calculate planned start and end times in LOCAL timezone
const plannedStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours, minutes, 0, 0)); const plannedStart = new Date(actualYear, actualMonth, actualDay, hours, minutes, 0, 0);
const plannedEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, hours + durationHours, minutes, 0, 0)); const plannedEnd = new Date(actualYear, actualMonth, actualDay, hours + durationHours, minutes, 0, 0);
// Find or create shift for this site/date (full day boundaries in UTC) // Find or create shift for this site/date (full day boundaries in LOCAL timezone)
const dayStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, 0, 0, 0, 0)); const dayStart = new Date(actualYear, actualMonth, actualDay, 0, 0, 0, 0);
const dayEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, 23, 59, 59, 999)); const dayEnd = new Date(actualYear, actualMonth, actualDay, 23, 59, 59, 999);
let existingShifts = await tx let existingShifts = await tx
.select() .select()
@ -1331,12 +1332,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
const [startHour, startMin] = serviceStart.split(":").map(Number); const [startHour, startMin] = serviceStart.split(":").map(Number);
const [endHour, endMin] = serviceEnd.split(":").map(Number); const [endHour, endMin] = serviceEnd.split(":").map(Number);
const shiftStart = new Date(Date.UTC(actualYear, actualMonth, actualDay, startHour, startMin, 0, 0)); const shiftStart = new Date(actualYear, actualMonth, actualDay, startHour, startMin, 0, 0);
let shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay, endHour, endMin, 0, 0)); let shiftEnd = new Date(actualYear, actualMonth, actualDay, endHour, endMin, 0, 0);
// If end time is before/equal to start time, shift extends to next day // If end time is before/equal to start time, shift extends to next day
if (shiftEnd <= shiftStart) { if (shiftEnd <= shiftStart) {
shiftEnd = new Date(Date.UTC(actualYear, actualMonth, actualDay + 1, endHour, endMin, 0, 0)); shiftEnd = new Date(actualYear, actualMonth, actualDay + 1, endHour, endMin, 0, 0);
} }
[shift] = await tx.insert(shifts).values({ [shift] = await tx.insert(shifts).values({