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
|
localPort = 33035
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 38383
|
||||||
|
externalPort = 4200
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41343
|
localPort = 41343
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|||||||
@ -157,7 +157,15 @@ export interface IStorage {
|
|||||||
deleteCcnlSetting(key: string): Promise<void>;
|
deleteCcnlSetting(key: string): Promise<void>;
|
||||||
|
|
||||||
// General Planning operations
|
// 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 {
|
export class DatabaseStorage implements IStorage {
|
||||||
@ -668,9 +676,24 @@ export class DatabaseStorage implements IStorage {
|
|||||||
await db.delete(ccnlSettings).where(eq(ccnlSettings.key, key));
|
await db.delete(ccnlSettings).where(eq(ccnlSettings.key, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
// General Planning operations
|
// General Planning operations with time slot conflict detection
|
||||||
async getGuardsAvailability(weekStart: Date, siteId: string, location: string): Promise<GuardAvailability[]> {
|
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);
|
const weekEnd = addDays(weekStart, 6);
|
||||||
|
weekEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
// Get max weekly hours from CCNL settings (default 45h)
|
// Get max weekly hours from CCNL settings (default 45h)
|
||||||
const maxHoursSetting = await this.getCcnlSetting('weeklyGuardHours');
|
const maxHoursSetting = await this.getCcnlSetting('weeklyGuardHours');
|
||||||
@ -689,64 +712,104 @@ export class DatabaseStorage implements IStorage {
|
|||||||
.where(eq(guards.location, location as any));
|
.where(eq(guards.location, location as any));
|
||||||
|
|
||||||
// Filter guards by site requirements
|
// 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.requiresArmed && !guard.isArmed) return false;
|
||||||
if (site.requiresDriverLicense && !guard.hasDriverLicense) return false;
|
if (site.requiresDriverLicense && !guard.hasDriverLicense) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate weekly hours for each guard
|
// Analyze each guard's availability
|
||||||
const guardsWithHours: GuardAvailability[] = [];
|
const guardsWithAvailability: GuardAvailability[] = [];
|
||||||
|
|
||||||
for (const guard of eligibleGuards) {
|
for (const guard of eligibleGuards) {
|
||||||
// Get all shift assignments for this guard in the week
|
// Get all shift assignments for this guard in the week (for weekly hours)
|
||||||
const assignments = await db
|
const weeklyAssignments = await db
|
||||||
.select({
|
.select({
|
||||||
|
id: shiftAssignments.id,
|
||||||
shiftId: shiftAssignments.shiftId,
|
shiftId: shiftAssignments.shiftId,
|
||||||
startTime: shifts.startTime,
|
plannedStartTime: shiftAssignments.plannedStartTime,
|
||||||
endTime: shifts.endTime,
|
plannedEndTime: shiftAssignments.plannedEndTime,
|
||||||
})
|
})
|
||||||
.from(shiftAssignments)
|
.from(shiftAssignments)
|
||||||
.innerJoin(shifts, eq(shiftAssignments.shiftId, shifts.id))
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(shiftAssignments.guardId, guard.id),
|
eq(shiftAssignments.guardId, guard.id),
|
||||||
gte(shifts.startTime, weekStart),
|
gte(shiftAssignments.plannedStartTime, weekStart),
|
||||||
lte(shifts.startTime, weekEnd)
|
lte(shiftAssignments.plannedStartTime, weekEnd)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate total hours assigned
|
// Calculate total weekly hours assigned
|
||||||
let weeklyHoursAssigned = 0;
|
let weeklyHoursAssigned = 0;
|
||||||
for (const assignment of assignments) {
|
for (const assignment of weeklyAssignments) {
|
||||||
const hours = differenceInHours(assignment.endTime, assignment.startTime);
|
const hours = differenceInHours(assignment.plannedEndTime, assignment.plannedStartTime);
|
||||||
weeklyHoursAssigned += hours;
|
weeklyHoursAssigned += hours;
|
||||||
}
|
}
|
||||||
|
|
||||||
const weeklyHoursRemaining = maxWeeklyHours - weeklyHoursAssigned;
|
const weeklyHoursRemaining = maxWeeklyHours - weeklyHoursAssigned;
|
||||||
|
const requestedHours = differenceInHours(plannedEnd, plannedStart);
|
||||||
|
|
||||||
// Only include guards with remaining hours
|
// Check for time conflicts with the requested slot
|
||||||
if (weeklyHoursRemaining > 0) {
|
const conflicts = [];
|
||||||
const user = guard.userId ? await this.getUser(guard.userId) : undefined;
|
const reasons: string[] = [];
|
||||||
const guardName = user
|
|
||||||
? `${user.firstName || ''} ${user.lastName || ''}`.trim() || 'N/A'
|
|
||||||
: 'N/A';
|
|
||||||
|
|
||||||
guardsWithHours.push({
|
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,
|
guardId: guard.id,
|
||||||
guardName,
|
guardName,
|
||||||
badgeNumber: guard.badgeNumber,
|
badgeNumber: guard.badgeNumber,
|
||||||
weeklyHoursRemaining,
|
weeklyHoursRemaining,
|
||||||
weeklyHoursAssigned,
|
weeklyHoursAssigned,
|
||||||
weeklyHoursMax: maxWeeklyHours,
|
weeklyHoursMax: maxWeeklyHours,
|
||||||
|
isAvailable,
|
||||||
|
conflicts,
|
||||||
|
unavailabilityReasons: reasons,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by remaining hours (descending)
|
// Sort: available first, then by remaining hours (descending)
|
||||||
guardsWithHours.sort((a, b) => b.weeklyHoursRemaining - a.weeklyHoursRemaining);
|
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 =============
|
// ============= 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({
|
export const guardAvailabilitySchema = z.object({
|
||||||
guardId: z.string(),
|
guardId: z.string(),
|
||||||
guardName: z.string(),
|
guardName: z.string(),
|
||||||
@ -879,8 +887,12 @@ export const guardAvailabilitySchema = z.object({
|
|||||||
weeklyHoursRemaining: z.number(),
|
weeklyHoursRemaining: z.number(),
|
||||||
weeklyHoursAssigned: z.number(),
|
weeklyHoursAssigned: z.number(),
|
||||||
weeklyHoursMax: 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>;
|
export type GuardAvailability = z.infer<typeof guardAvailabilitySchema>;
|
||||||
|
|
||||||
// DTO per creazione turno multi-giorno dal Planning Generale
|
// DTO per creazione turno multi-giorno dal Planning Generale
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user