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) });