Update client to allow creating multi-day shifts directly from the General Planning dialog, and fix the `apiRequest` parameter order in the mutation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/zGfvPmX
628 lines
27 KiB
TypeScript
628 lines
27 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import { format, startOfWeek, addWeeks } from "date-fns";
|
|
import { it } from "date-fns/locale";
|
|
import { useLocation } from "wouter";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ChevronLeft, ChevronRight, Calendar, MapPin, Users, AlertTriangle, Car, Edit, CheckCircle2, Plus } from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import type { GuardAvailability } from "@shared/schema";
|
|
|
|
interface GuardWithHours {
|
|
guardId: string;
|
|
guardName: string;
|
|
badgeNumber: string;
|
|
hours: number;
|
|
}
|
|
|
|
interface Vehicle {
|
|
vehicleId: string;
|
|
licensePlate: string;
|
|
brand: string;
|
|
model: string;
|
|
}
|
|
|
|
interface SiteData {
|
|
siteId: string;
|
|
siteName: string;
|
|
serviceType: string;
|
|
minGuards: number;
|
|
guards: GuardWithHours[];
|
|
vehicles: Vehicle[];
|
|
totalShiftHours: number;
|
|
guardsAssigned: number;
|
|
missingGuards: number;
|
|
shiftsCount: number;
|
|
}
|
|
|
|
interface DayData {
|
|
date: string;
|
|
dayOfWeek: string;
|
|
sites: SiteData[];
|
|
}
|
|
|
|
interface GeneralPlanningResponse {
|
|
weekStart: string;
|
|
weekEnd: string;
|
|
location: string;
|
|
days: DayData[];
|
|
summary: {
|
|
totalGuardsNeeded: number;
|
|
totalGuardsAssigned: number;
|
|
totalGuardsMissing: number;
|
|
};
|
|
}
|
|
|
|
export default function GeneralPlanning() {
|
|
const [, navigate] = useLocation();
|
|
const { toast } = useToast();
|
|
const [selectedLocation, setSelectedLocation] = useState<string>("roccapiemonte");
|
|
const [weekStart, setWeekStart] = useState<Date>(() => startOfWeek(new Date(), { weekStartsOn: 1 }));
|
|
const [selectedCell, setSelectedCell] = useState<{ siteId: string; siteName: string; date: string; data: SiteData } | null>(null);
|
|
|
|
// Form state per creazione turno
|
|
const [selectedGuardId, setSelectedGuardId] = useState<string>("");
|
|
const [days, setDays] = useState<number>(1);
|
|
|
|
// Query per dati planning settimanale
|
|
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
|
queryKey: ["/api/general-planning", format(weekStart, "yyyy-MM-dd"), selectedLocation],
|
|
queryFn: async () => {
|
|
const response = await fetch(
|
|
`/api/general-planning?weekStart=${format(weekStart, "yyyy-MM-dd")}&location=${selectedLocation}`
|
|
);
|
|
if (!response.ok) throw new Error("Failed to fetch general planning");
|
|
return response.json();
|
|
},
|
|
});
|
|
|
|
// Query per guardie disponibili (solo quando dialog è aperto)
|
|
const { data: availableGuards, isLoading: isLoadingGuards } = useQuery<GuardAvailability[]>({
|
|
queryKey: ["/api/guards/availability", format(weekStart, "yyyy-MM-dd"), selectedCell?.siteId, selectedLocation],
|
|
queryFn: async () => {
|
|
if (!selectedCell) return [];
|
|
const response = await fetch(
|
|
`/api/guards/availability?weekStart=${format(weekStart, "yyyy-MM-dd")}&siteId=${selectedCell.siteId}&location=${selectedLocation}`
|
|
);
|
|
if (!response.ok) throw new Error("Failed to fetch guards availability");
|
|
return response.json();
|
|
},
|
|
enabled: !!selectedCell, // Query attiva solo se dialog è aperto
|
|
});
|
|
|
|
// Mutation per creare turno multi-giorno
|
|
const createShiftMutation = useMutation({
|
|
mutationFn: async (data: { siteId: string; startDate: string; days: number; guardId: string }) => {
|
|
return apiRequest("POST", "/api/general-planning/shifts", data);
|
|
},
|
|
onSuccess: () => {
|
|
// Invalida cache planning generale
|
|
queryClient.invalidateQueries({ queryKey: ["/api/general-planning"] });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/guards/availability"] });
|
|
|
|
toast({
|
|
title: "Turno creato",
|
|
description: "Il turno è stato creato con successo",
|
|
});
|
|
|
|
// Reset form e chiudi dialog
|
|
setSelectedGuardId("");
|
|
setDays(1);
|
|
setSelectedCell(null);
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Errore",
|
|
description: error.message || "Impossibile creare il turno",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Handler per submit form creazione turno
|
|
const handleCreateShift = () => {
|
|
if (!selectedCell || !selectedGuardId) return;
|
|
|
|
createShiftMutation.mutate({
|
|
siteId: selectedCell.siteId,
|
|
startDate: selectedCell.date,
|
|
days,
|
|
guardId: selectedGuardId,
|
|
});
|
|
};
|
|
|
|
// Navigazione settimana
|
|
const goToPreviousWeek = () => setWeekStart(addWeeks(weekStart, -1));
|
|
const goToNextWeek = () => setWeekStart(addWeeks(weekStart, 1));
|
|
const goToCurrentWeek = () => setWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 }));
|
|
|
|
// Formatta nome sede
|
|
const formatLocation = (loc: string) => {
|
|
const locations: Record<string, string> = {
|
|
roccapiemonte: "Roccapiemonte",
|
|
milano: "Milano",
|
|
roma: "Roma",
|
|
};
|
|
return locations[loc] || loc;
|
|
};
|
|
|
|
// Raggruppa siti unici da tutti i giorni
|
|
const allSites = planningData?.days.flatMap(day => day.sites) || [];
|
|
const uniqueSites = Array.from(
|
|
new Map(allSites.map(site => [site.siteId, site])).values()
|
|
);
|
|
|
|
// Handler per aprire dialog cella
|
|
const handleCellClick = (siteId: string, siteName: string, date: string, data: SiteData) => {
|
|
setSelectedCell({ siteId, siteName, date, data });
|
|
};
|
|
|
|
// Naviga a pianificazione operativa con parametri
|
|
const navigateToOperationalPlanning = () => {
|
|
if (selectedCell) {
|
|
// Encode parameters nella URL
|
|
navigate(`/operational-planning?date=${selectedCell.date}&location=${selectedLocation}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight" data-testid="text-page-title">
|
|
Planning Generale
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Vista settimanale turni con calcolo automatico guardie mancanti
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filtri e navigazione */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
{/* Selezione Sede */}
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
|
<Select value={selectedLocation} onValueChange={setSelectedLocation}>
|
|
<SelectTrigger className="w-[200px]" data-testid="select-location">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
|
|
<SelectItem value="milano">Milano</SelectItem>
|
|
<SelectItem value="roma">Roma</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Navigazione settimana */}
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={goToPreviousWeek}
|
|
data-testid="button-previous-week"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={goToCurrentWeek}
|
|
data-testid="button-current-week"
|
|
>
|
|
<Calendar className="h-4 w-4 mr-2" />
|
|
Settimana Corrente
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={goToNextWeek}
|
|
data-testid="button-next-week"
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Info settimana */}
|
|
{planningData && (
|
|
<div className="text-sm text-muted-foreground">
|
|
{format(new Date(planningData.weekStart), "dd MMM", { locale: it })} -{" "}
|
|
{format(new Date(planningData.weekEnd), "dd MMM yyyy", { locale: it })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Summary Guardie Settimana */}
|
|
{!isLoading && planningData?.summary && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="h-5 w-5" />
|
|
Riepilogo Guardie Settimana
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="text-center p-4 bg-primary/10 rounded-lg border border-primary/20">
|
|
<p className="text-sm text-muted-foreground mb-2">Guardie Necessarie</p>
|
|
<p className="text-4xl font-bold text-primary">{planningData.summary.totalGuardsNeeded}</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-green-500/10 rounded-lg border border-green-500/20">
|
|
<p className="text-sm text-muted-foreground mb-2">Guardie Pianificate</p>
|
|
<p className="text-4xl font-bold text-green-600 dark:text-green-500">{planningData.summary.totalGuardsAssigned}</p>
|
|
</div>
|
|
<div className={`text-center p-4 rounded-lg border ${planningData.summary.totalGuardsMissing > 0 ? 'bg-destructive/10 border-destructive/20' : 'bg-green-500/10 border-green-500/20'}`}>
|
|
<p className="text-sm text-muted-foreground mb-2">Guardie Mancanti</p>
|
|
<p className={`text-4xl font-bold ${planningData.summary.totalGuardsMissing > 0 ? 'text-destructive' : 'text-green-600 dark:text-green-500'}`}>
|
|
{planningData.summary.totalGuardsMissing}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Tabella Planning */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Calendar className="h-5 w-5" />
|
|
Planning Settimanale - {formatLocation(selectedLocation)}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="space-y-4">
|
|
<Skeleton className="h-12 w-full" />
|
|
<Skeleton className="h-32 w-full" />
|
|
<Skeleton className="h-32 w-full" />
|
|
</div>
|
|
) : planningData ? (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="sticky left-0 bg-background p-3 text-left font-semibold min-w-[200px] border-r">
|
|
Sito
|
|
</th>
|
|
{planningData.days.map((day) => (
|
|
<th
|
|
key={day.date}
|
|
className="p-3 text-center font-semibold min-w-[200px] border-r"
|
|
>
|
|
<div className="flex flex-col">
|
|
<span className="text-sm capitalize">
|
|
{format(new Date(day.date), "EEEE", { locale: it })}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{format(new Date(day.date), "dd/MM", { locale: it })}
|
|
</span>
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{uniqueSites.map((site) => (
|
|
<tr key={site.siteId} className="border-b hover-elevate">
|
|
<td className="sticky left-0 bg-background p-3 border-r font-medium">
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-sm">{site.siteName}</span>
|
|
<Badge variant="outline" className="text-xs w-fit">
|
|
{site.serviceType}
|
|
</Badge>
|
|
</div>
|
|
</td>
|
|
{planningData.days.map((day) => {
|
|
const daySiteData = day.sites.find((s) => s.siteId === site.siteId);
|
|
|
|
return (
|
|
<td
|
|
key={day.date}
|
|
className="p-2 border-r hover:bg-accent/5 cursor-pointer"
|
|
data-testid={`cell-${site.siteId}-${day.date}`}
|
|
onClick={() => daySiteData && handleCellClick(site.siteId, site.siteName, day.date, daySiteData)}
|
|
>
|
|
{daySiteData ? (
|
|
<div className="space-y-2 text-xs">
|
|
{/* Riepilogo guardie necessarie/assegnate/mancanti - SEMPRE VISIBILE */}
|
|
<div className="pb-2 border-b">
|
|
{daySiteData.missingGuards > 0 ? (
|
|
<Badge variant="destructive" className="w-full justify-center gap-1">
|
|
<AlertTriangle className="h-3 w-3" />
|
|
Mancano {daySiteData.missingGuards} {daySiteData.missingGuards === 1 ? "guardia" : "guardie"}
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="default" className="w-full justify-center gap-1 bg-green-600 hover:bg-green-700">
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
Copertura Completa
|
|
</Badge>
|
|
)}
|
|
<div className="text-xs text-muted-foreground mt-1 text-center">
|
|
{daySiteData.guardsAssigned + daySiteData.missingGuards} necessarie · {daySiteData.guardsAssigned} assegnate
|
|
</div>
|
|
</div>
|
|
|
|
{/* Guardie assegnate */}
|
|
{daySiteData.guards.length > 0 && (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-muted-foreground mb-1">
|
|
<Users className="h-3 w-3" />
|
|
<span className="font-medium">Guardie:</span>
|
|
</div>
|
|
{daySiteData.guards.map((guard, idx) => (
|
|
<div key={idx} className="flex items-center justify-between gap-1 pl-4">
|
|
<span className="truncate">{guard.badgeNumber}</span>
|
|
<Badge variant="secondary" className="text-xs">
|
|
{guard.hours}h
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Veicoli */}
|
|
{daySiteData.vehicles.length > 0 && (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-muted-foreground mb-1">
|
|
<Car className="h-3 w-3" />
|
|
<span className="font-medium">Veicoli:</span>
|
|
</div>
|
|
{daySiteData.vehicles.map((vehicle, idx) => (
|
|
<div key={idx} className="pl-4 truncate">
|
|
{vehicle.licensePlate}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Info copertura - mostra solo se ci sono turni */}
|
|
{daySiteData.shiftsCount > 0 && (
|
|
<div className="text-xs text-muted-foreground pt-1 border-t">
|
|
<div>Turni: {daySiteData.shiftsCount}</div>
|
|
<div>Tot. ore: {daySiteData.totalShiftHours}h</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
<span className="text-xs">-</span>
|
|
</div>
|
|
)}
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
{uniqueSites.length === 0 && (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>Nessun sito attivo per la sede selezionata</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p>Errore nel caricamento dei dati</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Dialog dettagli cella */}
|
|
<Dialog open={!!selectedCell} onOpenChange={() => setSelectedCell(null)}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Calendar className="h-5 w-5" />
|
|
{selectedCell?.siteName}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{selectedCell && format(new Date(selectedCell.date), "EEEE dd MMMM yyyy", { locale: it })}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{selectedCell && (
|
|
<div className="space-y-4">
|
|
{/* Info turni */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Turni Pianificati</p>
|
|
<p className="text-2xl font-bold">{selectedCell.data.shiftsCount}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Ore Totali</p>
|
|
<p className="text-2xl font-bold">{selectedCell.data.totalShiftHours}h</p>
|
|
</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 */}
|
|
{selectedCell.data.vehicles.length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
<Car className="h-4 w-4" />
|
|
Veicoli Assegnati ({selectedCell.data.vehicles.length})
|
|
</div>
|
|
<div className="grid gap-2">
|
|
{selectedCell.data.vehicles.map((vehicle, idx) => (
|
|
<div key={idx} className="flex items-center gap-2 p-2 bg-accent/10 rounded-md">
|
|
<p className="font-medium">{vehicle.licensePlate}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{vehicle.brand} {vehicle.model}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Guardie mancanti */}
|
|
{selectedCell.data.missingGuards > 0 && (
|
|
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-md">
|
|
<div className="flex items-center gap-2 text-destructive font-semibold mb-2">
|
|
<AlertTriangle className="h-5 w-5" />
|
|
Attenzione: Guardie Mancanti
|
|
</div>
|
|
<p className="text-sm">
|
|
Servono ancora <span className="font-bold">{selectedCell.data.missingGuards}</span>{" "}
|
|
{selectedCell.data.missingGuards === 1 ? "guardia" : "guardie"} per coprire completamente il servizio
|
|
(calcolato su {selectedCell.data.totalShiftHours}h con max 9h per guardia e {selectedCell.data.minGuards} {selectedCell.data.minGuards === 1 ? "guardia minima" : "guardie minime"} contemporanee)
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* No turni */}
|
|
{selectedCell.data.shiftsCount === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>Nessun turno pianificato per questa data</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form creazione nuovo turno */}
|
|
<div className="border-t pt-4 space-y-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
<Plus className="h-4 w-4" />
|
|
Crea Nuovo Turno
|
|
</div>
|
|
|
|
<div className="grid gap-4">
|
|
{/* Select guardia disponibile */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="guard-select">Guardia Disponibile</Label>
|
|
{isLoadingGuards ? (
|
|
<Skeleton className="h-10 w-full" />
|
|
) : (
|
|
<Select
|
|
value={selectedGuardId}
|
|
onValueChange={setSelectedGuardId}
|
|
disabled={createShiftMutation.isPending}
|
|
>
|
|
<SelectTrigger id="guard-select" data-testid="select-guard">
|
|
<SelectValue placeholder="Seleziona guardia..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableGuards && availableGuards.length > 0 ? (
|
|
availableGuards.map((guard) => (
|
|
<SelectItem key={guard.guardId} value={guard.guardId}>
|
|
{guard.guardName} ({guard.badgeNumber}) - {guard.weeklyHoursRemaining}h disponibili
|
|
</SelectItem>
|
|
))
|
|
) : (
|
|
<SelectItem value="no-guards" disabled>
|
|
Nessuna guardia disponibile
|
|
</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
{availableGuards && availableGuards.length > 0 && selectedGuardId && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{(() => {
|
|
const guard = availableGuards.find(g => g.guardId === selectedGuardId);
|
|
return guard ? `Ore assegnate: ${guard.weeklyHoursAssigned}h / ${guard.weeklyHoursMax}h (rimangono ${guard.weeklyHoursRemaining}h)` : "";
|
|
})()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Input numero giorni */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="days-input">Numero Giorni Consecutivi</Label>
|
|
<Input
|
|
id="days-input"
|
|
type="number"
|
|
min={1}
|
|
max={7}
|
|
value={days}
|
|
onChange={(e) => setDays(Math.max(1, Math.min(7, parseInt(e.target.value) || 1)))}
|
|
disabled={createShiftMutation.isPending}
|
|
data-testid="input-days"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Il turno verrà creato a partire da {selectedCell && format(new Date(selectedCell.date), "dd/MM/yyyy")} per {days} {days === 1 ? "giorno" : "giorni"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Bottone crea turno */}
|
|
<Button
|
|
onClick={handleCreateShift}
|
|
disabled={!selectedGuardId || createShiftMutation.isPending || (availableGuards && availableGuards.length === 0)}
|
|
data-testid="button-create-shift"
|
|
className="w-full"
|
|
>
|
|
{createShiftMutation.isPending ? (
|
|
"Creazione in corso..."
|
|
) : (
|
|
<>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Crea Turno ({days} {days === 1 ? "giorno" : "giorni"})
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setSelectedCell(null)}>
|
|
Chiudi
|
|
</Button>
|
|
<Button onClick={navigateToOperationalPlanning} data-testid="button-edit-planning">
|
|
<Edit className="h-4 w-4 mr-2" />
|
|
Modifica in Pianificazione Operativa
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|