Compare commits

..

No commits in common. "743ec4261faf19b9a819c1abccb4b90d57453592" and "09ff76e02dcb77ac90bfc9d50fd7bbfa8d70a423" have entirely different histories.

12 changed files with 118 additions and 2271 deletions

View File

@ -9,7 +9,6 @@ import { AppSidebar } from "@/components/app-sidebar";
import { useAuth } from "@/hooks/useAuth";
import NotFound from "@/pages/not-found";
import Landing from "@/pages/landing";
import Login from "@/pages/login";
import Dashboard from "@/pages/dashboard";
import Guards from "@/pages/guards";
import Sites from "@/pages/sites";
@ -18,15 +17,12 @@ import Reports from "@/pages/reports";
import Notifications from "@/pages/notifications";
import Users from "@/pages/users";
import Planning from "@/pages/planning";
import Vehicles from "@/pages/vehicles";
import Parameters from "@/pages/parameters";
function Router() {
const { isAuthenticated, isLoading } = useAuth();
return (
<Switch>
<Route path="/login" component={Login} />
{isLoading || !isAuthenticated ? (
<Route path="/" component={Landing} />
) : (
@ -34,13 +30,11 @@ function Router() {
<Route path="/" component={Dashboard} />
<Route path="/guards" component={Guards} />
<Route path="/sites" component={Sites} />
<Route path="/vehicles" component={Vehicles} />
<Route path="/shifts" component={Shifts} />
<Route path="/planning" component={Planning} />
<Route path="/reports" component={Reports} />
<Route path="/notifications" component={Notifications} />
<Route path="/users" component={Users} />
<Route path="/parameters" component={Parameters} />
</>
)}
<Route component={NotFound} />

View File

@ -9,7 +9,6 @@ import {
LogOut,
UserCog,
ClipboardList,
Car,
} from "lucide-react";
import { Link, useLocation } from "wouter";
import {
@ -60,12 +59,6 @@ const menuItems = [
icon: MapPin,
roles: ["admin", "coordinator", "client"],
},
{
title: "Parco Automezzi",
url: "/vehicles",
icon: Car,
roles: ["admin", "coordinator"],
},
{
title: "Report",
url: "/reports",
@ -84,12 +77,6 @@ const menuItems = [
icon: UserCog,
roles: ["admin"],
},
{
title: "Parametri",
url: "/parameters",
icon: Settings,
roles: ["admin", "coordinator"],
},
];
export function AppSidebar() {

View File

@ -1,158 +0,0 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { useToast } from "@/hooks/use-toast";
import { Shield, Loader2 } from "lucide-react";
const loginSchema = z.object({
email: z.string().email("Email non valida"),
password: z.string().min(3, "Password troppo corta"),
});
type LoginForm = z.infer<typeof loginSchema>;
export default function Login() {
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const form = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: "",
password: "",
},
});
const loginMutation = useMutation({
mutationFn: async (data: LoginForm) => {
const formData = new URLSearchParams();
formData.append("email", data.email);
formData.append("password", data.password);
const response = await fetch("/api/local-login", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
credentials: "include",
});
if (!response.ok) {
throw new Error("Credenziali non valide");
}
return response.json();
},
onSuccess: () => {
toast({
title: "Accesso effettuato",
description: "Benvenuto nel sistema VigilanzaTurni",
});
window.location.href = "/";
},
onError: () => {
toast({
title: "Errore di accesso",
description: "Email o password non corretti",
variant: "destructive",
});
},
onSettled: () => {
setIsLoading(false);
},
});
const onSubmit = (data: LoginForm) => {
setIsLoading(true);
loginMutation.mutate(data);
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-background to-muted p-4">
<Card className="w-full max-w-md">
<CardHeader className="space-y-3 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
<Shield className="h-8 w-8 text-primary" />
</div>
<CardTitle className="text-2xl font-bold">VigilanzaTurni</CardTitle>
<CardDescription>
Accedi al sistema di gestione turni
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="admin@vt.alfacom.it"
data-testid="input-email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
data-testid="input-password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={isLoading}
data-testid="button-login"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Accesso in corso...
</>
) : (
"Accedi"
)}
</Button>
</form>
</Form>
<div className="mt-6 p-4 bg-muted rounded-md text-sm text-muted-foreground">
<p className="font-semibold mb-1">Credenziali di default:</p>
<p>Email: admin@vt.alfacom.it</p>
<p>Password: admin123</p>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,370 +0,0 @@
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>
);
}

View File

