diff --git a/.replit b/.replit index 90b1d94..4827ed6 100644 --- a/.replit +++ b/.replit @@ -19,6 +19,10 @@ externalPort = 80 localPort = 33035 externalPort = 3001 +[[ports]] +localPort = 37831 +externalPort = 5173 + [[ports]] localPort = 41343 externalPort = 3000 diff --git a/replit.md b/replit.md index 6d491d3..1403953 100644 --- a/replit.md +++ b/replit.md @@ -10,6 +10,43 @@ VigilanzaTurni is a professional 24/7 shift management system designed for secur - 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) +## ⚠️ 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 ### Stack Tecnologico diff --git a/server/routes.ts b/server/routes.ts index 7ae8682..2dc6cad 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -609,13 +609,13 @@ export async function registerRoutes(app: Express): Promise { 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)) { + // TIMEZONE FIX: Valida formato senza parseISO per evitare shift timezone + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(normalizedDateStr)) { 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) const location = req.query.location as string || "roccapiemonte"; @@ -748,13 +748,13 @@ export async function registerRoutes(app: Express): Promise { 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)) { + // TIMEZONE FIX: Valida formato senza parseISO per evitare shift timezone + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(normalizedDateStr)) { 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) const location = req.query.location as string || "roccapiemonte"; @@ -1142,9 +1142,12 @@ export async function registerRoutes(app: Express): Promise { } // 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++) { - 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"); if (site.contractStartDate && site.contractEndDate) { @@ -1163,7 +1166,8 @@ export async function registerRoutes(app: Express): Promise { const createdShiftsInTx = []; 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 const serviceStart = site.serviceStartTime || "00:00"; @@ -1172,11 +1176,9 @@ export async function registerRoutes(app: Express): Promise { const [startHour, startMin] = serviceStart.split(":").map(Number); const [endHour, endMin] = serviceEnd.split(":").map(Number); - const shiftStart = new Date(shiftDate); - shiftStart.setHours(startHour, startMin, 0, 0); - - const shiftEnd = new Date(shiftDate); - shiftEnd.setHours(endHour, endMin, 0, 0); + // Build timestamps using date components (no timezone conversion) + 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); // If service ends before it starts, it spans midnight (add 1 day to end) if (shiftEnd <= shiftStart) {