VigilanzaTurni/client/src/pages/vehicles.tsx
marco370 0203c9694d Add vehicle management and improve user authentication and management
Introduce a new section for vehicle management, enhance user authentication with bcrypt, and implement CRUD operations for users.

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/GNrPM6a
2025-10-16 17:41:22 +00:00

804 lines
30 KiB
TypeScript

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>
);
}