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:
parent
9c28befcb1
commit
ba0bd4d36f
4
.replit
4
.replit
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user