diff --git a/client/src/App.tsx b/client/src/App.tsx index a9b9d26..36dad9f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,6 +9,7 @@ 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"; @@ -17,12 +18,14 @@ 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"; function Router() { const { isAuthenticated, isLoading } = useAuth(); return ( + {isLoading || !isAuthenticated ? ( ) : ( @@ -30,6 +33,7 @@ function Router() { + diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index 5fb3e2d..c68ae43 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -9,6 +9,7 @@ import { LogOut, UserCog, ClipboardList, + Car, } from "lucide-react"; import { Link, useLocation } from "wouter"; import { @@ -59,6 +60,12 @@ const menuItems = [ icon: MapPin, roles: ["admin", "coordinator", "client"], }, + { + title: "Parco Automezzi", + url: "/vehicles", + icon: Car, + roles: ["admin", "coordinator"], + }, { title: "Report", url: "/reports", diff --git a/client/src/pages/login.tsx b/client/src/pages/login.tsx new file mode 100644 index 0000000..4caa2b9 --- /dev/null +++ b/client/src/pages/login.tsx @@ -0,0 +1,158 @@ +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; + +export default function Login() { + const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + 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 ( +
+ + +
+ +
+ VigilanzaTurni + + Accedi al sistema di gestione turni + +
+ +
+ + ( + + Email + + + + + + )} + /> + + ( + + Password + + + + + + )} + /> + + + + + +
+

Credenziali di default:

+

Email: admin@vt.alfacom.it

+

Password: admin123

+
+
+
+
+ ); +} diff --git a/client/src/pages/users.tsx b/client/src/pages/users.tsx index 95b965f..f53c1a9 100644 --- a/client/src/pages/users.tsx +++ b/client/src/pages/users.tsx @@ -1,7 +1,11 @@ +import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { queryClient, apiRequest } from "@/lib/queryClient"; import { useAuth } from "@/hooks/useAuth"; -import { type User } from "@shared/schema"; +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 { Card, CardContent, @@ -17,6 +21,24 @@ 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, @@ -24,11 +46,21 @@ 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 } from "lucide-react"; +import { Shield, UserCog, Users as UsersIcon, UserCheck, Plus, Pencil, Trash2, Loader2 } from "lucide-react"; const roleIcons = { admin: Shield, @@ -51,35 +83,128 @@ const roleColors = { client: "bg-orange-500/10 text-orange-500 border-orange-500/20", }; +type CreateUserForm = z.infer; +type UpdateUserForm = z.infer; + 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(null); const { data: users, isLoading } = useQuery({ queryKey: ["/api/users"], }); - const updateRoleMutation = useMutation({ - mutationFn: async ({ userId, role }: { userId: string; role: string }) => { - return apiRequest("PATCH", `/api/users/${userId}`, { role }); + const createForm = useForm({ + resolver: zodResolver(insertUserFormSchema), + defaultValues: { + email: "", + firstName: "", + lastName: "", + password: "", + role: "guard", + }, + }); + + const editForm = useForm({ + 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); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/users"] }); queryClient.invalidateQueries({ queryKey: ["/api/auth/user"] }); toast({ - title: "Ruolo aggiornato", - description: "Il ruolo dell'utente è stato modificato con successo.", + title: "Utente aggiornato", + description: "L'utente è stato modificato con successo.", }); + setEditDialogOpen(false); + setSelectedUser(null); + editForm.reset(); }, - onError: () => { + onError: (error: any) => { toast({ title: "Errore", - description: "Impossibile aggiornare il ruolo dell'utente.", + 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.", + 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 (
@@ -111,13 +236,19 @@ export default function Users() { return (
-
-

- Gestione Utenti -

-

- Gestisci utenti e permessi del sistema VigilanzaTurni -

+
+
+

+ Gestione Utenti +

+

+ Gestisci utenti e permessi del sistema VigilanzaTurni +

+
+
@@ -133,8 +264,8 @@ export default function Users() { Utente Email - Ruolo Attuale - Modifica Ruolo + Ruolo + Azioni @@ -181,45 +312,26 @@ export default function Users() { {roleLabels[user.role]} - - + +
+ + +
); @@ -234,6 +346,292 @@ export default function Users() { )}
+ + {/* Create User Dialog */} + + + + Crea Nuovo Utente + + Inserisci i dati del nuovo utente. La password deve essere di almeno 6 caratteri. + + +
+ createUserMutation.mutate(data))} className="space-y-4"> + ( + + Email + + + + + + )} + /> +
+ ( + + Nome + + + + + + )} + /> + ( + + Cognome + + + + + + )} + /> +
+ ( + + Password + + + + + + )} + /> + ( + + Ruolo + + + + )} + /> + + + + + + +
+
+ + {/* Edit User Dialog */} + + + + Modifica Utente + + Modifica i dati dell'utente. Lascia la password vuota per mantenerla invariata. + + +
+ selectedUser && updateUserMutation.mutate({ id: selectedUser.id, data }))} className="space-y-4"> + ( + + Email + + + + + + )} + /> +
+ ( + + Nome + + + + + + )} + /> + ( + + Cognome + + + + + + )} + /> +
+ ( + + Password (opzionale) + + + + + + )} + /> + ( + + Ruolo + + + + )} + /> + + + + + + +
+
+ + {/* Delete Confirmation Dialog */} + + + + Conferma Eliminazione + + Sei sicuro di voler eliminare l'utente {selectedUser?.firstName} {selectedUser?.lastName}? + Questa azione non può essere annullata. + + + + Annulla + selectedUser && deleteUserMutation.mutate(selectedUser.id)} + className="bg-destructive hover:bg-destructive/90" + data-testid="button-delete-confirm" + > + {deleteUserMutation.isPending ? ( + <> + + Eliminazione... + + ) : ( + "Elimina" + )} + + + +
); } diff --git a/client/src/pages/vehicles.tsx b/client/src/pages/vehicles.tsx new file mode 100644 index 0000000..baa6071 --- /dev/null +++ b/client/src/pages/vehicles.tsx @@ -0,0 +1,803 @@ +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; + +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(null); + + const { data: vehicles, isLoading: isLoadingVehicles } = useQuery({ + queryKey: ["/api/vehicles"], + }); + + const { data: guards } = useQuery({ + queryKey: ["/api/guards"], + }); + + const createForm = useForm({ + 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({ + 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 ( +
+
+

Parco Automezzi

+

Gestione veicoli aziendali

+
+ + +
+ {[1, 2, 3].map((i) => ( +
+ +
+ + +
+ +
+ ))} +
+
+
+
+ ); + } + + return ( +
+
+
+

+ Parco Automezzi +

+

+ Gestisci i veicoli aziendali e le assegnazioni +

+
+ +
+ + + + Veicoli Registrati + + {vehicles?.length || 0} veicoli nel parco aziendale + + + + + + + Targa + Veicolo + Tipo + Stato + Assegnato a + Azioni + + + + {vehicles?.map((vehicle) => { + const assignedGuard = guards?.find(g => g.id === vehicle.assignedGuardId); + + return ( + + + {vehicle.licensePlate} + + +
+

{vehicle.brand} {vehicle.model}

+ {vehicle.year &&

Anno {vehicle.year}

} +
+
+ {vehicleTypeLabels[vehicle.vehicleType]} + + + {vehicleStatusLabels[vehicle.status]} + + + + {assignedGuard ? ( + {assignedGuard.badgeNumber} + ) : ( + Non assegnato + )} + + +
+ + +
+
+
+ ); + })} +
+
+ + {vehicles?.length === 0 && ( +
+ +

Nessun veicolo registrato

+
+ )} +
+
+ + {/* Create Vehicle Dialog */} + + + + Aggiungi Nuovo Veicolo + + Inserisci i dati del veicolo da aggiungere al parco aziendale. + + +
+ createVehicleMutation.mutate(data))} className="space-y-4"> +
+ ( + + Targa * + + + + + + )} + /> + ( + + Tipo * + + + + )} + /> +
+
+ ( + + Marca * + + + + + + )} + /> + ( + + Modello * + + + + + + )} + /> + ( + + Anno + + field.onChange(e.target.value ? parseInt(e.target.value) : null)} + /> + + + + )} + /> +
+
+ ( + + Stato * + + + + )} + /> + ( + + Assegnato a + + + + )} + /> +
+ ( + + Chilometraggio + + field.onChange(e.target.value ? parseInt(e.target.value) : null)} + /> + + + + )} + /> + ( + + Note + +