diff --git a/.replit b/.replit index c50bc15..17d660d 100644 --- a/.replit +++ b/.replit @@ -19,10 +19,18 @@ externalPort = 80 localPort = 33035 externalPort = 3001 +[[ports]] +localPort = 33659 +externalPort = 5000 + [[ports]] localPort = 41343 externalPort = 3000 +[[ports]] +localPort = 41803 +externalPort = 4200 + [[ports]] localPort = 42175 externalPort = 3002 diff --git a/after_assign_click.png b/after_assign_click.png new file mode 100644 index 0000000..c93c5b1 Binary files /dev/null and b/after_assign_click.png differ diff --git a/after_login.png b/after_login.png new file mode 100644 index 0000000..6d360f6 Binary files /dev/null and b/after_login.png differ diff --git a/after_programmatic_assign_click.png b/after_programmatic_assign_click.png new file mode 100644 index 0000000..2279337 Binary files /dev/null and b/after_programmatic_assign_click.png differ diff --git a/after_sidebar_toggle.png b/after_sidebar_toggle.png new file mode 100644 index 0000000..2279337 Binary files /dev/null and b/after_sidebar_toggle.png differ diff --git a/capture_after_assign_check.png b/capture_after_assign_check.png new file mode 100644 index 0000000..9504838 Binary files /dev/null and b/capture_after_assign_check.png differ diff --git a/cell_clicked.png b/cell_clicked.png new file mode 100644 index 0000000..a8fd39d Binary files /dev/null and b/cell_clicked.png differ diff --git a/client/src/pages/general-planning.tsx b/client/src/pages/general-planning.tsx index e7283b6..5fc5da5 100644 --- a/client/src/pages/general-planning.tsx +++ b/client/src/pages/general-planning.tsx @@ -143,9 +143,26 @@ export default function GeneralPlanning() { setSelectedCell(null); }, onError: (error: any) => { + // Parse error message from API response + let errorMessage = "Impossibile assegnare la guardia"; + if (error.message) { + // Error format from apiRequest: "STATUS_CODE: {json_body}" + const match = error.message.match(/^\d+:\s*(.+)$/); + if (match) { + try { + const parsed = JSON.parse(match[1]); + errorMessage = parsed.message || errorMessage; + } catch { + errorMessage = match[1]; + } + } else { + errorMessage = error.message; + } + } + toast({ - title: "Errore", - description: error.message || "Impossibile assegnare la guardia", + title: "Errore Assegnazione", + description: errorMessage, variant: "destructive", }); }, @@ -653,7 +670,9 @@ export default function GeneralPlanning() {

{guard.conflicts && guard.conflicts.length > 0 && (

- ⚠️ Conflitto: {guard.conflicts.join(", ")} + ⚠️ Conflitto: {guard.conflicts.map((c: any) => + `${c.siteName} (${new Date(c.from).toLocaleTimeString('it-IT', {hour: '2-digit', minute:'2-digit'})} - ${new Date(c.to).toLocaleTimeString('it-IT', {hour: '2-digit', minute:'2-digit'})})` + ).join(", ")}

)} {guard.unavailabilityReasons && guard.unavailabilityReasons.length > 0 && ( diff --git a/filled_assignment_form.png b/filled_assignment_form.png new file mode 100644 index 0000000..ec9a4fd Binary files /dev/null and b/filled_assignment_form.png differ diff --git a/guard_selected_by_eval.png b/guard_selected_by_eval.png new file mode 100644 index 0000000..799fb50 Binary files /dev/null and b/guard_selected_by_eval.png differ diff --git a/overlap_form_filled_eval.png b/overlap_form_filled_eval.png new file mode 100644 index 0000000..9504838 Binary files /dev/null and b/overlap_form_filled_eval.png differ diff --git a/planning_page.png b/planning_page.png new file mode 100644 index 0000000..598eac0 Binary files /dev/null and b/planning_page.png differ diff --git a/server/routes.ts b/server/routes.ts index 4cdde8d..8665745 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1225,71 +1225,97 @@ export async function registerRoutes(app: Express): Promise { } } - // Find or create shift for this site/date (spanning the full service period) - const dayStart = new Date(shiftDate); - dayStart.setHours(0, 0, 0, 0); - const dayEnd = new Date(shiftDate); - dayEnd.setHours(23, 59, 59, 999); - - let existingShifts = await db - .select() - .from(shifts) - .where( - and( - eq(shifts.siteId, siteId), - gte(shifts.startTime, dayStart), - lte(shifts.startTime, dayEnd) - ) - ); - - let shift; - if (existingShifts.length > 0) { - // Use existing shift - shift = existingShifts[0]; - } else { - // Create new shift for full service period - const serviceStart = site.serviceStartTime || "00:00"; - const serviceEnd = site.serviceEndTime || "23:59"; + // Atomic transaction: find/create shift + verify no overlaps + create assignment + const result = await db.transaction(async (tx) => { + // Find or create shift for this site/date + const dayStart = new Date(shiftDate); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(shiftDate); + dayEnd.setHours(23, 59, 59, 999); - const [startHour, startMin] = serviceStart.split(":").map(Number); - const [endHour, endMin] = serviceEnd.split(":").map(Number); + let existingShifts = await tx + .select() + .from(shifts) + .where( + and( + eq(shifts.siteId, siteId), + gte(shifts.startTime, dayStart), + lte(shifts.startTime, dayEnd) + ) + ); - const shiftStart = new Date(shiftDate); - shiftStart.setHours(startHour, startMin, 0, 0); - - const shiftEnd = new Date(shiftDate); - shiftEnd.setHours(endHour, endMin, 0, 0); - - // If service ends before it starts, it spans midnight - if (shiftEnd <= shiftStart) { - shiftEnd.setDate(shiftEnd.getDate() + 1); + let shift; + if (existingShifts.length > 0) { + shift = existingShifts[0]; + } else { + // Create new shift for full service period + const serviceStart = site.serviceStartTime || "00:00"; + const serviceEnd = site.serviceEndTime || "23:59"; + + 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); + + if (shiftEnd <= shiftStart) { + shiftEnd.setDate(shiftEnd.getDate() + 1); + } + + [shift] = await tx.insert(shifts).values({ + siteId: site.id, + startTime: shiftStart, + endTime: shiftEnd, + shiftType: site.shiftType || "fixed_post", + status: "planned", + }).returning(); } - [shift] = await db.insert(shifts).values({ - siteId: site.id, - startTime: shiftStart, - endTime: shiftEnd, - shiftType: site.shiftType || "fixed_post", - status: "planned", + // Recheck overlaps within transaction to prevent race conditions + const existingAssignments = await tx + .select() + .from(shiftAssignments) + .where(eq(shiftAssignments.guardId, guard.id)); + + for (const existing of existingAssignments) { + const hasOverlap = + plannedStart < existing.plannedEndTime && + plannedEnd > existing.plannedStartTime; + + if (hasOverlap) { + throw new Error( + `Conflitto: guardia già assegnata ${existing.plannedStartTime.toLocaleString()} - ${existing.plannedEndTime.toLocaleString()}` + ); + } + } + + // Create assignment within transaction + const [assignment] = await tx.insert(shiftAssignments).values({ + shiftId: shift.id, + guardId: guard.id, + plannedStartTime: plannedStart, + plannedEndTime: plannedEnd, }).returning(); - } - - // Create shift assignment with planned time slot - const assignment = await storage.createShiftAssignment({ - shiftId: shift.id, - guardId: guard.id, - plannedStartTime: plannedStart, - plannedEndTime: plannedEnd, + + return { assignment, shift }; }); res.json({ message: "Guard assigned successfully", - assignment, - shift + assignment: result.assignment, + shift: result.shift }); } catch (error: any) { console.error("Error assigning guard:", error); - if (error.message?.includes('overlap') || error.message?.includes('conflict')) { + // Check for overlap/conflict errors (both English and Italian) + const errorMessage = error.message?.toLowerCase() || ''; + if (errorMessage.includes('overlap') || + errorMessage.includes('conflict') || + errorMessage.includes('conflitto') || + errorMessage.includes('già assegnata')) { return res.status(409).json({ message: error.message }); } res.status(500).json({ message: "Failed to assign guard", error: String(error) });