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
This commit is contained in:
parent
1caf5c4199
commit
c72125c68f
4
.replit
4
.replit
@ -19,6 +19,10 @@ externalPort = 80
|
||||
localPort = 33035
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 38383
|
||||
externalPort = 4200
|
||||
|
||||
[[ports]]
|
||||
localPort = 41343
|
||||
externalPort = 3000
|
||||
|
||||
@ -157,7 +157,15 @@ export interface IStorage {
|
||||
deleteCcnlSetting(key: string): Promise<void>;
|
||||
|
||||
// General Planning operations
|
||||
getGuardsAvailability(weekStart: Date, siteId: string, location: string): Promise<GuardAvailability[]>;
|
||||
getGuardsAvailability(
|
||||
siteId: string,
|
||||
location: string,
|
||||
plannedStart: Date,
|
||||
plannedEnd: Date
|
||||
): Promise<GuardAvailability[]>;
|
||||
|
||||
// Shift Assignment operations with time slot management
|
||||
deleteShiftAssignment(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
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<GuardAvailability[]> {
|
||||
// General Planning operations with time slot conflict detection
|
||||
async getGuardsAvailability(
|
||||
siteId: string,
|
||||
location: string,
|
||||
plannedStart: Date,
|
||||
plannedEnd: Date
|
||||
): Promise<GuardAvailability[]> {
|
||||
// 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';
|
||||
// Check for time conflicts with the requested slot
|
||||
const conflicts = [];
|
||||
const reasons: string[] = [];
|
||||
|
||||
guardsWithHours.push({
|
||||
guardId: guard.id,
|
||||
guardName,
|
||||
badgeNumber: guard.badgeNumber,
|
||||
weeklyHoursRemaining,
|
||||
weeklyHoursAssigned,
|
||||
weeklyHoursMax: maxWeeklyHours,
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<typeof guardConflictSchema>;
|
||||
export type GuardAvailability = z.infer<typeof guardAvailabilitySchema>;
|
||||
|
||||
// DTO per creazione turno multi-giorno dal Planning Generale
|
||||
|
||||
Loading…
Reference in New Issue
Block a user