From 100f20e422adacf630a7d6600b7972d9350776ff Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Tue, 21 Oct 2025 14:40:16 +0000 Subject: [PATCH] Add ability to assign guards for multiple consecutive days Adds support for multi-day guard assignments by modifying the assign-guard API endpoint and client-side logic to accept and process a `consecutiveDays` parameter. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/U8icLKT --- .replit | 4 + client/src/pages/general-planning.tsx | 50 ++++++- server/routes.ts | 201 ++++++++++++++------------ 3 files changed, 156 insertions(+), 99 deletions(-) diff --git a/.replit b/.replit index 14436ea..90b1d94 100644 --- a/.replit +++ b/.replit @@ -31,6 +31,10 @@ externalPort = 4200 localPort = 42175 externalPort = 3002 +[[ports]] +localPort = 43169 +externalPort = 5000 + [[ports]] localPort = 43267 externalPort = 3003 diff --git a/client/src/pages/general-planning.tsx b/client/src/pages/general-planning.tsx index 5fc5da5..68941fe 100644 --- a/client/src/pages/general-planning.tsx +++ b/client/src/pages/general-planning.tsx @@ -79,6 +79,7 @@ export default function GeneralPlanning() { const [selectedGuardId, setSelectedGuardId] = useState(""); const [startTime, setStartTime] = useState("06:00"); const [durationHours, setDurationHours] = useState(8); + const [consecutiveDays, setConsecutiveDays] = useState(1); // Query per dati planning settimanale const { data: planningData, isLoading } = useQuery({ @@ -123,9 +124,9 @@ export default function GeneralPlanning() { enabled: !!selectedCell, // Query attiva solo se dialog è aperto }); - // Mutation per assegnare guardia con orari + // Mutation per assegnare guardia con orari (anche multi-giorno) const assignGuardMutation = useMutation({ - mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number }) => { + mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number }) => { return apiRequest("POST", "/api/general-planning/assign-guard", data); }, onSuccess: () => { @@ -201,6 +202,7 @@ export default function GeneralPlanning() { guardId: selectedGuardId, startTime, durationHours, + consecutiveDays, }); }; @@ -493,7 +495,13 @@ export default function GeneralPlanning() { {/* Dialog dettagli cella */} - setSelectedCell(null)}> + { + setSelectedCell(null); + setSelectedGuardId(""); + setStartTime("06:00"); + setDurationHours(8); + setConsecutiveDays(1); + }}> @@ -585,14 +593,31 @@ export default function GeneralPlanning() { {/* Form assegnazione guardia */}
+ {/* Mostra guardie già assegnate per questo giorno */} + {selectedCell.data.guards.length > 0 && ( +
+

+ Guardie già assegnate per questa data: +

+
+ {selectedCell.data.guards.map((guard, idx) => ( +
+ {guard.guardName} #{guard.badgeNumber} + {guard.hours}h +
+ ))} +
+
+ )} +
- Assegna Guardia + Assegna Nuova Guardia
- {/* Ora Inizio e Durata */} -
+ {/* Ora Inizio, Durata e Giorni */} +
+
+ + setConsecutiveDays(Math.max(1, Math.min(30, parseInt(e.target.value) || 1)))} + disabled={assignGuardMutation.isPending} + data-testid="input-consecutive-days" + /> +
{/* Ora fine calcolata */} diff --git a/server/routes.ts b/server/routes.ts index 8665745..debfd18 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1177,10 +1177,10 @@ export async function registerRoutes(app: Express): Promise { } }); - // Assign guard to site/date with specific time slot + // Assign guard to site/date with specific time slot (supports multi-day assignments) app.post("/api/general-planning/assign-guard", isAuthenticated, async (req, res) => { try { - const { siteId, date, guardId, startTime, durationHours } = req.body; + const { siteId, date, guardId, startTime, durationHours, consecutiveDays = 1 } = req.body; if (!siteId || !date || !guardId || !startTime || !durationHours) { return res.status(400).json({ @@ -1188,6 +1188,10 @@ export async function registerRoutes(app: Express): Promise { }); } + if (consecutiveDays < 1 || consecutiveDays > 30) { + return res.status(400).json({ message: "consecutiveDays must be between 1 and 30" }); + } + // Get site to check contract and service details const site = await storage.getSite(siteId); if (!site) { @@ -1200,113 +1204,124 @@ export async function registerRoutes(app: Express): Promise { return res.status(404).json({ message: "Guard not found" }); } - // Parse date and time - const shiftDate = parseISO(date); - if (!isValid(shiftDate)) { - return res.status(400).json({ message: "Invalid date format" }); + // Parse start date WITHOUT timezone conversion (stay in local time) + const [year, month, day] = date.split("-").map(Number); + if (!year || !month || !day || month < 1 || month > 12 || day < 1 || day > 31) { + return res.status(400).json({ message: "Invalid date format. Expected YYYY-MM-DD" }); } - - // Calculate planned start and end times const [hours, minutes] = startTime.split(":").map(Number); - const plannedStart = new Date(shiftDate); - plannedStart.setHours(hours, minutes, 0, 0); - const plannedEnd = new Date(plannedStart); - plannedEnd.setHours(plannedStart.getHours() + durationHours); - - // Check contract validity - if (site.contractStartDate && site.contractEndDate) { - const contractStart = new Date(site.contractStartDate); - const contractEnd = new Date(site.contractEndDate); - if (shiftDate < contractStart || shiftDate > contractEnd) { - return res.status(400).json({ - message: `Cannot assign guard: date outside contract period` - }); - } - } - - // Atomic transaction: find/create shift + verify no overlaps + create assignment + // Atomic transaction: create assignments for all consecutive days 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 createdAssignments = []; - 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) { - shift = existingShifts[0]; - } else { - // Create new shift for full service period - const serviceStart = site.serviceStartTime || "00:00"; - const serviceEnd = site.serviceEndTime || "23:59"; + // Loop through each consecutive day + for (let dayOffset = 0; dayOffset < consecutiveDays; dayOffset++) { + // Calculate date for this iteration + const currentDate = new Date(year, month - 1, day + dayOffset); + const shiftDate = new Date(currentDate); + shiftDate.setHours(0, 0, 0, 0); - 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); + // Check contract validity for this date + if (site.contractStartDate && site.contractEndDate) { + const contractStart = new Date(site.contractStartDate); + const contractEnd = new Date(site.contractEndDate); + if (shiftDate < contractStart || shiftDate > contractEnd) { + throw new Error( + `Cannot assign guard for date ${shiftDate.toLocaleDateString()}: outside contract period` + ); + } } - [shift] = await tx.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)); - - for (const existing of existingAssignments) { - const hasOverlap = - plannedStart < existing.plannedEndTime && - plannedEnd > existing.plannedStartTime; + // Calculate planned start and end times for this day + const plannedStart = new Date(currentDate); + plannedStart.setHours(hours, minutes, 0, 0); + const plannedEnd = new Date(currentDate); + plannedEnd.setHours(hours + durationHours, minutes, 0, 0); - if (hasOverlap) { - throw new Error( - `Conflitto: guardia già assegnata ${existing.plannedStartTime.toLocaleString()} - ${existing.plannedEndTime.toLocaleString()}` + // 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 tx + .select() + .from(shifts) + .where( + and( + eq(shifts.siteId, siteId), + gte(shifts.startTime, dayStart), + lte(shifts.startTime, dayEnd) + ) ); + + 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(); } + + // Recheck overlaps within transaction for this day + 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 for this day + const [assignment] = await tx.insert(shiftAssignments).values({ + shiftId: shift.id, + guardId: guard.id, + plannedStartTime: plannedStart, + plannedEndTime: plannedEnd, + }).returning(); + + createdAssignments.push(assignment); } - // 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 }; + return { assignments: createdAssignments, count: createdAssignments.length }; }); res.json({ - message: "Guard assigned successfully", - assignment: result.assignment, - shift: result.shift + message: `Guard assigned successfully for ${result.count} day(s)`, + assignments: result.assignments, + count: result.count }); } catch (error: any) { console.error("Error assigning guard:", error);