Compare commits
4 Commits
3a7f44f49f
...
10b543ebab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10b543ebab | ||
|
|
a84f21bf24 | ||
|
|
da547137b7 | ||
|
|
b782f16797 |
BIN
attached_assets/immagine_1761064454333.png
Normal file
BIN
attached_assets/immagine_1761064454333.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@ -95,6 +95,7 @@ export default function GeneralPlanning() {
|
|||||||
const [startTime, setStartTime] = useState<string>("06:00");
|
const [startTime, setStartTime] = useState<string>("06:00");
|
||||||
const [durationHours, setDurationHours] = useState<number>(8);
|
const [durationHours, setDurationHours] = useState<number>(8);
|
||||||
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
||||||
|
const [showOvertimeGuards, setShowOvertimeGuards] = useState<boolean>(false);
|
||||||
|
|
||||||
// Query per dati planning settimanale
|
// Query per dati planning settimanale
|
||||||
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
||||||
@ -517,7 +518,7 @@ export default function GeneralPlanning() {
|
|||||||
setDurationHours(8);
|
setDurationHours(8);
|
||||||
setConsecutiveDays(1);
|
setConsecutiveDays(1);
|
||||||
}}>
|
}}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Calendar className="h-5 w-5" />
|
<Calendar className="h-5 w-5" />
|
||||||
@ -529,7 +530,7 @@ export default function GeneralPlanning() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{selectedCell && (
|
{selectedCell && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 overflow-y-auto pr-2">
|
||||||
{/* Info turni */}
|
{/* Info turni */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@ -542,27 +543,6 @@ export default function GeneralPlanning() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Guardie assegnate */}
|
|
||||||
{selectedCell.data.guards.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
||||||
<Users className="h-4 w-4" />
|
|
||||||
Guardie Assegnate ({selectedCell.data.guards.length})
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
{selectedCell.data.guards.map((guard, idx) => (
|
|
||||||
<div key={idx} className="flex items-center justify-between p-2 bg-accent/10 rounded-md">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{guard.guardName}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{guard.badgeNumber}</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="secondary">{guard.hours}h</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Veicoli */}
|
{/* Veicoli */}
|
||||||
{selectedCell.data.vehicles.length > 0 && (
|
{selectedCell.data.vehicles.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -705,63 +685,111 @@ export default function GeneralPlanning() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Select guardia disponibile */}
|
{/* Select guardia disponibile */}
|
||||||
<div className="space-y-2">
|
{(() => {
|
||||||
<Label htmlFor="guard-select">Guardia Disponibile</Label>
|
// Filtra guardie: mostra solo con ore ordinarie se toggle è off
|
||||||
{isLoadingGuards ? (
|
const filteredGuards = availableGuards?.filter(g =>
|
||||||
<Skeleton className="h-10 w-full" />
|
g.isAvailable && (showOvertimeGuards || !g.requiresOvertime)
|
||||||
) : (
|
) || [];
|
||||||
<Select
|
|
||||||
value={selectedGuardId}
|
const hasOvertimeGuards = availableGuards?.some(g => g.requiresOvertime && g.isAvailable) || false;
|
||||||
onValueChange={setSelectedGuardId}
|
|
||||||
disabled={assignGuardMutation.isPending}
|
return (
|
||||||
>
|
<div className="space-y-2">
|
||||||
<SelectTrigger id="guard-select" data-testid="select-guard">
|
<div className="flex items-center justify-between">
|
||||||
<SelectValue placeholder="Seleziona guardia..." />
|
<Label htmlFor="guard-select">Guardia Disponibile</Label>
|
||||||
</SelectTrigger>
|
{!isLoadingGuards && hasOvertimeGuards && (
|
||||||
<SelectContent>
|
<Button
|
||||||
{availableGuards && availableGuards.length > 0 ? (
|
variant="outline"
|
||||||
availableGuards.map((guard) => (
|
size="sm"
|
||||||
<SelectItem key={guard.guardId} value={guard.guardId}>
|
onClick={() => setShowOvertimeGuards(!showOvertimeGuards)}
|
||||||
{guard.guardName} ({guard.badgeNumber}) - {guard.weeklyHoursRemaining}h disponibili
|
className="h-7 text-xs"
|
||||||
{guard.conflicts && guard.conflicts.length > 0 && " ⚠️"}
|
data-testid="button-toggle-overtime"
|
||||||
</SelectItem>
|
>
|
||||||
))
|
{showOvertimeGuards ? "Nascondi" : "Mostra"} Straordinario
|
||||||
) : (
|
</Button>
|
||||||
<SelectItem value="no-guards" disabled>
|
|
||||||
Nessuna guardia disponibile
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
{isLoadingGuards ? (
|
||||||
)}
|
<Skeleton className="h-10 w-full" />
|
||||||
{availableGuards && availableGuards.length > 0 && selectedGuardId && (
|
) : (
|
||||||
<div className="text-xs space-y-1">
|
<>
|
||||||
{(() => {
|
<Select
|
||||||
const guard = availableGuards.find(g => g.guardId === selectedGuardId);
|
value={selectedGuardId}
|
||||||
if (!guard) return null;
|
onValueChange={setSelectedGuardId}
|
||||||
return (
|
disabled={assignGuardMutation.isPending}
|
||||||
<>
|
>
|
||||||
<p className="text-muted-foreground">
|
<SelectTrigger id="guard-select" data-testid="select-guard">
|
||||||
Ore assegnate: {guard.weeklyHoursAssigned}h / {guard.weeklyHoursMax}h (rimangono {guard.weeklyHoursRemaining}h)
|
<SelectValue placeholder="Seleziona guardia..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{filteredGuards.length > 0 ? (
|
||||||
|
filteredGuards.map((guard) => (
|
||||||
|
<SelectItem key={guard.guardId} value={guard.guardId}>
|
||||||
|
{guard.guardName} ({guard.badgeNumber}) - {guard.ordinaryHoursRemaining}h ord.
|
||||||
|
{guard.requiresOvertime && ` + ${guard.overtimeHoursRemaining}h strao.`}
|
||||||
|
{guard.requiresOvertime && " 🔸"}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="no-guards" disabled>
|
||||||
|
{showOvertimeGuards
|
||||||
|
? "Nessuna guardia disponibile"
|
||||||
|
: "Nessuna guardia con ore ordinarie (prova 'Mostra Straordinario')"}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{filteredGuards.length === 0 && !showOvertimeGuards && hasOvertimeGuards && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
ℹ️ Alcune guardie disponibili richiedono straordinario. Clicca "Mostra Straordinario" per vederle.
|
||||||
</p>
|
</p>
|
||||||
{guard.conflicts && guard.conflicts.length > 0 && (
|
)}
|
||||||
<p className="text-destructive font-medium">
|
{filteredGuards.length > 0 && selectedGuardId && (
|
||||||
⚠️ Conflitto: {guard.conflicts.map((c: any) =>
|
<div className="text-xs space-y-1">
|
||||||
`${c.siteName} (${new Date(c.from).toLocaleTimeString('it-IT', {hour: '2-digit', minute:'2-digit'})} - ${new Date(c.to).toLocaleTimeString('it-IT', {hour: '2-digit', minute:'2-digit'})})`
|
{(() => {
|
||||||
).join(", ")}
|
const guard = availableGuards?.find(g => g.guardId === selectedGuardId);
|
||||||
</p>
|
if (!guard) return null;
|
||||||
)}
|
return (
|
||||||
{guard.unavailabilityReasons && guard.unavailabilityReasons.length > 0 && (
|
<>
|
||||||
<p className="text-yellow-600 dark:text-yellow-500">
|
<p className="text-muted-foreground">
|
||||||
{guard.unavailabilityReasons.join(", ")}
|
Ore ordinarie: {guard.ordinaryHoursRemaining}h / 40h disponibili
|
||||||
</p>
|
{guard.requiresOvertime && ` • Straordinario: ${guard.overtimeHoursRemaining}h / 8h`}
|
||||||
)}
|
</p>
|
||||||
</>
|
<p className="text-muted-foreground">
|
||||||
);
|
Ore assegnate: {guard.weeklyHoursAssigned}h / {guard.weeklyHoursMax}h (rimangono {guard.weeklyHoursRemaining}h)
|
||||||
})()}
|
</p>
|
||||||
|
{guard.nightHoursAssigned > 0 && (
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Ore notturne: {guard.nightHoursAssigned}h / 48h settimanali
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{guard.hasRestViolation && (
|
||||||
|
<p className="text-yellow-600 dark:text-yellow-500 font-medium">
|
||||||
|
⚠️ Attenzione: riposo insufficiente dall'ultimo turno
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{guard.conflicts && guard.conflicts.length > 0 && (
|
||||||
|
<p className="text-destructive font-medium">
|
||||||
|
⚠️ Conflitto: {guard.conflicts.map((c: any) =>
|
||||||
|
`${c.siteName} (${new Date(c.from).toLocaleTimeString('it-IT', {hour: '2-digit', minute:'2-digit'})} - ${new Date(c.to).toLocaleTimeString('it-IT', {hour: '2-digit', minute:'2-digit'})})`
|
||||||
|
).join(", ")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{guard.unavailabilityReasons && guard.unavailabilityReasons.length > 0 && (
|
||||||
|
<p className="text-yellow-600 dark:text-yellow-500">
|
||||||
|
{guard.unavailabilityReasons.join(", ")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
})()}
|
||||||
|
|
||||||
{/* Bottone assegna */}
|
{/* Bottone assegna */}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Binary file not shown.
BIN
database-backups/vigilanzaturni_v1.0.28_20251021_165619.sql.gz
Normal file
BIN
database-backups/vigilanzaturni_v1.0.28_20251021_165619.sql.gz
Normal file
Binary file not shown.
@ -688,6 +688,22 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return start1 < end2 && end1 > start2;
|
return start1 < end2 && end1 > start2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper: Calculate night hours (22:00-06:00)
|
||||||
|
const calculateNightHours = (start: Date, end: Date): number => {
|
||||||
|
let nightHours = 0;
|
||||||
|
const current = new Date(start);
|
||||||
|
|
||||||
|
while (current < end) {
|
||||||
|
const hour = current.getUTCHours();
|
||||||
|
if (hour >= 22 || hour < 6) {
|
||||||
|
nightHours += 1;
|
||||||
|
}
|
||||||
|
current.setHours(current.getHours() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nightHours;
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate week boundaries for weekly hours calculation
|
// Calculate week boundaries for weekly hours calculation
|
||||||
const weekStart = new Date(plannedStart);
|
const weekStart = new Date(plannedStart);
|
||||||
weekStart.setDate(plannedStart.getDate() - plannedStart.getDay() + (plannedStart.getDay() === 0 ? -6 : 1));
|
weekStart.setDate(plannedStart.getDate() - plannedStart.getDay() + (plannedStart.getDay() === 0 ? -6 : 1));
|
||||||
@ -695,9 +711,19 @@ export class DatabaseStorage implements IStorage {
|
|||||||
const weekEnd = addDays(weekStart, 6);
|
const weekEnd = addDays(weekStart, 6);
|
||||||
weekEnd.setHours(23, 59, 59, 999);
|
weekEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
// Get max weekly hours from CCNL settings (default 45h)
|
// Get contract parameters
|
||||||
const maxHoursSetting = await this.getCcnlSetting('weeklyGuardHours');
|
let contractParams = await this.getContractParameters();
|
||||||
const maxWeeklyHours = maxHoursSetting ? Number(maxHoursSetting.value) : 45;
|
if (!contractParams) {
|
||||||
|
contractParams = await this.createContractParameters({
|
||||||
|
contractType: "CCNL_VIGILANZA_2024",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxOrdinaryHours = contractParams.maxHoursPerWeek || 40; // 40h
|
||||||
|
const maxOvertimeHours = contractParams.maxOvertimePerWeek || 8; // 8h
|
||||||
|
const maxTotalHours = maxOrdinaryHours + maxOvertimeHours; // 48h
|
||||||
|
const maxNightHours = contractParams.maxNightHoursPerWeek || 48; // 48h
|
||||||
|
const minDailyRest = contractParams.minDailyRestHours || 11; // 11h
|
||||||
|
|
||||||
// Get site to check requirements
|
// Get site to check requirements
|
||||||
const site = await this.getSite(siteId);
|
const site = await this.getSite(siteId);
|
||||||
@ -718,6 +744,9 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const requestedHours = differenceInHours(plannedEnd, plannedStart);
|
||||||
|
const requestedNightHours = calculateNightHours(plannedStart, plannedEnd);
|
||||||
|
|
||||||
// Analyze each guard's availability
|
// Analyze each guard's availability
|
||||||
const guardsWithAvailability: GuardAvailability[] = [];
|
const guardsWithAvailability: GuardAvailability[] = [];
|
||||||
|
|
||||||
@ -737,17 +766,34 @@ export class DatabaseStorage implements IStorage {
|
|||||||
gte(shiftAssignments.plannedStartTime, weekStart),
|
gte(shiftAssignments.plannedStartTime, weekStart),
|
||||||
lte(shiftAssignments.plannedStartTime, weekEnd)
|
lte(shiftAssignments.plannedStartTime, weekEnd)
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
|
.orderBy(desc(shiftAssignments.plannedEndTime));
|
||||||
|
|
||||||
// Calculate total weekly hours assigned
|
// Calculate total weekly hours and night hours assigned
|
||||||
let weeklyHoursAssigned = 0;
|
let weeklyHoursAssigned = 0;
|
||||||
|
let nightHoursAssigned = 0;
|
||||||
|
let lastShiftEnd: Date | null = null;
|
||||||
|
|
||||||
for (const assignment of weeklyAssignments) {
|
for (const assignment of weeklyAssignments) {
|
||||||
const hours = differenceInHours(assignment.plannedEndTime, assignment.plannedStartTime);
|
const hours = differenceInHours(assignment.plannedEndTime, assignment.plannedStartTime);
|
||||||
weeklyHoursAssigned += hours;
|
weeklyHoursAssigned += hours;
|
||||||
|
nightHoursAssigned += calculateNightHours(assignment.plannedStartTime, assignment.plannedEndTime);
|
||||||
|
|
||||||
|
// Track last shift end for rest calculation
|
||||||
|
if (!lastShiftEnd || assignment.plannedEndTime > lastShiftEnd) {
|
||||||
|
lastShiftEnd = assignment.plannedEndTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const weeklyHoursRemaining = maxWeeklyHours - weeklyHoursAssigned;
|
// Calculate ordinary and overtime hours
|
||||||
const requestedHours = differenceInHours(plannedEnd, plannedStart);
|
const ordinaryHoursAssigned = Math.min(weeklyHoursAssigned, maxOrdinaryHours);
|
||||||
|
const overtimeHoursAssigned = Math.max(0, weeklyHoursAssigned - maxOrdinaryHours);
|
||||||
|
const ordinaryHoursRemaining = Math.max(0, maxOrdinaryHours - weeklyHoursAssigned);
|
||||||
|
const overtimeHoursRemaining = Math.max(0, maxOvertimeHours - overtimeHoursAssigned);
|
||||||
|
const weeklyHoursRemaining = ordinaryHoursRemaining + overtimeHoursRemaining;
|
||||||
|
|
||||||
|
// Check if shift requires overtime
|
||||||
|
const requiresOvertime = requestedHours > ordinaryHoursRemaining;
|
||||||
|
|
||||||
// Check for time conflicts with the requested slot
|
// Check for time conflicts with the requested slot
|
||||||
const conflicts = [];
|
const conflicts = [];
|
||||||
@ -771,19 +817,42 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check rest violation (11h between shifts)
|
||||||
|
let hasRestViolation = false;
|
||||||
|
if (lastShiftEnd) {
|
||||||
|
const hoursSinceLastShift = differenceInHours(plannedStart, lastShiftEnd);
|
||||||
|
if (hoursSinceLastShift < minDailyRest) {
|
||||||
|
hasRestViolation = true;
|
||||||
|
reasons.push(`Riposo insufficiente (${Math.max(0, hoursSinceLastShift).toFixed(1)}h dall'ultimo turno, minimo ${minDailyRest}h)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine availability
|
// Determine availability
|
||||||
let isAvailable = true;
|
let isAvailable = true;
|
||||||
|
|
||||||
|
// EXCLUDE guards already assigned on same day/time
|
||||||
if (conflicts.length > 0) {
|
if (conflicts.length > 0) {
|
||||||
isAvailable = false;
|
isAvailable = false;
|
||||||
reasons.push(`Già assegnata in ${conflicts.length} turno/i nello stesso orario`);
|
reasons.push(`Già assegnata in ${conflicts.length} turno/i nello stesso orario`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if enough hours available (total)
|
||||||
if (weeklyHoursRemaining < requestedHours) {
|
if (weeklyHoursRemaining < requestedHours) {
|
||||||
isAvailable = false;
|
isAvailable = false;
|
||||||
reasons.push(`Ore settimanali insufficienti (${Math.max(0, weeklyHoursRemaining)}h disponibili, ${requestedHours}h richieste)`);
|
reasons.push(`Ore settimanali insufficienti (${Math.max(0, weeklyHoursRemaining)}h disponibili, ${requestedHours}h richieste)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check night hours limit
|
||||||
|
if (nightHoursAssigned + requestedNightHours > maxNightHours) {
|
||||||
|
isAvailable = false;
|
||||||
|
reasons.push(`Ore notturne esaurite (${nightHoursAssigned}h lavorate, max ${maxNightHours}h/settimana)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rest violation makes guard unavailable
|
||||||
|
if (hasRestViolation) {
|
||||||
|
isAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Build guard name from new fields
|
// Build guard name from new fields
|
||||||
const guardName = guard.firstName && guard.lastName
|
const guardName = guard.firstName && guard.lastName
|
||||||
? `${guard.firstName} ${guard.lastName}`
|
? `${guard.firstName} ${guard.lastName}`
|
||||||
@ -795,17 +864,27 @@ export class DatabaseStorage implements IStorage {
|
|||||||
badgeNumber: guard.badgeNumber,
|
badgeNumber: guard.badgeNumber,
|
||||||
weeklyHoursRemaining,
|
weeklyHoursRemaining,
|
||||||
weeklyHoursAssigned,
|
weeklyHoursAssigned,
|
||||||
weeklyHoursMax: maxWeeklyHours,
|
weeklyHoursMax: maxTotalHours,
|
||||||
|
ordinaryHoursRemaining,
|
||||||
|
overtimeHoursRemaining,
|
||||||
|
nightHoursAssigned,
|
||||||
|
requiresOvertime,
|
||||||
|
hasRestViolation,
|
||||||
|
lastShiftEnd,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
conflicts,
|
conflicts,
|
||||||
unavailabilityReasons: reasons,
|
unavailabilityReasons: reasons,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: available first, then by remaining hours (descending)
|
// Sort: available with ordinary hours first, then overtime, then unavailable
|
||||||
guardsWithAvailability.sort((a, b) => {
|
guardsWithAvailability.sort((a, b) => {
|
||||||
if (a.isAvailable && !b.isAvailable) return -1;
|
if (a.isAvailable && !b.isAvailable) return -1;
|
||||||
if (!a.isAvailable && b.isAvailable) return 1;
|
if (!a.isAvailable && b.isAvailable) return 1;
|
||||||
|
if (a.isAvailable && b.isAvailable) {
|
||||||
|
if (!a.requiresOvertime && b.requiresOvertime) return -1;
|
||||||
|
if (a.requiresOvertime && !b.requiresOvertime) return 1;
|
||||||
|
}
|
||||||
return b.weeklyHoursRemaining - a.weeklyHoursRemaining;
|
return b.weeklyHoursRemaining - a.weeklyHoursRemaining;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -887,6 +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(),
|
||||||
|
ordinaryHoursRemaining: z.number(), // Ore ordinarie disponibili (max 40h)
|
||||||
|
overtimeHoursRemaining: z.number(), // Ore straordinario disponibili (max 8h)
|
||||||
|
nightHoursAssigned: z.number(), // Ore notturne lavorate (22:00-06:00)
|
||||||
|
requiresOvertime: z.boolean(), // True se richiede straordinario
|
||||||
|
hasRestViolation: z.boolean(), // True se viola riposo obbligatorio
|
||||||
|
lastShiftEnd: z.date().nullable(), // Fine ultimo turno (per calcolo riposo)
|
||||||
isAvailable: z.boolean(),
|
isAvailable: z.boolean(),
|
||||||
conflicts: z.array(guardConflictSchema),
|
conflicts: z.array(guardConflictSchema),
|
||||||
unavailabilityReasons: z.array(z.string()),
|
unavailabilityReasons: z.array(z.string()),
|
||||||
|
|||||||
10
version.json
10
version.json
@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.27",
|
"version": "1.0.28",
|
||||||
"lastUpdate": "2025-10-21T16:27:43.584Z",
|
"lastUpdate": "2025-10-21T16:56:37.634Z",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
{
|
||||||
|
"version": "1.0.28",
|
||||||
|
"date": "2025-10-21",
|
||||||
|
"type": "patch",
|
||||||
|
"description": "Deployment automatico v1.0.28"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.27",
|
"version": "1.0.27",
|
||||||
"date": "2025-10-21",
|
"date": "2025-10-21",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user