Add system parameters configuration for contract rules
Introduce a new section in the application for managing contract parameters. This includes backend API endpoints for fetching and updating contract parameters, schema definitions for these parameters, and a frontend page to display and edit them. Admins and coordinators can now configure various aspects of contract rules and shift planning. 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/Z1LDqzu
This commit is contained in:
parent
0203c9694d
commit
b81f1253ac
4
.replit
4
.replit
@ -15,6 +15,10 @@ run = ["npm", "run", "start"]
|
|||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 32847
|
||||||
|
externalPort = 3003
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 33035
|
localPort = 33035
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import Notifications from "@/pages/notifications";
|
|||||||
import Users from "@/pages/users";
|
import Users from "@/pages/users";
|
||||||
import Planning from "@/pages/planning";
|
import Planning from "@/pages/planning";
|
||||||
import Vehicles from "@/pages/vehicles";
|
import Vehicles from "@/pages/vehicles";
|
||||||
|
import Parameters from "@/pages/parameters";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
@ -39,6 +40,7 @@ function Router() {
|
|||||||
<Route path="/reports" component={Reports} />
|
<Route path="/reports" component={Reports} />
|
||||||
<Route path="/notifications" component={Notifications} />
|
<Route path="/notifications" component={Notifications} />
|
||||||
<Route path="/users" component={Users} />
|
<Route path="/users" component={Users} />
|
||||||
|
<Route path="/parameters" component={Parameters} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
|
|||||||
@ -84,6 +84,12 @@ const menuItems = [
|
|||||||
icon: UserCog,
|
icon: UserCog,
|
||||||
roles: ["admin"],
|
roles: ["admin"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Parametri",
|
||||||
|
url: "/parameters",
|
||||||
|
icon: Settings,
|
||||||
|
roles: ["admin", "coordinator"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
|
|||||||
370
client/src/pages/parameters.tsx
Normal file
370
client/src/pages/parameters.tsx
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
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 { 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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -347,6 +347,66 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============= CONTRACT PARAMETERS ROUTES =============
|
||||||
|
app.get("/api/contract-parameters", isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const currentUserId = getUserId(req);
|
||||||
|
const currentUser = await storage.getUser(currentUserId);
|
||||||
|
|
||||||
|
// Only admins and coordinators can view parameters
|
||||||
|
if (currentUser?.role !== "admin" && currentUser?.role !== "coordinator") {
|
||||||
|
return res.status(403).json({ message: "Forbidden: Admin or Coordinator access required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = await storage.getContractParameters();
|
||||||
|
|
||||||
|
// Se non esistono parametri, creali con valori di default CCNL
|
||||||
|
if (!params) {
|
||||||
|
params = await storage.createContractParameters({
|
||||||
|
contractType: "CCNL_VIGILANZA_2024",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(params);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching contract parameters:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch contract parameters" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/api/contract-parameters/:id", isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const currentUserId = getUserId(req);
|
||||||
|
const currentUser = await storage.getUser(currentUserId);
|
||||||
|
|
||||||
|
// Only admins can update parameters
|
||||||
|
if (currentUser?.role !== "admin") {
|
||||||
|
return res.status(403).json({ message: "Forbidden: Admin access required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request body with insert schema
|
||||||
|
const { insertContractParametersSchema } = await import("@shared/schema");
|
||||||
|
const validationResult = insertContractParametersSchema.partial().safeParse(req.body);
|
||||||
|
|
||||||
|
if (!validationResult.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: "Invalid parameters data",
|
||||||
|
errors: validationResult.error.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await storage.updateContractParameters(req.params.id, validationResult.data);
|
||||||
|
if (!updated) {
|
||||||
|
return res.status(404).json({ message: "Contract parameters not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating contract parameters:", error);
|
||||||
|
res.status(500).json({ message: "Failed to update contract parameters" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============= CERTIFICATION ROUTES =============
|
// ============= CERTIFICATION ROUTES =============
|
||||||
app.post("/api/certifications", isAuthenticated, async (req, res) => {
|
app.post("/api/certifications", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
holidayAssignments,
|
holidayAssignments,
|
||||||
absences,
|
absences,
|
||||||
absenceAffectedShifts,
|
absenceAffectedShifts,
|
||||||
|
contractParameters,
|
||||||
type User,
|
type User,
|
||||||
type UpsertUser,
|
type UpsertUser,
|
||||||
type Guard,
|
type Guard,
|
||||||
@ -45,6 +46,8 @@ import {
|
|||||||
type InsertAbsence,
|
type InsertAbsence,
|
||||||
type AbsenceAffectedShift,
|
type AbsenceAffectedShift,
|
||||||
type InsertAbsenceAffectedShift,
|
type InsertAbsenceAffectedShift,
|
||||||
|
type ContractParameters,
|
||||||
|
type InsertContractParameters,
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { eq, and, gte, lte, desc } from "drizzle-orm";
|
import { eq, and, gte, lte, desc } from "drizzle-orm";
|
||||||
@ -126,6 +129,11 @@ export interface IStorage {
|
|||||||
getAffectedShiftsByAbsence(absenceId: string): Promise<AbsenceAffectedShift[]>;
|
getAffectedShiftsByAbsence(absenceId: string): Promise<AbsenceAffectedShift[]>;
|
||||||
createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise<AbsenceAffectedShift>;
|
createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise<AbsenceAffectedShift>;
|
||||||
deleteAbsenceAffectedShift(id: string): Promise<void>;
|
deleteAbsenceAffectedShift(id: string): Promise<void>;
|
||||||
|
|
||||||
|
// Contract Parameters operations
|
||||||
|
getContractParameters(): Promise<ContractParameters | undefined>;
|
||||||
|
createContractParameters(params: InsertContractParameters): Promise<ContractParameters>;
|
||||||
|
updateContractParameters(id: string, params: Partial<InsertContractParameters>): Promise<ContractParameters | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DatabaseStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
@ -547,6 +555,26 @@ export class DatabaseStorage implements IStorage {
|
|||||||
async deleteAbsenceAffectedShift(id: string): Promise<void> {
|
async deleteAbsenceAffectedShift(id: string): Promise<void> {
|
||||||
await db.delete(absenceAffectedShifts).where(eq(absenceAffectedShifts.id, id));
|
await db.delete(absenceAffectedShifts).where(eq(absenceAffectedShifts.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contract Parameters operations
|
||||||
|
async getContractParameters(): Promise<ContractParameters | undefined> {
|
||||||
|
const params = await db.select().from(contractParameters).limit(1);
|
||||||
|
return params[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createContractParameters(params: InsertContractParameters): Promise<ContractParameters> {
|
||||||
|
const [newParams] = await db.insert(contractParameters).values(params).returning();
|
||||||
|
return newParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateContractParameters(id: string, params: Partial<InsertContractParameters>): Promise<ContractParameters | undefined> {
|
||||||
|
const [updated] = await db
|
||||||
|
.update(contractParameters)
|
||||||
|
.set(params)
|
||||||
|
.where(eq(contractParameters.id, id))
|
||||||
|
.returning();
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = new DatabaseStorage();
|
export const storage = new DatabaseStorage();
|
||||||
|
|||||||
@ -292,8 +292,14 @@ export const contractParameters = pgTable("contract_parameters", {
|
|||||||
|
|
||||||
// Riposi obbligatori
|
// Riposi obbligatori
|
||||||
minDailyRestHours: integer("min_daily_rest_hours").notNull().default(11),
|
minDailyRestHours: integer("min_daily_rest_hours").notNull().default(11),
|
||||||
|
minDailyRestHoursReduced: integer("min_daily_rest_hours_reduced").notNull().default(9), // Deroga CCNL
|
||||||
|
maxDailyRestReductionsPerMonth: integer("max_daily_rest_reductions_per_month").notNull().default(3),
|
||||||
|
maxDailyRestReductionsPerYear: integer("max_daily_rest_reductions_per_year").notNull().default(12),
|
||||||
minWeeklyRestHours: integer("min_weekly_rest_hours").notNull().default(24),
|
minWeeklyRestHours: integer("min_weekly_rest_hours").notNull().default(24),
|
||||||
|
|
||||||
|
// Pause obbligatorie
|
||||||
|
pauseMinutesIfOver6Hours: integer("pause_minutes_if_over_6_hours").notNull().default(10),
|
||||||
|
|
||||||
// Limiti notturni (22:00-06:00)
|
// Limiti notturni (22:00-06:00)
|
||||||
maxNightHoursPerWeek: integer("max_night_hours_per_week").default(48),
|
maxNightHoursPerWeek: integer("max_night_hours_per_week").default(48),
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user