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
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 40311
|
||||
externalPort = 5173
|
||||
|
||||
[[ports]]
|
||||
localPort = 41343
|
||||
externalPort = 3000
|
||||
|
||||
@ -19,6 +19,16 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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 { useToast } from "@/hooks/use-toast";
|
||||
import type { GuardAvailability, Vehicle as VehicleDb } from "@shared/schema";
|
||||
@ -44,6 +54,9 @@ interface SiteData {
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
serviceType: string;
|
||||
serviceStartTime: string;
|
||||
serviceEndTime: string;
|
||||
serviceHours: number;
|
||||
minGuards: number;
|
||||
guards: GuardWithHours[];
|
||||
vehicles: Vehicle[];
|
||||
@ -103,6 +116,7 @@ export default function GeneralPlanning() {
|
||||
const [durationHours, setDurationHours] = useState<number>(8);
|
||||
const [consecutiveDays, setConsecutiveDays] = useState<number>(1);
|
||||
const [showOvertimeGuards, setShowOvertimeGuards] = useState<boolean>(false);
|
||||
const [ccnlConfirmation, setCcnlConfirmation] = useState<{ message: string; data: any } | null>(null);
|
||||
|
||||
// Query per dati planning settimanale
|
||||
const { data: planningData, isLoading } = useQuery<GeneralPlanningResponse>({
|
||||
@ -200,7 +214,7 @@ export default function GeneralPlanning() {
|
||||
|
||||
// Mutation per assegnare guardia con orari (anche multi-giorno)
|
||||
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);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
@ -220,19 +234,33 @@ export default function GeneralPlanning() {
|
||||
// Reset form (NON chiudere dialog per vedere lista aggiornata)
|
||||
setSelectedGuardId("");
|
||||
setSelectedVehicleId("");
|
||||
setCcnlConfirmation(null); // Reset dialog conferma se aperto
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: any, variables) => {
|
||||
// Parse error message from API response
|
||||
let errorMessage = "Impossibile assegnare la guardia";
|
||||
let errorType = "";
|
||||
|
||||
if (error.message) {
|
||||
// Error format from apiRequest: "STATUS_CODE: {json_body}"
|
||||
const match = error.message.match(/^\d+:\s*(.+)$/);
|
||||
const match = error.message.match(/^(\d+):\s*(.+)$/);
|
||||
if (match) {
|
||||
const statusCode = match[1];
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]);
|
||||
const parsed = JSON.parse(match[2]);
|
||||
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 {
|
||||
errorMessage = match[1];
|
||||
errorMessage = match[2];
|
||||
}
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
@ -834,7 +862,25 @@ export default function GeneralPlanning() {
|
||||
|
||||
{/* Info turni esistenti */}
|
||||
<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>
|
||||
@ -842,7 +888,7 @@ export default function GeneralPlanning() {
|
||||
<p className="text-2xl font-bold">{currentCellData?.shiftsCount || 0}</p>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -905,6 +951,43 @@ export default function GeneralPlanning() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1058,6 +1058,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
siteId: site.id,
|
||||
siteName: site.name,
|
||||
serviceType: serviceType?.label || "N/A",
|
||||
serviceStartTime: serviceStart,
|
||||
serviceEndTime: serviceEnd,
|
||||
serviceHours: Math.round(serviceHours * 10) / 10, // Arrotonda a 1 decimale
|
||||
minGuards: site.minGuards,
|
||||
guards: guardsWithHours,
|
||||
vehicles: dayVehicles,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user