From c72125c68f1d4bfc9e58eabc63104b7b97779445 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Tue, 21 Oct 2025 06:36:42 +0000 Subject: [PATCH] Improve guard assignment and availability checks for shift planning Update storage interface and implementation to handle shift assignment deletion, modify getGuardsAvailability to accept specific planned start and end times, and introduce conflict detection logic for guard availability. Add new DTOs (guardConflictSchema) and update guardAvailabilitySchema to include availability status, conflicts, and unavailability reasons, enhancing shift planning accuracy. 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/ZZOTK7r --- .replit | 4 ++ server/storage.ts | 131 ++++++++++++++++++++++++++++++++++------------ shared/schema.ts | 14 ++++- 3 files changed, 114 insertions(+), 35 deletions(-) diff --git a/.replit b/.replit index c50bc15..269b307 100644 --- a/.replit +++ b/.replit @@ -19,6 +19,10 @@ externalPort = 80 localPort = 33035 externalPort = 3001 +[[ports]] +localPort = 38383 +externalPort = 4200 + [[ports]] localPort = 41343 externalPort = 3000 diff --git a/server/storage.ts b/server/storage.ts index e6ec88d..3b1ac65 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -157,7 +157,15 @@ export interface IStorage { deleteCcnlSetting(key: string): Promise; // General Planning operations - getGuardsAvailability(weekStart: Date, siteId: string, location: string): Promise; + getGuardsAvailability( + siteId: string, + location: string, + plannedStart: Date, + plannedEnd: Date + ): Promise; + + // Shift Assignment operations with time slot management + deleteShiftAssignment(id: string): Promise; } export class DatabaseStorage implements IStorage { @@ -668,9 +676,24 @@ export class DatabaseStorage implements IStorage { await db.delete(ccnlSettings).where(eq(ccnlSettings.key, key)); } - // General Planning operations - async getGuardsAvailability(weekStart: Date, siteId: string, location: string): Promise { + // General Planning operations with time slot conflict detection + async getGuardsAvailability( + siteId: string, + location: string, + plannedStart: Date, + plannedEnd: Date + ): Promise { + // Helper: Check if two time ranges overlap + const hasOverlap = (start1: Date, end1: Date, start2: Date, end2: Date): boolean => { + return start1 < end2 && end1 > start2; + }; + + // Calculate week boundaries for weekly hours calculation + const weekStart = new Date(plannedStart); + weekStart.setDate(plannedStart.getDate() - plannedStart.getDay() + (plannedStart.getDay() === 0 ? -6 : 1)); + weekStart.setHours(0, 0, 0, 0); const weekEnd = addDays(weekStart, 6); + weekEnd.setHours(23, 59, 59, 999); // Get max weekly hours from CCNL settings (default 45h) const maxHoursSetting = await this.getCcnlSetting('weeklyGuardHours'); @@ -689,64 +712,104 @@ export class DatabaseStorage implements IStorage { .where(eq(guards.location, location as any)); // Filter guards by site requirements - const eligibleGuards = allGuards.filter(guard => { + const eligibleGuards = allGuards.filter((guard: Guard) => { if (site.requiresArmed && !guard.isArmed) return false; if (site.requiresDriverLicense && !guard.hasDriverLicense) return false; return true; }); - // Calculate weekly hours for each guard - const guardsWithHours: GuardAvailability[] = []; + // Analyze each guard's availability + const guardsWithAvailability: GuardAvailability[] = []; for (const guard of eligibleGuards) { - // Get all shift assignments for this guard in the week - const assignments = await db + // Get all shift assignments for this guard in the week (for weekly hours) + const weeklyAssignments = await db .select({ + id: shiftAssignments.id, shiftId: shiftAssignments.shiftId, - startTime: shifts.startTime, - endTime: shifts.endTime, + plannedStartTime: shiftAssignments.plannedStartTime, + plannedEndTime: shiftAssignments.plannedEndTime, }) .from(shiftAssignments) - .innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id)) .where( and( eq(shiftAssignments.guardId, guard.id), - gte(shifts.startTime, weekStart), - lte(shifts.startTime, weekEnd) + gte(shiftAssignments.plannedStartTime, weekStart), + lte(shiftAssignments.plannedStartTime, weekEnd) ) ); - // Calculate total hours assigned + // Calculate total weekly hours assigned let weeklyHoursAssigned = 0; - for (const assignment of assignments) { - const hours = differenceInHours(assignment.endTime, assignment.startTime); + for (const assignment of weeklyAssignments) { + const hours = differenceInHours(assignment.plannedEndTime, assignment.plannedStartTime); weeklyHoursAssigned += hours; } const weeklyHoursRemaining = maxWeeklyHours - weeklyHoursAssigned; + const requestedHours = differenceInHours(plannedEnd, plannedStart); - // Only include guards with remaining hours - if (weeklyHoursRemaining > 0) { - const user = guard.userId ? await this.getUser(guard.userId) : undefined; - const guardName = user - ? `${user.firstName || ''} ${user.lastName || ''}`.trim() || 'N/A' - : 'N/A'; - - guardsWithHours.push({ - guardId: guard.id, - guardName, - badgeNumber: guard.badgeNumber, - weeklyHoursRemaining, - weeklyHoursAssigned, - weeklyHoursMax: maxWeeklyHours, - }); + // Check for time conflicts with the requested slot + const conflicts = []; + const reasons: string[] = []; + + for (const assignment of weeklyAssignments) { + if (hasOverlap(plannedStart, plannedEnd, assignment.plannedStartTime, assignment.plannedEndTime)) { + // Get site name for conflict + const [shift] = await db + .select({ siteName: sites.name }) + .from(shifts) + .innerJoin(sites, eq(shifts.siteId, sites.id)) + .where(eq(shifts.id, assignment.shiftId)); + + conflicts.push({ + from: assignment.plannedStartTime, + to: assignment.plannedEndTime, + siteName: shift?.siteName || 'Sito sconosciuto', + shiftId: assignment.shiftId, + }); + } } + + // Determine availability + let isAvailable = true; + + if (conflicts.length > 0) { + isAvailable = false; + reasons.push(`Già assegnata in ${conflicts.length} turno/i nello stesso orario`); + } + + if (weeklyHoursRemaining < requestedHours) { + isAvailable = false; + reasons.push(`Ore settimanali insufficienti (${Math.max(0, weeklyHoursRemaining)}h disponibili, ${requestedHours}h richieste)`); + } + + // Build guard name from new fields + const guardName = guard.firstName && guard.lastName + ? `${guard.firstName} ${guard.lastName}` + : guard.badgeNumber; + + guardsWithAvailability.push({ + guardId: guard.id, + guardName, + badgeNumber: guard.badgeNumber, + weeklyHoursRemaining, + weeklyHoursAssigned, + weeklyHoursMax: maxWeeklyHours, + isAvailable, + conflicts, + unavailabilityReasons: reasons, + }); } - // Sort by remaining hours (descending) - guardsWithHours.sort((a, b) => b.weeklyHoursRemaining - a.weeklyHoursRemaining); + // Sort: available first, then by remaining hours (descending) + guardsWithAvailability.sort((a, b) => { + if (a.isAvailable && !b.isAvailable) return -1; + if (!a.isAvailable && b.isAvailable) return 1; + return b.weeklyHoursRemaining - a.weeklyHoursRemaining; + }); - return guardsWithHours; + return guardsWithAvailability; } } diff --git a/shared/schema.ts b/shared/schema.ts index a63b5c7..b10041e 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -871,7 +871,15 @@ export type AbsenceWithDetails = Absence & { // ============= DTOs FOR GENERAL PLANNING ============= -// DTO per disponibilità guardia nella settimana +// DTO per conflitto orario guardia +export const guardConflictSchema = z.object({ + from: z.date(), + to: z.date(), + siteName: z.string(), + shiftId: z.string(), +}); + +// DTO per disponibilità guardia con controllo conflitti orari export const guardAvailabilitySchema = z.object({ guardId: z.string(), guardName: z.string(), @@ -879,8 +887,12 @@ export const guardAvailabilitySchema = z.object({ weeklyHoursRemaining: z.number(), weeklyHoursAssigned: z.number(), weeklyHoursMax: z.number(), + isAvailable: z.boolean(), + conflicts: z.array(guardConflictSchema), + unavailabilityReasons: z.array(z.string()), }); +export type GuardConflict = z.infer; export type GuardAvailability = z.infer; // DTO per creazione turno multi-giorno dal Planning Generale