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:
parent
1598eb208b
commit
983adcfbe1
4
.replit
4
.replit
@ -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
|
||||||
|
|||||||
37
replit.md
37
replit.md
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user