@ -1,11 +1,7 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { useAuth } from "@/hooks/useAuth";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertUserFormSchema, updateUserFormSchema, type User } from "@shared/schema";
import { z } from "zod";
import { type User } from "@shared/schema";
import {
Card,
CardContent,
@ -21,24 +17,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
@ -46,21 +24,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useToast } from "@/hooks/use-toast";
import { Shield, UserCog, Users as UsersIcon, UserCheck, Plus, Pencil, Trash2, Loader2 } from "lucide-react";
import { Shield, UserCog, Users as UsersIcon, UserCheck } from "lucide-react";
const roleIcons = {
admin: Shield,
@ -83,128 +51,35 @@ const roleColors = {
client: "bg-orange-500/10 text-orange-500 border-orange-500/20",
};
type CreateUserForm = z.infer<typeof insertUserFormSchema>;
type UpdateUserForm = z.infer<typeof updateUserFormSchema>;
export default function Users() {
const { user: currentUser } = useAuth();
const { toast } = useToast();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const { data: users, isLoading } = useQuery<User[]>({
queryKey: ["/api/users"],
});
const createForm = useForm<CreateUserForm>({
resolver: zodResolver(insertUserFormSchema),
defaultValues: {
email: "",
firstName: "",
lastName: "",
password: "",
role: "guard",
},
});
const editForm = useForm<UpdateUserForm>({
resolver: zodResolver(updateUserFormSchema.required({ role: true })),
defaultValues: {
email: "",
firstName: "",
lastName: "",
password: "",
role: "guard",
},
});
const createUserMutation = useMutation({
mutationFn: async (data: CreateUserForm) => {
return apiRequest("POST", "/api/users", data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
toast({
title: "Utente creato",
description: "L'utente è stato creato con successo.",
});
setCreateDialogOpen(false);
createForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile creare l'utente.",
variant: "destructive",
});
},
});
const updateUserMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: UpdateUserForm }) => {
return apiRequest("PUT", `/api/users/${id}`, data);
const updateRoleMutation = useMutation({
mutationFn: async ({ userId, role }: { userId: string; role: string }) => {
return apiRequest("PATCH", `/api/users/${userId}`, { role });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
queryClient.invalidateQueries({ queryKey: ["/api/auth/user"] });
toast({
title: "Utente aggiornato",
description: "L'utente è stato modificato con successo.",
title: "Ruolo aggiornato",
description: "Il ruolo dell'utente è stato modificato con successo.",
});
setEditDialogOpen(false);
setSelectedUser(null);
editForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile aggiornare l'utente.",
variant: "destructive",
});
},
});
const deleteUserMutation = useMutation({
mutationFn: async (id: string) => {
return apiRequest("DELETE", `/api/users/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
toast({
title: "Utente eliminato",
description: "L'utente è stato eliminato con successo.",
});
setDeleteDialogOpen(false);
setSelectedUser(null);
},
onError: () => {
toast({
title: "Errore",
description: "Impossibile eliminare l'utente.",
description: "Impossibile aggiornare il ruolo dell'utente.",
variant: "destructive",
});
},
});
const handleEdit = (user: User) => {
setSelectedUser(user);
editForm.reset({
email: user.email || "",
firstName: user.firstName || "",
lastName: user.lastName || "",
password: "",
role: user.role,
});
setEditDialogOpen(true);
};
const handleDelete = (user: User) => {
setSelectedUser(user);
setDeleteDialogOpen(true);
};
if (isLoading) {
return (
<div className="space-y-6">
@ -236,7 +111,6 @@ export default function Users() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold" data-testid="text-page-title">
Gestione Utenti
@ -245,11 +119,6 @@ export default function Users() {
Gestisci utenti e permessi del sistema VigilanzaTurni
</p>
</div>
<Button onClick={() => setCreateDialogOpen(true)} data-testid="button-add-user">
<Plus className="h-4 w-4 mr-2" />
Aggiungi Utente
</Button>
</div>
<Card>
<CardHeader>
@ -264,8 +133,8 @@ export default function Users() {
<TableRow>
<TableHead>Utente</TableHead>
<TableHead>Email</TableHead>
<TableHead>Ruolo</TableHead>
<TableHead className="text-right">Azioni</TableHead>
<TableHead>Ruolo Attuale</TableHead>
<TableHead>Modifica Ruolo</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -312,26 +181,45 @@ export default function Users() {
{roleLabels[user.role]}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(user)}
data-testid={`button-edit-${user.id}`}
<TableCell>
<Select
value={user.role}
onValueChange={(role) =>
updateRoleMutation.mutate({ userId: user.id, role })
}
disabled={isCurrentUser || updateRoleMutation.isPending}
data-testid={`select-role-${user.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(user)}
disabled={isCurrentUser}
data-testid={`button-delete-${user.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin" data-testid={`option-admin-${user.id}`}>
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Admin
</div>
</SelectItem>
<SelectItem value="coordinator" data-testid={`option-coordinator-${user.id}`}>
<div className="flex items-center gap-2">
<UserCog className="h-4 w-4" />
Coordinatore
</div>
</SelectItem>
<SelectItem value="guard" data-testid={`option-guard-${user.id}`}>
<div className="flex items-center gap-2">
<UsersIcon className="h-4 w-4" />
Guardia
</div>
</SelectItem>
<SelectItem value="client" data-testid={`option-client-${user.id}`}>
<div className="flex items-center gap-2">
<UserCheck className="h-4 w-4" />
Cliente
</div>
</SelectItem>
</SelectContent>
</Select>
</TableCell>
</TableRow>
);
@ -346,292 +234,6 @@ export default function Users() {
)}
</CardContent>
</Card>
{/* Create User Dialog */}
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent data-testid="dialog-create-user">
<DialogHeader>
<DialogTitle>Crea Nuovo Utente</DialogTitle>
<DialogDescription>
Inserisci i dati del nuovo utente. La password deve essere di almeno 6 caratteri.
</DialogDescription>
</DialogHeader>
<Form {...createForm}>
<form onSubmit={createForm.handleSubmit((data) => createUserMutation.mutate(data))} className="space-y-4">
<FormField
control={createForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="utente@esempio.it" data-testid="input-create-email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={createForm.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input placeholder="Mario" data-testid="input-create-firstname" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Cognome</FormLabel>
<FormControl>
<Input placeholder="Rossi" data-testid="input-create-lastname" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={createForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" data-testid="input-create-password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Ruolo</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-create-role">
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Admin
</div>
</SelectItem>
<SelectItem value="coordinator">
<div className="flex items-center gap-2">
<UserCog className="h-4 w-4" />
Coordinatore
</div>
</SelectItem>
<SelectItem value="guard">
<div className="flex items-center gap-2">
<UsersIcon className="h-4 w-4" />
Guardia
</div>
</SelectItem>
<SelectItem value="client">
<div className="flex items-center gap-2">
<UserCheck className="h-4 w-4" />
Cliente
</div>
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setCreateDialogOpen(false)} data-testid="button-create-cancel">
Annulla
</Button>
<Button type="submit" disabled={createUserMutation.isPending} data-testid="button-create-submit">
{createUserMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creazione...
</>
) : (
"Crea Utente"
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Edit User Dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent data-testid="dialog-edit-user">
<DialogHeader>
<DialogTitle>Modifica Utente</DialogTitle>
<DialogDescription>
Modifica i dati dell'utente. Lascia la password vuota per mantenerla invariata.
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit((data) => selectedUser && updateUserMutation.mutate({ id: selectedUser.id, data }))} className="space-y-4">
<FormField
control={editForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="utente@esempio.it" data-testid="input-edit-email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input placeholder="Mario" data-testid="input-edit-firstname" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Cognome</FormLabel>
<FormControl>
<Input placeholder="Rossi" data-testid="input-edit-lastname" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (opzionale)</FormLabel>
<FormControl>
<Input type="password" placeholder="Lascia vuoto per non cambiare" data-testid="input-edit-password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Ruolo</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-edit-role">
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Admin
</div>
</SelectItem>
<SelectItem value="coordinator">
<div className="flex items-center gap-2">
<UserCog className="h-4 w-4" />
Coordinatore
</div>
</SelectItem>
<SelectItem value="guard">
<div className="flex items-center gap-2">
<UsersIcon className="h-4 w-4" />
Guardia
</div>
</SelectItem>
<SelectItem value="client">
<div className="flex items-center gap-2">
<UserCheck className="h-4 w-4" />
Cliente
</div>
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setEditDialogOpen(false)} data-testid="button-edit-cancel">
Annulla
</Button>
<Button type="submit" disabled={updateUserMutation.isPending} data-testid="button-edit-submit">
{updateUserMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Salvataggio...
</>
) : (
"Salva Modifiche"
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent data-testid="dialog-delete-user">
<AlertDialogHeader>
<AlertDialogTitle>Conferma Eliminazione</AlertDialogTitle>
<AlertDialogDescription>
Sei sicuro di voler eliminare l'utente <strong>{selectedUser?.firstName} {selectedUser?.lastName}</strong>?
Questa azione non può essere annullata.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-delete-cancel">Annulla</AlertDialogCancel>
<AlertDialogAction
onClick={() => selectedUser && deleteUserMutation.mutate(selectedUser.id)}
className="bg-destructive hover:bg-destructive/90"
data-testid="button-delete-confirm"
>
{deleteUserMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Eliminazione...
</>
) : (
"Elimina"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@ -1,803 +0,0 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertVehicleSchema, type Vehicle, type Guard } from "@shared/schema";
import { z } from "zod";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/hooks/use-toast";
import { Car, Plus, Pencil, Trash2, Loader2 } from "lucide-react";
const vehicleTypeLabels = {
car: "Auto",
van: "Furgone",
motorcycle: "Moto",
suv: "SUV",
};
const vehicleStatusLabels = {
available: "Disponibile",
in_use: "In uso",
maintenance: "In manutenzione",
out_of_service: "Fuori servizio",
};
const vehicleStatusColors = {
available: "bg-green-500/10 text-green-500 border-green-500/20",
in_use: "bg-blue-500/10 text-blue-500 border-blue-500/20",
maintenance: "bg-orange-500/10 text-orange-500 border-orange-500/20",
out_of_service: "bg-red-500/10 text-red-500 border-red-500/20",
};
type VehicleForm = z.infer<typeof insertVehicleSchema>;
export default function Vehicles() {
const { toast } = useToast();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
const { data: vehicles, isLoading: isLoadingVehicles } = useQuery<Vehicle[]>({
queryKey: ["/api/vehicles"],
});
const { data: guards } = useQuery<Guard[]>({
queryKey: ["/api/guards"],
});
const createForm = useForm<VehicleForm>({
resolver: zodResolver(insertVehicleSchema.extend({
year: z.number().min(1900).max(new Date().getFullYear() + 1).optional().or(z.literal(null)),
mileage: z.number().min(0).optional().or(z.literal(null)),
})),
defaultValues: {
licensePlate: "",
brand: "",
model: "",
vehicleType: "car",
year: undefined,
assignedGuardId: null,
status: "available",
lastMaintenanceDate: null,
nextMaintenanceDate: null,
mileage: undefined,
notes: null,
},
});
const editForm = useForm<VehicleForm>({
resolver: zodResolver(insertVehicleSchema.extend({
year: z.number().min(1900).max(new Date().getFullYear() + 1).optional().or(z.literal(null)),
mileage: z.number().min(0).optional().or(z.literal(null)),
})),
defaultValues: {
licensePlate: "",
brand: "",
model: "",
vehicleType: "car",
year: undefined,
assignedGuardId: null,
status: "available",
lastMaintenanceDate: null,
nextMaintenanceDate: null,
mileage: undefined,
notes: null,
},
});
const createVehicleMutation = useMutation({
mutationFn: async (data: VehicleForm) => {
return apiRequest("POST", "/api/vehicles", data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/vehicles"] });
toast({
title: "Veicolo creato",
description: "Il veicolo è stato aggiunto con successo.",
});
setCreateDialogOpen(false);
createForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile creare il veicolo.",
variant: "destructive",
});
},
});
const updateVehicleMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: VehicleForm }) => {
return apiRequest("PATCH", `/api/vehicles/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/vehicles"] });
toast({
title: "Veicolo aggiornato",
description: "Il veicolo è stato modificato con successo.",
});
setEditDialogOpen(false);
setSelectedVehicle(null);
editForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile aggiornare il veicolo.",
variant: "destructive",
});
},
});
const deleteVehicleMutation = useMutation({
mutationFn: async (id: string) => {
return apiRequest("DELETE", `/api/vehicles/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/vehicles"] });
toast({
title: "Veicolo eliminato",
description: "Il veicolo è stato eliminato con successo.",
});
setDeleteDialogOpen(false);
setSelectedVehicle(null);
},
onError: () => {
toast({
title: "Errore",
description: "Impossibile eliminare il veicolo.",
variant: "destructive",
});
},
});
const handleEdit = (vehicle: Vehicle) => {
setSelectedVehicle(vehicle);
editForm.reset({
licensePlate: vehicle.licensePlate,
brand: vehicle.brand,
model: vehicle.model,
vehicleType: vehicle.vehicleType,
year: vehicle.year ?? undefined,
assignedGuardId: vehicle.assignedGuardId,
status: vehicle.status,
lastMaintenanceDate: vehicle.lastMaintenanceDate,
nextMaintenanceDate: vehicle.nextMaintenanceDate,
mileage: vehicle.mileage ?? undefined,
notes: vehicle.notes,
});
setEditDialogOpen(true);
};
const handleDelete = (vehicle: Vehicle) => {
setSelectedVehicle(vehicle);
setDeleteDialogOpen(true);
};
if (isLoadingVehicles) {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Parco Automezzi</h1>
<p className="text-muted-foreground">Gestione veicoli aziendali</p>
</div>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-3 w-32" />
</div>
<Skeleton className="h-9 w-32" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold" data-testid="text-page-title">
Parco Automezzi
</h1>
<p className="text-muted-foreground">
Gestisci i veicoli aziendali e le assegnazioni
</p>
</div>
<Button onClick={() => setCreateDialogOpen(true)} data-testid="button-add-vehicle">
<Plus className="h-4 w-4 mr-2" />
Aggiungi Veicolo
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Veicoli Registrati</CardTitle>
<CardDescription>
{vehicles?.length || 0} veicoli nel parco aziendale
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Targa</TableHead>
<TableHead>Veicolo</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Stato</TableHead>
<TableHead>Assegnato a</TableHead>
<TableHead className="text-right">Azioni</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vehicles?.map((vehicle) => {
const assignedGuard = guards?.find(g => g.id === vehicle.assignedGuardId);
return (
<TableRow key={vehicle.id} data-testid={`row-vehicle-${vehicle.id}`}>
<TableCell className="font-medium" data-testid={`text-plate-${vehicle.id}`}>
{vehicle.licensePlate}
</TableCell>
<TableCell>
<div>
<p className="font-medium">{vehicle.brand} {vehicle.model}</p>
{vehicle.year && <p className="text-sm text-muted-foreground">Anno {vehicle.year}</p>}
</div>
</TableCell>
<TableCell>{vehicleTypeLabels[vehicle.vehicleType]}</TableCell>
<TableCell>
<Badge
variant="outline"
className={vehicleStatusColors[vehicle.status]}
data-testid={`badge-status-${vehicle.id}`}
>
{vehicleStatusLabels[vehicle.status]}
</Badge>
</TableCell>
<TableCell>
{assignedGuard ? (
<span className="text-sm">{assignedGuard.badgeNumber}</span>
) : (
<span className="text-sm text-muted-foreground">Non assegnato</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(vehicle)}
data-testid={`button-edit-${vehicle.id}`}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(vehicle)}
data-testid={`button-delete-${vehicle.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{vehicles?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<Car className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nessun veicolo registrato</p>
</div>
)}
</CardContent>
</Card>
{/* Create Vehicle Dialog */}
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto" data-testid="dialog-create-vehicle">
<DialogHeader>
<DialogTitle>Aggiungi Nuovo Veicolo</DialogTitle>
<DialogDescription>
Inserisci i dati del veicolo da aggiungere al parco aziendale.
</DialogDescription>
</DialogHeader>
<Form {...createForm}>
<form onSubmit={createForm.handleSubmit((data) => createVehicleMutation.mutate(data))} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={createForm.control}
name="licensePlate"
render={({ field }) => (
<FormItem>
<FormLabel>Targa *</FormLabel>
<FormControl>
<Input placeholder="AB123CD" data-testid="input-create-plate" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="vehicleType"
render={({ field }) => (
<FormItem>
<FormLabel>Tipo *</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-create-type">
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="car">Auto</SelectItem>
<SelectItem value="van">Furgone</SelectItem>
<SelectItem value="motorcycle">Moto</SelectItem>
<SelectItem value="suv">SUV</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<FormField
control={createForm.control}
name="brand"
render={({ field }) => (
<FormItem>
<FormLabel>Marca *</FormLabel>
<FormControl>
<Input placeholder="Fiat" data-testid="input-create-brand" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="model"
render={({ field }) => (
<FormItem>
<FormLabel>Modello *</FormLabel>
<FormControl>
<Input placeholder="500" data-testid="input-create-model" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="year"
render={({ field }) => (
<FormItem>
<FormLabel>Anno</FormLabel>
<FormControl>
<Input
type="number"
placeholder="2024"
data-testid="input-create-year"
{...field}
value={field.value ?? ""}
onChange={e => field.onChange(e.target.value ? parseInt(e.target.value) : null)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={createForm.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Stato *</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-create-status">
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="available">Disponibile</SelectItem>
<SelectItem value="in_use">In uso</SelectItem>
<SelectItem value="maintenance">In manutenzione</SelectItem>
<SelectItem value="out_of_service">Fuori servizio</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="assignedGuardId"
render={({ field }) => (
<FormItem>
<FormLabel>Assegnato a</FormLabel>
<Select onValueChange={field.onChange} value={field.value || ""} data-testid="select-create-guard">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleziona guardia" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="">Nessuno</SelectItem>
{guards?.map(guard => (
<SelectItem key={guard.id} value={guard.id}>
{guard.badgeNumber}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={createForm.control}
name="mileage"
render={({ field }) => (
<FormItem>
<FormLabel>Chilometraggio</FormLabel>
<FormControl>
<Input
type="number"
placeholder="50000"
data-testid="input-create-mileage"
{...field}
value={field.value ?? ""}
onChange={e => field.onChange(e.target.value ? parseInt(e.target.value) : null)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Note</FormLabel>
<FormControl>
<Textarea
placeholder="Note aggiuntive sul veicolo..."
data-testid="input-create-notes"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setCreateDialogOpen(false)} data-testid="button-create-cancel">
Annulla
</Button>
<Button type="submit" disabled={createVehicleMutation.isPending} data-testid="button-create-submit">
{createVehicleMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creazione...
</>
) : (
"Crea Veicolo"
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Edit Vehicle Dialog - Same structure as create */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto" data-testid="dialog-edit-vehicle">
<DialogHeader>
<DialogTitle>Modifica Veicolo</DialogTitle>
<DialogDescription>
Modifica i dati del veicolo {selectedVehicle?.licensePlate}.
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit((data) => selectedVehicle && updateVehicleMutation.mutate({ id: selectedVehicle.id, data }))} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="licensePlate"
render={({ field }) => (
<FormItem>
<FormLabel>Targa *</FormLabel>
<FormControl>
<Input placeholder="AB123CD" data-testid="input-edit-plate" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="vehicleType"
render={({ field }) => (
<FormItem>
<FormLabel>Tipo *</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-edit-type">
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="car">Auto</SelectItem>
<SelectItem value="van">Furgone</SelectItem>
<SelectItem value="motorcycle">Moto</SelectItem>
<SelectItem value="suv">SUV</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<FormField
control={editForm.control}
name="brand"
render={({ field }) => (
<FormItem>
<FormLabel>Marca *</FormLabel>
<FormControl>
<Input placeholder="Fiat" data-testid="input-edit-brand" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="model"
render={({ field }) => (
<FormItem>
<FormLabel>Modello *</FormLabel>
<FormControl>
<Input placeholder="500" data-testid="input-edit-model" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="year"
render={({ field }) => (
<FormItem>
<FormLabel>Anno</FormLabel>
<FormControl>
<Input
type="number"
placeholder="2024"
data-testid="input-edit-year"
{...field}
value={field.value ?? ""}
onChange={e => field.onChange(e.target.value ? parseInt(e.target.value) : null)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Stato *</FormLabel>
<Select onValueChange={field.onChange} value={field.value} data-testid="select-edit-status">
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="available">Disponibile</SelectItem>
<SelectItem value="in_use">In uso</SelectItem>
<SelectItem value="maintenance">In manutenzione</SelectItem>
<SelectItem value="out_of_service">Fuori servizio</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="assignedGuardId"
render={({ field }) => (
<FormItem>
<FormLabel>Assegnato a</FormLabel>
<Select onValueChange={field.onChange} value={field.value || ""} data-testid="select-edit-guard">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleziona guardia" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="">Nessuno</SelectItem>
{guards?.map(guard => (
<SelectItem key={guard.id} value={guard.id}>
{guard.badgeNumber}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="mileage"
render={({ field }) => (
<FormItem>
<FormLabel>Chilometraggio</FormLabel>
<FormControl>
<Input
type="number"
placeholder="50000"
data-testid="input-edit-mileage"
{...field}
value={field.value ?? ""}
onChange={e => field.onChange(e.target.value ? parseInt(e.target.value) : null)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Note</FormLabel>
<FormControl>
<Textarea
placeholder="Note aggiuntive sul veicolo..."
data-testid="input-edit-notes"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setEditDialogOpen(false)} data-testid="button-edit-cancel">
Annulla
</Button>
<Button type="submit" disabled={updateVehicleMutation.isPending} data-testid="button-edit-submit">
{updateVehicleMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Salvataggio...
</>
) : (
"Salva Modifiche"
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent data-testid="dialog-delete-vehicle">
<AlertDialogHeader>
<AlertDialogTitle>Conferma Eliminazione</AlertDialogTitle>
<AlertDialogDescription>
Sei sicuro di voler eliminare il veicolo <strong>{selectedVehicle?.licensePlate}</strong>?
Questa azione non può essere annullata.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-delete-cancel">Annulla</AlertDialogCancel>
<AlertDialogAction
onClick={() => selectedVehicle && deleteVehicleMutation.mutate(selectedVehicle.id)}
className="bg-destructive hover:bg-destructive/90"
data-testid="button-delete-confirm"
>
{deleteVehicleMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Eliminazione...
</>
) : (
"Elimina"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

41
package-lock.json generated
View File

@ -40,10 +40,8 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/memoizee": "^0.4.12",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -3423,15 +3421,6 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -3864,20 +3853,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -6103,20 +6078,12 @@
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
"integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",

View File

@ -42,10 +42,8 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/memoizee": "^0.4.12",
"@types/pg": "^8.15.5",
"bcrypt": "^6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",

View File

@ -34,38 +34,18 @@ export function getSession() {
async function initDefaultAdmin() {
try {
const bcrypt = await import("bcrypt");
// Verifica se esiste già un admin
const users = await storage.getAllUsers();
const existingAdmin = users.find((u: any) => u.email === DEFAULT_ADMIN_EMAIL);
if (existingAdmin) {
// Admin esiste: controlla se ha passwordHash
if (!existingAdmin.passwordHash) {
console.log(`🔄 [LocalAuth] Aggiornamento password hash per admin esistente...`);
const passwordHash = await bcrypt.hash(DEFAULT_ADMIN_PASSWORD, 10);
await storage.upsertUser({
id: existingAdmin.id,
email: existingAdmin.email,
firstName: existingAdmin.firstName || "Admin",
lastName: existingAdmin.lastName || "Sistema",
profileImageUrl: existingAdmin.profileImageUrl,
passwordHash,
});
console.log(`✅ [LocalAuth] Password hash aggiornato per: ${DEFAULT_ADMIN_EMAIL}`);
}
} else {
// Admin non esiste: crealo
const passwordHash = await bcrypt.hash(DEFAULT_ADMIN_PASSWORD, 10);
const adminExists = users.some((u: any) => u.email === DEFAULT_ADMIN_EMAIL);
if (!adminExists) {
// Crea utente admin di default
await storage.upsertUser({
id: DEFAULT_ADMIN_ID,
email: DEFAULT_ADMIN_EMAIL,
firstName: "Admin",
lastName: "Sistema",
profileImageUrl: null,
passwordHash,
});
// Imposta ruolo admin
@ -87,31 +67,23 @@ export async function setupLocalAuth(app: Express) {
// Inizializza admin di default
await initDefaultAdmin();
// Strategia passport-local con password hash bcrypt
// Strategia passport-local
passport.use(new LocalStrategy(
{ usernameField: "email" },
async (email, password, done) => {
try {
// Per demo: accetta credenziali admin di default
if (email === DEFAULT_ADMIN_EMAIL && password === DEFAULT_ADMIN_PASSWORD) {
const users = await storage.getAllUsers();
const user = users.find((u: any) => u.email === email);
const admin = users.find((u: any) => u.email === DEFAULT_ADMIN_EMAIL);
if (!user) {
return done(null, false, { message: "Credenziali non valide" });
if (admin) {
return done(null, { id: admin.id, email: admin.email });
}
}
if (!user.passwordHash) {
// Credenziali non valide
return done(null, false, { message: "Credenziali non valide" });
}
// Verifica password con bcrypt
const bcrypt = await import("bcrypt");
const isValidPassword = await bcrypt.compare(password, user.passwordHash);
if (!isValidPassword) {
return done(null, false, { message: "Credenziali non valide" });
}
return done(null, { id: user.id, email: user.email });
} catch (error) {
return done(error);
}
@ -132,9 +104,10 @@ export async function setupLocalAuth(app: Express) {
}
});
// Route login GET (redirect a pagina login frontend)
// Route login GET (redirect auto-login per compatibilità)
app.get("/api/login", (req, res) => {
res.redirect("/login");
// Redirect a auto-login admin per demo
res.redirect("/api/auto-login-admin");
});
// Route login locale POST
@ -146,6 +119,42 @@ export async function setupLocalAuth(app: Express) {
});
});
// Route auto-login admin (solo per demo/sviluppo)
app.get("/api/auto-login-admin", async (req, res) => {
if (process.env.NODE_ENV !== 'production') {
console.warn("⚠️ [LocalAuth] Auto-login admin attivato (solo sviluppo!)");
}
try {
console.log("🔍 [LocalAuth] Recupero lista utenti...");
const users = await storage.getAllUsers();
console.log(`✅ [LocalAuth] Trovati ${users.length} utenti`);
const admin = users.find((u: any) => u.email === DEFAULT_ADMIN_EMAIL);
if (admin) {
console.log(`✅ [LocalAuth] Admin trovato: ${admin.email}`);
req.login({ id: admin.id, email: admin.email }, (err) => {
if (err) {
console.error("❌ [LocalAuth] Errore req.login:", err);
return res.status(500).json({ error: "Errore auto-login", details: err.message });
}
console.log("✅ [LocalAuth] Login effettuato, redirect a /");
res.redirect("/");
});
} else {
console.error(`❌ [LocalAuth] Admin non trovato (cercato: ${DEFAULT_ADMIN_EMAIL})`);
res.status(404).json({ error: "Admin non trovato", users: users.map((u: any) => u.email) });
}
} catch (error: any) {
console.error("❌ [LocalAuth] Errore in auto-login-admin:", error);
res.status(500).json({
error: "Errore server",
message: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
}
});
// Route logout
app.get("/api/logout", (req, res) => {
@ -155,9 +164,9 @@ export async function setupLocalAuth(app: Express) {
});
console.log("✅ [LocalAuth] Sistema autenticazione locale attivato");
console.log(`📧 Admin email: ${DEFAULT_ADMIN_EMAIL}`);
console.log(`🔑 Admin password: ${DEFAULT_ADMIN_PASSWORD}`);
console.log(`🔗 Login page: /login`);
console.log(`📧 Email admin: ${DEFAULT_ADMIN_EMAIL}`);
console.log(`🔑 Password admin: ${DEFAULT_ADMIN_PASSWORD}`);
console.log(`🔗 Auto-login: GET /api/auto-login-admin`);
}
export const isAuthenticated = async (req: any, res: any, next: any) => {

View File

@ -64,54 +64,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/users", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can create users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
const { email, firstName, lastName, password, role } = req.body;
if (!email || !firstName || !lastName || !password) {
return res.status(400).json({ message: "Missing required fields" });
}
// Hash password
const bcrypt = await import("bcrypt");
const passwordHash = await bcrypt.hash(password, 10);
// Generate UUID
const crypto = await import("crypto");
const userId = crypto.randomUUID();
const newUser = await storage.upsertUser({
id: userId,
email,
firstName,
lastName,
profileImageUrl: null,
passwordHash,
});
// Set role if provided
if (role) {
await storage.updateUserRole(newUser.id, role);
}
res.json(newUser);
} catch (error: any) {
console.error("Error creating user:", error);
if (error.code === '23505') { // Unique violation
return res.status(409).json({ message: "Email già esistente" });
}
res.status(500).json({ message: "Failed to create user" });
}
});
app.patch("/api/users/:id", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
@ -143,78 +95,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.put("/api/users/:id", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can update users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
const { email, firstName, lastName, password, role } = req.body;
const existingUser = await storage.getUser(req.params.id);
if (!existingUser) {
return res.status(404).json({ message: "User not found" });
}
// Prepare update data
const updateData: any = {
id: req.params.id,
email: email || existingUser.email,
firstName: firstName || existingUser.firstName,
lastName: lastName || existingUser.lastName,
profileImageUrl: existingUser.profileImageUrl,
};
// Hash new password if provided
if (password) {
const bcrypt = await import("bcrypt");
updateData.passwordHash = await bcrypt.hash(password, 10);
}
const updated = await storage.upsertUser(updateData);
// Update role if provided and not changing own role
if (role && req.params.id !== currentUserId) {
await storage.updateUserRole(req.params.id, role);
}
res.json(updated);
} catch (error: any) {
console.error("Error updating user:", error);
if (error.code === '23505') {
return res.status(409).json({ message: "Email già esistente" });
}
res.status(500).json({ message: "Failed to update user" });
}
});
app.delete("/api/users/:id", isAuthenticated, async (req: any, res) => {
try {
const currentUserId = getUserId(req);
const currentUser = await storage.getUser(currentUserId);
// Only admins can delete users
if (currentUser?.role !== "admin") {
return res.status(403).json({ message: "Forbidden: Admin access required" });
}
// Prevent admins from deleting themselves
if (req.params.id === currentUserId) {
return res.status(403).json({ message: "Cannot delete your own account" });
}
await storage.deleteUser(req.params.id);
res.json({ success: true });
} catch (error) {
console.error("Error deleting user:", error);
res.status(500).json({ message: "Failed to delete user" });
}
});
// ============= GUARD ROUTES =============
app.get("/api/guards", isAuthenticated, async (req, res) => {
try {
@ -294,119 +174,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// ============= VEHICLE ROUTES =============
app.get("/api/vehicles", isAuthenticated, async (req, res) => {
try {
const vehicles = await storage.getAllVehicles();
res.json(vehicles);
} catch (error) {
console.error("Error fetching vehicles:", error);
res.status(500).json({ message: "Failed to fetch vehicles" });
}
});
app.post("/api/vehicles", isAuthenticated, async (req, res) => {
try {
const vehicle = await storage.createVehicle(req.body);
res.json(vehicle);
} catch (error: any) {
console.error("Error creating vehicle:", error);
if (error.code === '23505') {
return res.status(409).json({ message: "Targa già esistente" });
}
res.status(500).json({ message: "Failed to create vehicle" });
}
});
app.patch("/api/vehicles/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateVehicle(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Vehicle not found" });
}
res.json(updated);
} catch (error: any) {
console.error("Error updating vehicle:", error);
if (error.code === '23505') {
return res.status(409).json({ message: "Targa già esistente" });
}
res.status(500).json({ message: "Failed to update vehicle" });
}
});
app.delete("/api/vehicles/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteVehicle(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Vehicle not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting vehicle:", error);
res.status(500).json({ message: "Failed to delete vehicle" });
}
});
// ============= 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 =============
app.post("/api/certifications", isAuthenticated, async (req, res) => {
try {

View File

@ -3,7 +3,6 @@ import {
users,
guards,
certifications,
vehicles,
sites,
shifts,
shiftAssignments,
@ -15,15 +14,12 @@ import {
holidayAssignments,
absences,
absenceAffectedShifts,
contractParameters,
type User,
type UpsertUser,
type Guard,
type InsertGuard,
type Certification,
type InsertCertification,
type Vehicle,
type InsertVehicle,
type Site,
type InsertSite,
type Shift,
@ -46,8 +42,6 @@ import {
type InsertAbsence,
type AbsenceAffectedShift,
type InsertAbsenceAffectedShift,
type ContractParameters,
type InsertContractParameters,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc } from "drizzle-orm";
@ -129,11 +123,6 @@ export interface IStorage {
getAffectedShiftsByAbsence(absenceId: string): Promise<AbsenceAffectedShift[]>;
createAbsenceAffectedShift(affected: InsertAbsenceAffectedShift): Promise<AbsenceAffectedShift>;
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 {
@ -185,11 +174,6 @@ export class DatabaseStorage implements IStorage {
return updated;
}
async deleteUser(id: string): Promise<User | undefined> {
const [deleted] = await db.delete(users).where(eq(users.id, id)).returning();
return deleted;
}
// Guard operations
async getAllGuards(): Promise<Guard[]> {
return await db.select().from(guards);
@ -219,35 +203,6 @@ export class DatabaseStorage implements IStorage {
return deleted;
}
// Vehicle operations
async getAllVehicles(): Promise<Vehicle[]> {
return await db.select().from(vehicles).orderBy(desc(vehicles.createdAt));
}
async getVehicle(id: string): Promise<Vehicle | undefined> {
const [vehicle] = await db.select().from(vehicles).where(eq(vehicles.id, id));
return vehicle;
}
async createVehicle(vehicle: InsertVehicle): Promise<Vehicle> {
const [newVehicle] = await db.insert(vehicles).values(vehicle).returning();
return newVehicle;
}
async updateVehicle(id: string, vehicleData: Partial<InsertVehicle>): Promise<Vehicle | undefined> {
const [updated] = await db
.update(vehicles)
.set({ ...vehicleData, updatedAt: new Date() })
.where(eq(vehicles.id, id))
.returning();
return updated;
}
async deleteVehicle(id: string): Promise<Vehicle | undefined> {
const [deleted] = await db.delete(vehicles).where(eq(vehicles.id, id)).returning();
return deleted;
}
// Certification operations
async getCertificationsByGuard(guardId: string): Promise<Certification[]> {
return await db
@ -555,26 +510,6 @@ export class DatabaseStorage implements IStorage {
async deleteAbsenceAffectedShift(id: string): Promise<void> {
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();

View File

@ -71,20 +71,6 @@ export const sitePreferenceTypeEnum = pgEnum("site_preference_type", [
"blacklisted", // Non assegnare mai questo operatore a questo sito
]);
export const vehicleStatusEnum = pgEnum("vehicle_status", [
"available", // Disponibile
"in_use", // In uso
"maintenance", // In manutenzione
"out_of_service", // Fuori servizio
]);
export const vehicleTypeEnum = pgEnum("vehicle_type", [
"car", // Auto
"van", // Furgone
"motorcycle", // Moto
"suv", // SUV
]);
// ============= SESSION & AUTH TABLES (Replit Auth) =============
// Session storage table - mandatory for Replit Auth
@ -105,7 +91,6 @@ export const users = pgTable("users", {
firstName: varchar("first_name"),
lastName: varchar("last_name"),
profileImageUrl: varchar("profile_image_url"),
passwordHash: varchar("password_hash"), // For local auth - bcrypt hash
role: userRoleEnum("role").notNull().default("guard"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
@ -142,30 +127,6 @@ export const certifications = pgTable("certifications", {
createdAt: timestamp("created_at").defaultNow(),
});
// ============= VEHICLES (PARCO AUTOMEZZI) =============
export const vehicles = pgTable("vehicles", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
licensePlate: varchar("license_plate").notNull().unique(), // Targa
brand: varchar("brand").notNull(), // Marca (es: Fiat, Volkswagen)
model: varchar("model").notNull(), // Modello
vehicleType: vehicleTypeEnum("vehicle_type").notNull(),
year: integer("year"), // Anno immatricolazione
// Assegnazione
assignedGuardId: varchar("assigned_guard_id").references(() => guards.id, { onDelete: "set null" }),
// Stato e manutenzione
status: vehicleStatusEnum("status").notNull().default("available"),
lastMaintenanceDate: date("last_maintenance_date"),
nextMaintenanceDate: date("next_maintenance_date"),
mileage: integer("mileage"), // Chilometraggio
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// ============= SITES & CONTRACTS =============
export const sites = pgTable("sites", {
@ -292,14 +253,8 @@ export const contractParameters = pgTable("contract_parameters", {
// Riposi obbligatori
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),
// Pause obbligatorie
pauseMinutesIfOver6Hours: integer("pause_minutes_if_over_6_hours").notNull().default(10),
// Limiti notturni (22:00-06:00)
maxNightHoursPerWeek: integer("max_night_hours_per_week").default(48),
@ -425,14 +380,6 @@ export const guardsRelations = relations(guards, ({ one, many }) => ({
sitePreferences: many(sitePreferences),
trainingCourses: many(trainingCourses),
absences: many(absences),
assignedVehicles: many(vehicles),
}));
export const vehiclesRelations = relations(vehicles, ({ one }) => ({
assignedGuard: one(guards, {
fields: [vehicles.assignedGuardId],
references: [guards.id],
}),
}));
export const certificationsRelations = relations(certifications, ({ one }) => ({
@ -555,28 +502,9 @@ export const insertUserSchema = createInsertSchema(users).pick({
firstName: true,
lastName: true,
profileImageUrl: true,
passwordHash: true,
role: true,
});
// Form schema with plain password (will be hashed on backend)
export const insertUserFormSchema = z.object({
email: z.string().email("Email non valida"),
firstName: z.string().min(1, "Nome obbligatorio"),
lastName: z.string().min(1, "Cognome obbligatorio"),
password: z.string().min(6, "Password minimo 6 caratteri"),
role: z.enum(["admin", "coordinator", "guard", "client"]).default("guard"),
});
// Update user form schema (password optional)
export const updateUserFormSchema = z.object({
email: z.string().email("Email non valida").optional(),
firstName: z.string().min(1, "Nome obbligatorio").optional(),
lastName: z.string().min(1, "Cognome obbligatorio").optional(),
password: z.string().min(6, "Password minimo 6 caratteri").optional(),
role: z.enum(["admin", "coordinator", "guard", "client"]).optional(),
});
export const insertGuardSchema = createInsertSchema(guards).omit({
id: true,
createdAt: true,
@ -589,12 +517,6 @@ export const insertCertificationSchema = createInsertSchema(certifications).omit
status: true,
});
export const insertVehicleSchema = createInsertSchema(vehicles).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertSiteSchema = createInsertSchema(sites).omit({
id: true,
createdAt: true,
@ -682,9 +604,6 @@ export type Guard = typeof guards.$inferSelect;
export type InsertCertification = z.infer<typeof insertCertificationSchema>;
export type Certification = typeof certifications.$inferSelect;
export type InsertVehicle = z.infer<typeof insertVehicleSchema>;
export type Vehicle = typeof vehicles.$inferSelect;
export type InsertSite = z.infer<typeof insertSiteSchema>;
export type Site = typeof sites.$inferSelect;