Add confirmation dialog for guard assignments exceeding limits

Update general planning to include AlertDialog for CCNL_VIOLATION errors, allowing forced guard assignments and displaying service details.

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/2w7P7NW
This commit is contained in:
marco370 2025-10-23 07:46:11 +00:00
parent 9c28befcb1
commit ba0bd4d36f
3 changed files with 93 additions and 11 deletions

View File

@ -19,10 +19,6 @@ externalPort = 80
localPort = 33035 localPort = 33035
externalPort = 3001 externalPort = 3001
[[ports]]
localPort = 40311
externalPort = 5173
[[ports]] [[ports]]
localPort = 41343 localPort = 41343
externalPort = 3000 externalPort = 3000

View File

@ -19,6 +19,16 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { queryClient, apiRequest } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import type { GuardAvailability, Vehicle as VehicleDb } from "@shared/schema"; import type { GuardAvailability, Vehicle as VehicleDb } from "@shared/schema";
@ -44,6 +54,9 @@ interface SiteData {
siteId: string; siteId: string;
siteName: string; siteName: string;
serviceType: string; serviceType: string;
serviceStartTime: string;
serviceEndTime: string;
serviceHours: number;
minGuards: number; minGuards: number;
guards: GuardWithHours[]; guards: GuardWithHours[];
vehicles: Vehicle[]; vehicles: Vehicle[];
@ -103,6 +116,7 @@ export default function GeneralPlanning() {
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); const [showOvertimeGuards, setShowOvertimeGuards] = useState<boolean>(false);
const [ccnlConfirmation, setCcnlConfirmation] = useState<{ message: string; data: any } | null>(null);
// Query per dati planning settimanale // Query per dati planning settimanale
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({ const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
@ -200,7 +214,7 @@ export default function GeneralPlanning() {
// Mutation per assegnare guardia con orari (anche multi-giorno) // Mutation per assegnare guardia con orari (anche multi-giorno)
const assignGuardMutation = useMutation({ const assignGuardMutation = useMutation({
mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number; vehicleId?: string }) => { mutationFn: async (data: { siteId: string; date: string; guardId: string; startTime: string; durationHours: number; consecutiveDays: number; vehicleId?: string; force?: boolean }) => {
return apiRequest("POST", "/api/general-planning/assign-guard", data); return apiRequest("POST", "/api/general-planning/assign-guard", data);
}, },
onSuccess: async () => { onSuccess: async () => {
@ -220,19 +234,33 @@ export default function GeneralPlanning() {
// Reset form (NON chiudere dialog per vedere lista aggiornata) // Reset form (NON chiudere dialog per vedere lista aggiornata)
setSelectedGuardId(""); setSelectedGuardId("");
setSelectedVehicleId(""); setSelectedVehicleId("");
setCcnlConfirmation(null); // Reset dialog conferma se aperto
}, },
onError: (error: any) => { onError: (error: any, variables) => {
// Parse error message from API response // Parse error message from API response
let errorMessage = "Impossibile assegnare la guardia"; let errorMessage = "Impossibile assegnare la guardia";
let errorType = "";
if (error.message) { if (error.message) {
// Error format from apiRequest: "STATUS_CODE: {json_body}" // Error format from apiRequest: "STATUS_CODE: {json_body}"
const match = error.message.match(/^\d+:\s*(.+)$/); const match = error.message.match(/^(\d+):\s*(.+)$/);
if (match) { if (match) {
const statusCode = match[1];
try { try {
const parsed = JSON.parse(match[1]); const parsed = JSON.parse(match[2]);
errorMessage = parsed.message || errorMessage; errorMessage = parsed.message || errorMessage;
errorType = parsed.type || "";
// Se è un errore CCNL (409 con tipo CCNL_VIOLATION), mostra dialog conferma
if (statusCode === "409" && errorType === "CCNL_VIOLATION") {
setCcnlConfirmation({
message: errorMessage,
data: variables
});
return; // Non mostrare toast, mostra dialog
}
} catch { } catch {
errorMessage = match[1]; errorMessage = match[2];
} }
} else { } else {
errorMessage = error.message; errorMessage = error.message;
@ -834,7 +862,25 @@ export default function GeneralPlanning() {
{/* Info turni esistenti */} {/* Info turni esistenti */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-semibold text-sm">Situazione Attuale</h3> <h3 className="font-semibold text-sm">Informazioni Servizio</h3>
{/* Tipo servizio e orario */}
{currentCellData && (
<div className="bg-muted/30 p-3 rounded-md space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Tipo Servizio</span>
<Badge variant="outline">{currentCellData.serviceType}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Orario Servizio</span>
<span className="text-sm font-medium">{currentCellData.serviceStartTime} - {currentCellData.serviceEndTime}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Ore Richieste</span>
<span className="text-sm font-bold">{currentCellData.serviceHours}h</span>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
@ -842,7 +888,7 @@ export default function GeneralPlanning() {
<p className="text-2xl font-bold">{currentCellData?.shiftsCount || 0}</p> <p className="text-2xl font-bold">{currentCellData?.shiftsCount || 0}</p>
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Ore Totali</p> <p className="text-sm text-muted-foreground">Ore Assegnate</p>
<p className="text-2xl font-bold">{currentCellData?.totalShiftHours || 0}h</p> <p className="text-2xl font-bold">{currentCellData?.totalShiftHours || 0}h</p>
</div> </div>
</div> </div>
@ -905,6 +951,43 @@ export default function GeneralPlanning() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Dialog conferma forzatura CCNL */}
<AlertDialog open={!!ccnlConfirmation} onOpenChange={() => setCcnlConfirmation(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-600" />
Superamento Limite CCNL
</AlertDialogTitle>
<AlertDialogDescription className="space-y-2">
<p className="text-foreground font-medium">
{ccnlConfirmation?.message}
</p>
<p className="text-sm">
Vuoi forzare comunque l'assegnazione? L'operazione verrà registrata e potrai consultarla nei report.
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-force">Annulla</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (ccnlConfirmation) {
assignGuardMutation.mutate({
...ccnlConfirmation.data,
force: true
});
}
}}
data-testid="button-confirm-force"
className="bg-yellow-600 hover:bg-yellow-700"
>
Forza Assegnazione
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
} }

View File

@ -1058,6 +1058,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
siteId: site.id, siteId: site.id,
siteName: site.name, siteName: site.name,
serviceType: serviceType?.label || "N/A", serviceType: serviceType?.label || "N/A",
serviceStartTime: serviceStart,
serviceEndTime: serviceEnd,
serviceHours: Math.round(serviceHours * 10) / 10, // Arrotonda a 1 decimale
minGuards: site.minGuards, minGuards: site.minGuards,
guards: guardsWithHours, guards: guardsWithHours,
vehicles: dayVehicles, vehicles: dayVehicles,