VigilanzaTurni/client/src/pages/parameters.tsx
marco370 34221555d8 Add location filtering and meal voucher settings
Implements multi-location filtering on Dashboard and Shifts pages, adds meal voucher configuration options in Parameters, and introduces multi-location seeding in server/seed.ts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 42d8028a-fa71-4ec2-938c-e43eedf7df01
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/42d8028a-fa71-4ec2-938c-e43eedf7df01/IdDfihe
2025-10-17 07:25:08 +00:00

427 lines
16 KiB
TypeScript

import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import { Loader2, Save, Settings } from "lucide-react";
import type { ContractParameters } from "@shared/schema";
export default function Parameters() {
const { toast } = useToast();
const [isEditing, setIsEditing] = useState(false);
const { data: parameters, isLoading } = useQuery<ContractParameters>({
queryKey: ["/api/contract-parameters"],
});
const [formData, setFormData] = useState<Partial<ContractParameters>>({});
// Sync formData with parameters when they load
useEffect(() => {
if (parameters && !isEditing) {
setFormData(parameters);
}
}, [parameters, isEditing]);
const updateMutation = useMutation({
mutationFn: async (data: Partial<ContractParameters>) => {
if (!parameters?.id) throw new Error("No parameters ID");
const response = await fetch(`/api/contract-parameters/${parameters.id}`, {
method: "PUT",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error("Failed to update parameters");
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/contract-parameters"] });
toast({
title: "Parametri aggiornati",
description: "I parametri CCNL sono stati aggiornati con successo.",
});
setIsEditing(false);
},
onError: (error: Error) => {
toast({
title: "Errore",
description: error.message || "Impossibile aggiornare i parametri",
variant: "destructive",
});
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate all numeric fields are present and valid
const requiredNumericFields = [
'maxHoursPerDay', 'maxOvertimePerDay', 'maxHoursPerWeek', 'maxOvertimePerWeek',
'minDailyRestHours', 'minDailyRestHoursReduced', 'maxDailyRestReductionsPerMonth',
'maxDailyRestReductionsPerYear', 'minWeeklyRestHours', 'pauseMinutesIfOver6Hours'
];
for (const field of requiredNumericFields) {
const value = (formData as any)[field];
if (value === undefined || value === null || isNaN(value)) {
toast({
title: "Errore Validazione",
description: `Il campo ${field} deve essere un numero valido`,
variant: "destructive",
});
return;
}
}
updateMutation.mutate(formData);
};
const handleCancel = () => {
setFormData(parameters || {});
setIsEditing(false);
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!parameters) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Nessun parametro configurato</p>
</div>
);
}
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold flex items-center gap-2" data-testid="heading-parameters">
<Settings className="h-8 w-8" />
Parametri Sistema
</h1>
<p className="text-muted-foreground mt-1">
Configurazione limiti CCNL e regole turni
</p>
</div>
{!isEditing ? (
<Button onClick={() => setIsEditing(true)} data-testid="button-edit-parameters">
Modifica Parametri
</Button>
) : (
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleCancel}
disabled={updateMutation.isPending}
data-testid="button-cancel-edit"
>
Annulla
</Button>
<Button
onClick={handleSubmit}
disabled={updateMutation.isPending}
data-testid="button-save-parameters"
>
{updateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Salvataggio...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Salva Modifiche
</>
)}
</Button>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Limiti Orari */}
<Card>
<CardHeader>
<CardTitle>Limiti Orari</CardTitle>
<CardDescription>Orari massimi giornalieri e settimanali secondo CCNL</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="maxHoursPerDay">Ore Massime Giornaliere</Label>
<Input
id="maxHoursPerDay"
type="number"
value={formData.maxHoursPerDay || 8}
onChange={(e) => setFormData({ ...formData, maxHoursPerDay: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-hours-per-day"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxOvertimePerDay">Straordinario Max Giornaliero (ore)</Label>
<Input
id="maxOvertimePerDay"
type="number"
value={formData.maxOvertimePerDay || 2}
onChange={(e) => setFormData({ ...formData, maxOvertimePerDay: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-overtime-per-day"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxHoursPerWeek">Ore Massime Settimanali</Label>
<Input
id="maxHoursPerWeek"
type="number"
value={formData.maxHoursPerWeek || 40}
onChange={(e) => setFormData({ ...formData, maxHoursPerWeek: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-hours-per-week"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxOvertimePerWeek">Straordinario Max Settimanale (ore)</Label>
<Input
id="maxOvertimePerWeek"
type="number"
value={formData.maxOvertimePerWeek || 8}
onChange={(e) => setFormData({ ...formData, maxOvertimePerWeek: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-overtime-per-week"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxNightHoursPerWeek">Ore Notturne Max Settimanali (22:00-06:00)</Label>
<Input
id="maxNightHoursPerWeek"
type="number"
value={formData.maxNightHoursPerWeek || 48}
onChange={(e) => setFormData({ ...formData, maxNightHoursPerWeek: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-night-hours-per-week"
/>
</div>
</CardContent>
</Card>
{/* Riposi Obbligatori */}
<Card>
<CardHeader>
<CardTitle>Riposi Obbligatori</CardTitle>
<CardDescription>Riposi minimi giornalieri e settimanali secondo CCNL</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="minDailyRestHours">Riposo Giornaliero Minimo (ore)</Label>
<Input
id="minDailyRestHours"
type="number"
value={formData.minDailyRestHours || 11}
onChange={(e) => setFormData({ ...formData, minDailyRestHours: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-min-daily-rest-hours"
/>
</div>
<div className="space-y-2">
<Label htmlFor="minDailyRestHoursReduced">Riposo Giornaliero Ridotto (ore)</Label>
<Input
id="minDailyRestHoursReduced"
type="number"
value={formData.minDailyRestHoursReduced || 9}
onChange={(e) => setFormData({ ...formData, minDailyRestHoursReduced: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-min-daily-rest-hours-reduced"
/>
<p className="text-sm text-muted-foreground">Deroga CCNL - max 12 volte/anno</p>
</div>
<div className="space-y-2">
<Label htmlFor="maxDailyRestReductionsPerMonth">Riduzioni Riposo Max al Mese</Label>
<Input
id="maxDailyRestReductionsPerMonth"
type="number"
value={formData.maxDailyRestReductionsPerMonth || 3}
onChange={(e) => setFormData({ ...formData, maxDailyRestReductionsPerMonth: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-daily-rest-reductions-per-month"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxDailyRestReductionsPerYear">Riduzioni Riposo Max all'Anno</Label>
<Input
id="maxDailyRestReductionsPerYear"
type="number"
value={formData.maxDailyRestReductionsPerYear || 12}
onChange={(e) => setFormData({ ...formData, maxDailyRestReductionsPerYear: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-max-daily-rest-reductions-per-year"
/>
</div>
<div className="space-y-2">
<Label htmlFor="minWeeklyRestHours">Riposo Settimanale Minimo (ore)</Label>
<Input
id="minWeeklyRestHours"
type="number"
value={formData.minWeeklyRestHours || 24}
onChange={(e) => setFormData({ ...formData, minWeeklyRestHours: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-min-weekly-rest-hours"
/>
</div>
<div className="space-y-2">
<Label htmlFor="pauseMinutesIfOver6Hours">Pausa se Turno {'>'} 6 ore (minuti)</Label>
<Input
id="pauseMinutesIfOver6Hours"
type="number"
value={formData.pauseMinutesIfOver6Hours || 10}
onChange={(e) => setFormData({ ...formData, pauseMinutesIfOver6Hours: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-pause-minutes-if-over-6-hours"
/>
</div>
</CardContent>
</Card>
{/* Maggiorazioni */}
<Card>
<CardHeader>
<CardTitle>Maggiorazioni Retributive</CardTitle>
<CardDescription>Percentuali maggiorazione per festivi, notturni e straordinari</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="holidayPayIncrease">Maggiorazione Festivi (%)</Label>
<Input
id="holidayPayIncrease"
type="number"
value={formData.holidayPayIncrease || 30}
onChange={(e) => setFormData({ ...formData, holidayPayIncrease: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-holiday-pay-increase"
/>
</div>
<div className="space-y-2">
<Label htmlFor="nightPayIncrease">Maggiorazione Notturni (%)</Label>
<Input
id="nightPayIncrease"
type="number"
value={formData.nightPayIncrease || 20}
onChange={(e) => setFormData({ ...formData, nightPayIncrease: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-night-pay-increase"
/>
</div>
<div className="space-y-2">
<Label htmlFor="overtimePayIncrease">Maggiorazione Straordinari (%)</Label>
<Input
id="overtimePayIncrease"
type="number"
value={formData.overtimePayIncrease || 15}
onChange={(e) => setFormData({ ...formData, overtimePayIncrease: parseInt(e.target.value) })}
disabled={!isEditing}
data-testid="input-overtime-pay-increase"
/>
</div>
</CardContent>
</Card>
{/* Buoni Pasto */}
<Card>
<CardHeader>
<CardTitle>Buoni Pasto</CardTitle>
<CardDescription>Configurazione ticket restaurant per turni superiori a soglia ore</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="mealVoucherEnabled">Buoni Pasto Attivi</Label>
<Switch
id="mealVoucherEnabled"
checked={formData.mealVoucherEnabled ?? true}
onCheckedChange={(checked) => setFormData({ ...formData, mealVoucherEnabled: checked })}
disabled={!isEditing}
data-testid="switch-meal-voucher-enabled"
/>
</div>
<p className="text-sm text-muted-foreground">
Abilita emissione buoni pasto automatici
</p>
</div>
<div className="space-y-2">
<Label htmlFor="mealVoucherAfterHours">Ore Minime per Buono Pasto</Label>
<Input
id="mealVoucherAfterHours"
type="number"
value={formData.mealVoucherAfterHours ?? 6}
onChange={(e) => setFormData({ ...formData, mealVoucherAfterHours: parseInt(e.target.value) })}
disabled={!isEditing || !formData.mealVoucherEnabled}
data-testid="input-meal-voucher-after-hours"
/>
<p className="text-sm text-muted-foreground">
Ore di turno necessarie per diritto al buono
</p>
</div>
<div className="space-y-2">
<Label htmlFor="mealVoucherAmount">Importo Buono Pasto ()</Label>
<Input
id="mealVoucherAmount"
type="number"
value={formData.mealVoucherAmount ?? 8}
onChange={(e) => setFormData({ ...formData, mealVoucherAmount: parseInt(e.target.value) })}
disabled={!isEditing || !formData.mealVoucherEnabled}
data-testid="input-meal-voucher-amount"
/>
<p className="text-sm text-muted-foreground">
Valore nominale ticket (facoltativo)
</p>
</div>
</CardContent>
</Card>
{/* Tipo Contratto */}
<Card>
<CardHeader>
<CardTitle>Tipo Contratto</CardTitle>
<CardDescription>Identificatore CCNL di riferimento</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="contractType">CCNL di Riferimento</Label>
<Input
id="contractType"
value={formData.contractType || "CCNL_VIGILANZA_2024"}
onChange={(e) => setFormData({ ...formData, contractType: e.target.value })}
disabled={!isEditing}
data-testid="input-contract-type"
/>
</div>
</CardContent>
</Card>
</form>
</div>
);
}