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