Fix date shifting issues for shift assignments

Update date handling logic in `server/routes.ts` to prevent timezone-related shifts when assigning shifts, by parsing dates using components instead of ISO strings. Documentation in `replit.md` has also been updated with new rules to avoid this recurring problem.

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/2w7P7NW
This commit is contained in:
marco370 2025-10-23 08:15:59 +00:00
parent 1598eb208b
commit 983adcfbe1
3 changed files with 59 additions and 16 deletions

View File

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

View File

@ -10,6 +10,43 @@ VigilanzaTurni is a professional 24/7 shift management system designed for secur
- Focus su efficienza e densità informativa - Focus su efficienza e densità informativa
- **Testing**: Tutti i test vengono eseguiti ESCLUSIVAMENTE sul server esterno (vt.alfacom.it) con autenticazione locale (non Replit Auth) - **Testing**: Tutti i test vengono eseguiti ESCLUSIVAMENTE sul server esterno (vt.alfacom.it) con autenticazione locale (non Replit Auth)
## ⚠️ CRITICAL: Date/Timezone Handling Rules
**PROBLEMA RICORRENTE**: Quando si assegna una guardia per il giorno X, appare assegnata al giorno X±1 a causa di conversioni timezone.
**REGOLE OBBLIGATORIE** per evitare questo bug:
1. **MAI usare `parseISO()` su date YYYY-MM-DD**
- ❌ SBAGLIATO: `const date = parseISO("2025-10-20")` → converte in UTC causando shift
- ✅ CORRETTO: `const [y, m, d] = "2025-10-20".split("-").map(Number); const date = new Date(y, m-1, d)`
2. **Costruire Date da componenti, NON da stringhe ISO**
```typescript
// ✅ CORRETTO - date components (no timezone conversion)
const [year, month, day] = startDate.split("-").map(Number);
const shiftDate = new Date(year, month - 1, day);
const shiftStart = new Date(year, month - 1, day, startHour, startMin, 0, 0);
// ❌ SBAGLIATO - parseISO o new Date(string ISO)
const date = parseISO(startDate); // converte in UTC!
const date = new Date("2025-10-20"); // timezone-dependent!
```
3. **Validazione date: usare regex, NON parseISO**
```typescript
// ✅ CORRETTO
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(dateStr)) { /* invalid */ }
// ❌ SBAGLIATO
const parsed = parseISO(dateStr);
if (!isValid(parsed)) { /* invalid */ }
```
4. **File da verificare sempre**: `server/routes.ts` - tutte le route che ricevono date dal frontend
5. **Testare sempre**: Assegnare guardia giorno X → verificare appaia nel giorno X (non X±1)
**RIFERIMENTI FIX**: Vedere commit "Fix timezone bug in shift creation" - linee 1148-1184, 615-621, 753-759 in server/routes.ts
## System Architecture ## System Architecture
### Stack Tecnologico ### Stack Tecnologico

View File

@ -609,13 +609,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
const rawDateStr = 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 const normalizedDateStr = rawDateStr.split("/")[0]; // Prende solo la prima parte se c'è uno slash
// Valida la data // TIMEZONE FIX: Valida formato senza parseISO per evitare shift timezone
const parsedDate = parseISO(normalizedDateStr); const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!isValid(parsedDate)) { if (!dateRegex.test(normalizedDateStr)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" }); return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
} }
const dateStr = format(parsedDate, "yyyy-MM-dd"); const dateStr = normalizedDateStr;
// Ottieni location dalla query (default: roccapiemonte) // Ottieni location dalla query (default: roccapiemonte)
const location = req.query.location as string || "roccapiemonte"; const location = req.query.location as string || "roccapiemonte";
@ -748,13 +748,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
const rawDateStr = 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 const normalizedDateStr = rawDateStr.split("/")[0]; // Prende solo la prima parte se c'è uno slash
// Valida la data // TIMEZONE FIX: Valida formato senza parseISO per evitare shift timezone
const parsedDate = parseISO(normalizedDateStr); const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!isValid(parsedDate)) { if (!dateRegex.test(normalizedDateStr)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" }); return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
} }
const dateStr = format(parsedDate, "yyyy-MM-dd"); const dateStr = normalizedDateStr;
// Ottieni location dalla query (default: roccapiemonte) // Ottieni location dalla query (default: roccapiemonte)
const location = req.query.location as string || "roccapiemonte"; const location = req.query.location as string || "roccapiemonte";
@ -1142,9 +1142,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
// Pre-validate all dates are within contract period // Pre-validate all dates are within contract period
const startDateParsed = parseISO(startDate); // TIMEZONE FIX: Parse date as YYYY-MM-DD components to avoid timezone shifts
const [year, month, day] = startDate.split("-").map(Number);
for (let dayOffset = 0; dayOffset < days; dayOffset++) { for (let dayOffset = 0; dayOffset < days; dayOffset++) {
const shiftDate = addDays(startDateParsed, dayOffset); // Create date using local timezone components (no UTC conversion)
const shiftDate = new Date(year, month - 1, day + dayOffset);
const shiftDateStr = format(shiftDate, "yyyy-MM-dd"); const shiftDateStr = format(shiftDate, "yyyy-MM-dd");
if (site.contractStartDate && site.contractEndDate) { if (site.contractStartDate && site.contractEndDate) {
@ -1163,7 +1166,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
const createdShiftsInTx = []; const createdShiftsInTx = [];
for (let dayOffset = 0; dayOffset < days; dayOffset++) { for (let dayOffset = 0; dayOffset < days; dayOffset++) {
const shiftDate = addDays(startDateParsed, dayOffset); // TIMEZONE FIX: Build date from components to maintain correct day
const shiftDate = new Date(year, month - 1, day + dayOffset);
// Use site service schedule or default 24h // Use site service schedule or default 24h
const serviceStart = site.serviceStartTime || "00:00"; const serviceStart = site.serviceStartTime || "00:00";
@ -1172,11 +1176,9 @@ 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(shiftDate); // Build timestamps using date components (no timezone conversion)
shiftStart.setHours(startHour, startMin, 0, 0); const shiftStart = new Date(year, month - 1, day + dayOffset, startHour, startMin, 0, 0);
const shiftEnd = new Date(year, month - 1, day + dayOffset, endHour, endMin, 0, 0);
const shiftEnd = new Date(shiftDate);
shiftEnd.setHours(endHour, endMin, 0, 0);
// If service ends before it starts, it spans midnight (add 1 day to end) // If service ends before it starts, it spans midnight (add 1 day to end)
if (shiftEnd <= shiftStart) { if (shiftEnd <= shiftStart) {