Improve guard selection by filtering based on availability and overtime

Refactors the guard selection UI to dynamically filter available guards, showing regular hours first and providing an option to display those requiring overtime.

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/qoWuIE4
This commit is contained in:
marco370 2025-10-21 16:51:34 +00:00
parent b782f16797
commit da547137b7
2 changed files with 106 additions and 88 deletions

View File

@ -39,6 +39,10 @@ externalPort = 5000
localPort = 43267 localPort = 43267
externalPort = 3003 externalPort = 3003
[[ports]]
localPort = 45469
externalPort = 5173
[env] [env]
PORT = "5000" PORT = "5000"

View File

@ -685,97 +685,111 @@ export default function GeneralPlanning() {
</div> </div>
{/* Select guardia disponibile */} {/* Select guardia disponibile */}
<div className="space-y-2"> {(() => {
<div className="flex items-center justify-between"> // Filtra guardie: mostra solo con ore ordinarie se toggle è off
<Label htmlFor="guard-select">Guardia Disponibile</Label> const filteredGuards = availableGuards?.filter(g =>
{!isLoadingGuards && availableGuards && availableGuards.some(g => g.requiresOvertime && g.isAvailable) && ( g.isAvailable && (showOvertimeGuards || !g.requiresOvertime)
<Button ) || [];
variant="outline"
size="sm" const hasOvertimeGuards = availableGuards?.some(g => g.requiresOvertime && g.isAvailable) || false;
onClick={() => setShowOvertimeGuards(!showOvertimeGuards)}
className="h-7 text-xs" return (
data-testid="button-toggle-overtime" <div className="space-y-2">
> <div className="flex items-center justify-between">
{showOvertimeGuards ? "Nascondi" : "Mostra"} Straordinario <Label htmlFor="guard-select">Guardia Disponibile</Label>
</Button> {!isLoadingGuards && hasOvertimeGuards && (
)} <Button
</div> variant="outline"
{isLoadingGuards ? ( size="sm"
<Skeleton className="h-10 w-full" /> onClick={() => setShowOvertimeGuards(!showOvertimeGuards)}
) : ( className="h-7 text-xs"
<> data-testid="button-toggle-overtime"
{(() => { >
// Filtra guardie: mostra solo con ore ordinarie se toggle è off {showOvertimeGuards ? "Nascondi" : "Mostra"} Straordinario
const filteredGuards = availableGuards?.filter(g => </Button>
g.isAvailable && (showOvertimeGuards || !g.requiresOvertime) )}
) || []; </div>
{isLoadingGuards ? (
return ( <Skeleton className="h-10 w-full" />
<> ) : (
<Select <>
value={selectedGuardId} <Select
onValueChange={setSelectedGuardId} value={selectedGuardId}
disabled={assignGuardMutation.isPending} onValueChange={setSelectedGuardId}
> disabled={assignGuardMutation.isPending}
<SelectTrigger id="guard-select" data-testid="select-guard"> >
<SelectValue placeholder="Seleziona guardia..." /> <SelectTrigger id="guard-select" data-testid="select-guard">
</SelectTrigger> <SelectValue placeholder="Seleziona guardia..." />
<SelectContent> </SelectTrigger>
{filteredGuards.length > 0 ? ( <SelectContent>
filteredGuards.map((guard) => ( {filteredGuards.length > 0 ? (
<SelectItem key={guard.guardId} value={guard.guardId}> filteredGuards.map((guard) => (
{guard.guardName} ({guard.badgeNumber}) - {guard.ordinaryHoursRemaining}h ord. <SelectItem key={guard.guardId} value={guard.guardId}>
{guard.requiresOvertime && ` + ${guard.overtimeHoursRemaining}h strao.`} {guard.guardName} ({guard.badgeNumber}) - {guard.ordinaryHoursRemaining}h ord.
{guard.requiresOvertime && " 🔸"} {guard.requiresOvertime && ` + ${guard.overtimeHoursRemaining}h strao.`}
</SelectItem> {guard.requiresOvertime && " 🔸"}
))
) : (
<SelectItem value="no-guards" disabled>
{showOvertimeGuards
? "Nessuna guardia disponibile"
: "Nessuna guardia con ore ordinarie (prova 'Mostra Straordinario')"}
</SelectItem> </SelectItem>
)} ))
</SelectContent> ) : (
</Select> <SelectItem value="no-guards" disabled>
{filteredGuards.length === 0 && !showOvertimeGuards && availableGuards && availableGuards.some(g => g.isAvailable && g.requiresOvertime) && ( {showOvertimeGuards
<p className="text-xs text-muted-foreground"> ? "Nessuna guardia disponibile"
Alcune guardie disponibili richiedono straordinario. Clicca "Mostra Straordinario" per vederle. : "Nessuna guardia con ore ordinarie (prova 'Mostra Straordinario')"}
</p> </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.
{availableGuards && availableGuards.length > 0 && selectedGuardId && (
<div className="text-xs space-y-1">
{(() => {
const guard = availableGuards.find(g => g.guardId === selectedGuardId);
if (!guard) return null;
return (
<>
<p className="text-muted-foreground">
Ore assegnate: {guard.weeklyHoursAssigned}h / {guard.weeklyHoursMax}h (rimangono {guard.weeklyHoursRemaining}h)
</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