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:
parent
d8a6ec9c49
commit
3b7c55b55b
BIN
attached_assets/immagine_1761123543970.png
Normal file
BIN
attached_assets/immagine_1761123543970.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@ -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",
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user