Improve error reporting and conflict visualization for shift assignments

Refactor shift assignment logic to use database transactions and improve error message parsing for assignment failures.

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/NBwOcnR
This commit is contained in:
marco370 2025-10-21 07:44:30 +00:00
parent 3d80f75f43
commit 052cc6896a
13 changed files with 109 additions and 56 deletions

View File

@ -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

BIN
after_assign_click.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
after_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
after_sidebar_toggle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
cell_clicked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -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() {
</p>
{guard.conflicts && guard.conflicts.length > 0 && (
<p className="text-destructive font-medium">
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(", ")}
</p>
)}
{guard.unavailabilityReasons && guard.unavailabilityReasons.length > 0 && (

BIN
filled_assignment_form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
guard_selected_by_eval.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
planning_page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1225,71 +1225,97 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
}
// 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);
// 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);
let existingShifts = await db
.select()
.from(shifts)
.where(
and(
eq(shifts.siteId, siteId),
gte(shifts.startTime, dayStart),
lte(shifts.startTime, dayEnd)
)
);
let existingShifts = await tx
.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";
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 [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 shiftStart = new Date(shiftDate);
shiftStart.setHours(startHour, startMin, 0, 0);
const shiftEnd = new Date(shiftDate);
shiftEnd.setHours(endHour, endMin, 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);
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",
}).returning();
}
// Recheck overlaps within transaction to prevent race conditions
const existingAssignments = await tx
.select()
.from(shiftAssignments)
.where(eq(shiftAssignments.guardId, guard.id));
// Create shift assignment with planned time slot
const assignment = await storage.createShiftAssignment({
shiftId: shift.id,
guardId: guard.id,
plannedStartTime: plannedStart,
plannedEndTime: plannedEnd,
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();
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) });