inserita la creazione e la gestione degil utenti
This commit is contained in:
parent
dc7dbc4166
commit
05f1629aee
9 changed files with 797 additions and 221 deletions
|
|
@ -18,7 +18,7 @@ import MieiCorsi from "@/pages/MieiCorsi";
|
||||||
import MioCorsoDettaglio from "@/pages/MioCorsoDettaglio";
|
import MioCorsoDettaglio from "@/pages/MioCorsoDettaglio";
|
||||||
import Corsi from "@/pages/Corsi";
|
import Corsi from "@/pages/Corsi";
|
||||||
import Assegnazioni from "@/pages/Assegnazioni";
|
import Assegnazioni from "@/pages/Assegnazioni";
|
||||||
import Dipendenti from "@/pages/Dipendenti";
|
import Utenti from "@/pages/Utenti";
|
||||||
import Statistiche from "@/pages/Statistiche";
|
import Statistiche from "@/pages/Statistiche";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
@ -63,10 +63,10 @@ export default function App() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/dipendenti"
|
path="/utenti"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute roles={["Referente Academy"]}>
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
<Dipendenti />
|
<Utenti />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -53,44 +53,11 @@ export async function getCsrfToken(): Promise<string> {
|
||||||
return data.csrfToken;
|
return data.csrfToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(email: string, password: string): Promise<void> {
|
export async function login(email: string, password: string): Promise<Utente> {
|
||||||
const csrfToken = await getCsrfToken();
|
return request<Utente>("/api/utenti/login", {
|
||||||
const body = new URLSearchParams({
|
|
||||||
csrfToken,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
redirect: "false",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/api/auth/callback/credentials`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
body: JSON.stringify({ email, password }),
|
||||||
redirect: "manual",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
"X-Auth-Return-Redirect": "1",
|
|
||||||
},
|
|
||||||
body,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.type === "opaqueredirect" || response.status === 302) {
|
|
||||||
throw new Error("Login non riuscito: frontend non aggiornato o configurazione Auth errata");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json().catch(() => ({})) as { url?: string };
|
|
||||||
if (!response.ok || data.url?.includes("error=")) {
|
|
||||||
const errorType = data.url?.match(/error=([^&]+)/)?.[1];
|
|
||||||
if (errorType === "CredentialsSignin") {
|
|
||||||
throw new Error("Credenziali non valide");
|
|
||||||
}
|
|
||||||
if (errorType === "MissingCSRF") {
|
|
||||||
throw new Error("Sessione scaduta: ricarica la pagina e riprova");
|
|
||||||
}
|
|
||||||
if (errorType === "Configuration") {
|
|
||||||
throw new Error("Errore di configurazione del server (database o auth)");
|
|
||||||
}
|
|
||||||
throw new Error("Accesso non riuscito");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
|
|
@ -265,6 +232,37 @@ export async function getUtenti(filters?: { ruolo?: Ruolo }): Promise<Utente[]>
|
||||||
return request(`/api/utenti${buildQuery({ ruolo: filters?.ruolo })}`);
|
return request(`/api/utenti${buildQuery({ ruolo: filters?.ruolo })}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UtenteInput {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
ruolo: Ruolo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUtente(data: UtenteInput): Promise<Utente> {
|
||||||
|
return request("/api/utenti/register", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUtente(
|
||||||
|
id: number,
|
||||||
|
data: {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
email: string;
|
||||||
|
ruolo: Ruolo;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
): Promise<Utente> {
|
||||||
|
return request(`/api/utenti/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStatisticheAcademy(filters?: {
|
export async function getStatisticheAcademy(filters?: {
|
||||||
mese?: string;
|
mese?: string;
|
||||||
daMese?: string;
|
daMese?: string;
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ export const NAV_ITEMS: NavItem[] = [
|
||||||
roles: ["Referente Academy"],
|
roles: ["Referente Academy"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Dipendenti",
|
label: "Utenti",
|
||||||
to: "/dipendenti",
|
to: "/utenti",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
roles: ["Referente Academy"],
|
roles: ["Referente Academy"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -302,9 +302,9 @@ function ReferenteDashboard() {
|
||||||
to="/assegnazioni"
|
to="/assegnazioni"
|
||||||
/>
|
/>
|
||||||
<QuickLinkCard
|
<QuickLinkCard
|
||||||
title="Dipendenti"
|
title="Utenti"
|
||||||
description="Consulta l'elenco dei dipendenti."
|
description="Gestisci Dipendenti e Referenti Academy."
|
||||||
to="/dipendenti"
|
to="/utenti"
|
||||||
/>
|
/>
|
||||||
<QuickLinkCard
|
<QuickLinkCard
|
||||||
title="Statistiche"
|
title="Statistiche"
|
||||||
|
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
//==========================================
|
|
||||||
// File: Dipendenti.tsx
|
|
||||||
// Componente per la gestione dei dipendenti
|
|
||||||
// @author: "villari.andrea@libero.it"
|
|
||||||
// @version: "1.0.0 2026-07-14"
|
|
||||||
//==========================================
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { AlertCircle, ClipboardList, Search, Users } from "lucide-react";
|
|
||||||
import { getUtenti } from "@/api/client";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
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 { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import type { Utente } from "@/types";
|
|
||||||
|
|
||||||
function TableSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
|
||||||
<Skeleton key={index} className="h-12 w-full" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesSearch(utente: Utente, query: string): boolean {
|
|
||||||
const normalized = query.trim().toLowerCase();
|
|
||||||
if (!normalized) return true;
|
|
||||||
|
|
||||||
return [utente.nome, utente.cognome, utente.email]
|
|
||||||
.join(" ")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dipendenti() {
|
|
||||||
const [dipendenti, setDipendenti] = useState<Utente[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
getUtenti({ ruolo: "Dipendente" })
|
|
||||||
.then(setDipendenti)
|
|
||||||
.catch((err) =>
|
|
||||||
setError(err instanceof Error ? err.message : "Errore nel caricamento")
|
|
||||||
)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filtered = useMemo(
|
|
||||||
() => dipendenti.filter((d) => matchesSearch(d, search)),
|
|
||||||
[dipendenti, search]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Dipendenti</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Elenco dei dipendenti dell'organizzazione iscritti all'Academy.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle />
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-4">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<Search className="size-4" />
|
|
||||||
Ricerca
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Filtra per nome, cognome o email.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="max-w-sm space-y-1.5">
|
|
||||||
<Label htmlFor="ricerca-dipendenti">Cerca dipendente</Label>
|
|
||||||
<Input
|
|
||||||
id="ricerca-dipendenti"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
placeholder="Es. Maria, Bianchi, academy.it"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<Users className="size-4" />
|
|
||||||
{loading ? "Caricamento..." : `${filtered.length} dipendenti`}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loading ? (
|
|
||||||
<TableSkeleton />
|
|
||||||
) : filtered.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{search
|
|
||||||
? "Nessun dipendente corrisponde alla ricerca."
|
|
||||||
: "Nessun dipendente registrato."}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Cognome</TableHead>
|
|
||||||
<TableHead>Nome</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead className="text-right">Azioni</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filtered.map((dipendente) => (
|
|
||||||
<TableRow key={dipendente.id}>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{dipendente.cognome}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{dipendente.nome}</TableCell>
|
|
||||||
<TableCell>{dipendente.email}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
render={
|
|
||||||
<Link
|
|
||||||
to={`/assegnazioni?dipendenteId=${dipendente.id}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ClipboardList className="size-4" />
|
|
||||||
Assegnazioni
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { CircleAlert } from "lucide-react";
|
import { CircleAlert } from "lucide-react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ApiClientError } from "@/api/client";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,6 +20,13 @@ import {
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
|
import { validateLoginForm } from "@/validation/utente";
|
||||||
|
import type { FieldErrors } from "@/validation/primitives";
|
||||||
|
|
||||||
|
function FieldError({ message }: { message?: string }) {
|
||||||
|
if (!message) return null;
|
||||||
|
return <p className="text-xs text-destructive">{message}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
|
@ -26,16 +34,28 @@ export default function Login() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
const validation = validateLoginForm({ email, password });
|
||||||
|
if (validation.success === false) {
|
||||||
|
setFieldErrors(validation.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldErrors({});
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await login(email, password);
|
await login(validation.data.email, validation.data.password);
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof ApiClientError && err.fields) {
|
||||||
|
setFieldErrors(err.fields);
|
||||||
|
}
|
||||||
setError(err instanceof Error ? err.message : "Errore di accesso");
|
setError(err instanceof Error ? err.message : "Errore di accesso");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -68,10 +88,18 @@ export default function Login() {
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
placeholder="nome@academy.it"
|
placeholder="nome@academy.it"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => {
|
||||||
required
|
setEmail(e.target.value);
|
||||||
|
setFieldErrors((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next.email;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-invalid={Boolean(fieldErrors.email)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
|
<FieldError message={fieldErrors.email} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|
@ -82,10 +110,18 @@ export default function Login() {
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => {
|
||||||
required
|
setPassword(e.target.value);
|
||||||
|
setFieldErrors((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next.password;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-invalid={Boolean(fieldErrors.password)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
|
<FieldError message={fieldErrors.password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
|
|
|
||||||
485
src/pages/Utenti.tsx
Normal file
485
src/pages/Utenti.tsx
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
//==========================================
|
||||||
|
// File: Utenti.tsx
|
||||||
|
// Componente per la gestione degli utenti
|
||||||
|
// @author: "villari.andrea@libero.it"
|
||||||
|
// @version: "1.0.0 2026-07-14"
|
||||||
|
//==========================================
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
ClipboardList,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
ApiClientError,
|
||||||
|
createUtente,
|
||||||
|
getUtenti,
|
||||||
|
updateUtente,
|
||||||
|
} from "@/api/client";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import type { Ruolo, Utente } from "@/types";
|
||||||
|
import {
|
||||||
|
emptyUtenteForm,
|
||||||
|
utenteToForm,
|
||||||
|
validateUtenteCreate,
|
||||||
|
validateUtenteUpdate,
|
||||||
|
type UtenteFormInput,
|
||||||
|
} from "@/validation/utente";
|
||||||
|
import type { FieldErrors } from "@/validation/primitives";
|
||||||
|
|
||||||
|
const ALL_VALUE = "__all__";
|
||||||
|
const RUOLI: Ruolo[] = ["Dipendente", "Referente Academy"];
|
||||||
|
|
||||||
|
function TableSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<Skeleton key={index} className="h-12 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldError({ message }: { message?: string }) {
|
||||||
|
if (!message) return null;
|
||||||
|
return <p className="text-xs text-destructive">{message}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSearch(utente: Utente, query: string): boolean {
|
||||||
|
const normalized = query.trim().toLowerCase();
|
||||||
|
if (!normalized) return true;
|
||||||
|
|
||||||
|
return [utente.nome, utente.cognome, utente.email, utente.ruolo]
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UtenteFormDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
mode: "create" | "edit";
|
||||||
|
utente: Utente | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UtenteFormDialog({
|
||||||
|
open,
|
||||||
|
mode,
|
||||||
|
utente,
|
||||||
|
onOpenChange,
|
||||||
|
onSaved,
|
||||||
|
}: UtenteFormDialogProps) {
|
||||||
|
const [form, setForm] = useState<UtenteFormInput>(emptyUtenteForm());
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setFieldErrors({});
|
||||||
|
setForm(utente ? utenteToForm(utente) : emptyUtenteForm());
|
||||||
|
}, [open, utente]);
|
||||||
|
|
||||||
|
function updateField<K extends keyof UtenteFormInput>(
|
||||||
|
key: K,
|
||||||
|
value: UtenteFormInput[K]
|
||||||
|
) {
|
||||||
|
setForm((prev) => ({ ...prev, [key]: value }));
|
||||||
|
setFieldErrors((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: React.FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
setFieldErrors({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mode === "create") {
|
||||||
|
const result = validateUtenteCreate(form);
|
||||||
|
if (result.success === false) {
|
||||||
|
setFieldErrors(result.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await createUtente(result.data);
|
||||||
|
toast.success("Utente creato con successo");
|
||||||
|
} else if (utente) {
|
||||||
|
const result = validateUtenteUpdate(form);
|
||||||
|
if (result.success === false) {
|
||||||
|
setFieldErrors(result.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateUtente(utente.id, result.data);
|
||||||
|
toast.success("Utente aggiornato con successo");
|
||||||
|
}
|
||||||
|
onOpenChange(false);
|
||||||
|
onSaved();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiClientError && err.fields) {
|
||||||
|
setFieldErrors(err.fields);
|
||||||
|
}
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : "Operazione non riuscita"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{mode === "create" ? "Nuovo utente" : "Modifica utente"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{mode === "create"
|
||||||
|
? "Registra un nuovo Dipendente o Referente Academy."
|
||||||
|
: "Aggiorna i dati utente. Lascia la password vuota per non modificarla."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="utente-nome">Nome</Label>
|
||||||
|
<Input
|
||||||
|
id="utente-nome"
|
||||||
|
value={form.nome}
|
||||||
|
onChange={(e) => updateField("nome", e.target.value)}
|
||||||
|
aria-invalid={Boolean(fieldErrors.nome)}
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.nome} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="utente-cognome">Cognome</Label>
|
||||||
|
<Input
|
||||||
|
id="utente-cognome"
|
||||||
|
value={form.cognome}
|
||||||
|
onChange={(e) => updateField("cognome", e.target.value)}
|
||||||
|
aria-invalid={Boolean(fieldErrors.cognome)}
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.cognome} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="utente-email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="utente-email"
|
||||||
|
type="email"
|
||||||
|
value={form.email}
|
||||||
|
onChange={(e) => updateField("email", e.target.value)}
|
||||||
|
aria-invalid={Boolean(fieldErrors.email)}
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.email} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="utente-ruolo">Ruolo</Label>
|
||||||
|
<Select
|
||||||
|
value={form.ruolo || null}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateField("ruolo", (value ?? "") as Ruolo)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="utente-ruolo"
|
||||||
|
className="w-full"
|
||||||
|
aria-invalid={Boolean(fieldErrors.ruolo)}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Seleziona ruolo" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{RUOLI.map((ruolo) => (
|
||||||
|
<SelectItem key={ruolo} value={ruolo}>
|
||||||
|
{ruolo}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FieldError message={fieldErrors.ruolo} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="utente-password">
|
||||||
|
Password{mode === "edit" ? " (opzionale)" : ""}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="utente-password"
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(e) => updateField("password", e.target.value)}
|
||||||
|
autoComplete={mode === "create" ? "new-password" : "off"}
|
||||||
|
aria-invalid={Boolean(fieldErrors.password)}
|
||||||
|
/>
|
||||||
|
<FieldError message={fieldErrors.password} />
|
||||||
|
{mode === "create" && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Minimo 8 caratteri.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
Annulla
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={submitting}>
|
||||||
|
{submitting
|
||||||
|
? "Salvataggio..."
|
||||||
|
: mode === "create"
|
||||||
|
? "Registra utente"
|
||||||
|
: "Salva modifiche"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Utenti() {
|
||||||
|
const [utenti, setUtenti] = useState<Utente[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [ruoloFilter, setRuoloFilter] = useState<string>(ALL_VALUE);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [dialogMode, setDialogMode] = useState<"create" | "edit">("create");
|
||||||
|
const [selectedUtente, setSelectedUtente] = useState<Utente | null>(null);
|
||||||
|
|
||||||
|
const loadUtenti = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await getUtenti(
|
||||||
|
ruoloFilter === ALL_VALUE
|
||||||
|
? undefined
|
||||||
|
: { ruolo: ruoloFilter as Ruolo }
|
||||||
|
);
|
||||||
|
setUtenti(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Errore nel caricamento");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [ruoloFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUtenti();
|
||||||
|
}, [loadUtenti]);
|
||||||
|
|
||||||
|
const filtered = useMemo(
|
||||||
|
() => utenti.filter((u) => matchesSearch(u, search)),
|
||||||
|
[utenti, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
setDialogMode("create");
|
||||||
|
setSelectedUtente(null);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(utente: Utente) {
|
||||||
|
setDialogMode("edit");
|
||||||
|
setSelectedUtente(utente);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Utenti</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Gestisci i Dipendenti e i Referenti Academy dell'organizzazione.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={openCreateDialog}>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
Nuovo utente
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Search className="size-4" />
|
||||||
|
Filtri
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Filtra per ruolo o cerca per nome, cognome ed email.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4 sm:flex-row">
|
||||||
|
<div className="max-w-sm flex-1 space-y-1.5">
|
||||||
|
<Label htmlFor="ricerca-utenti">Cerca utente</Label>
|
||||||
|
<Input
|
||||||
|
id="ricerca-utenti"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Es. Maria, Bianchi, academy.it"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-xs space-y-1.5">
|
||||||
|
<Label htmlFor="filtro-ruolo">Ruolo</Label>
|
||||||
|
<Select
|
||||||
|
value={ruoloFilter}
|
||||||
|
onValueChange={(value) => setRuoloFilter(value ?? ALL_VALUE)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="filtro-ruolo">
|
||||||
|
<SelectValue placeholder="Tutti i ruoli" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={ALL_VALUE}>Tutti i ruoli</SelectItem>
|
||||||
|
{RUOLI.map((ruolo) => (
|
||||||
|
<SelectItem key={ruolo} value={ruolo}>
|
||||||
|
{ruolo}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Users className="size-4" />
|
||||||
|
{loading ? "Caricamento..." : `${filtered.length} utenti`}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<TableSkeleton />
|
||||||
|
) : filtered.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{search
|
||||||
|
? "Nessun utente corrisponde ai filtri selezionati."
|
||||||
|
: "Nessun utente registrato."}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Cognome</TableHead>
|
||||||
|
<TableHead>Nome</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Ruolo</TableHead>
|
||||||
|
<TableHead className="text-right">Azioni</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filtered.map((utente) => (
|
||||||
|
<TableRow key={utente.id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{utente.cognome}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{utente.nome}</TableCell>
|
||||||
|
<TableCell>{utente.email}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="secondary">{utente.ruolo}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openEditDialog(utente)}
|
||||||
|
>
|
||||||
|
<Pencil className="size-4" />
|
||||||
|
Modifica
|
||||||
|
</Button>
|
||||||
|
{utente.ruolo === "Dipendente" && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
render={
|
||||||
|
<Link
|
||||||
|
to={`/assegnazioni?dipendenteId=${utente.id}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClipboardList className="size-4" />
|
||||||
|
Assegnazioni
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<UtenteFormDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
mode={dialogMode}
|
||||||
|
utente={selectedUtente}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
onSaved={loadUtenti}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,17 @@
|
||||||
// @version: "1.0.0 2026-07-14"
|
// @version: "1.0.0 2026-07-14"
|
||||||
//===================================================
|
//===================================================
|
||||||
|
|
||||||
|
export const UTENTE_CONSTRAINTS = {
|
||||||
|
nome: { maxLength: 100, required: true },
|
||||||
|
cognome: { maxLength: 100, required: true },
|
||||||
|
email: { maxLength: 255, required: true },
|
||||||
|
password: { minLength: 8, maxLength: 72, required: true },
|
||||||
|
ruolo: {
|
||||||
|
allowedValues: ["Dipendente", "Referente Academy"] as const,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const CORSO_CONSTRAINTS = {
|
export const CORSO_CONSTRAINTS = {
|
||||||
titolo: { maxLength: 200, required: true },
|
titolo: { maxLength: 200, required: true },
|
||||||
descrizione: { maxLength: 65535, required: false },
|
descrizione: { maxLength: 65535, required: false },
|
||||||
|
|
|
||||||
218
src/validation/utente.ts
Normal file
218
src/validation/utente.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
//=======================================
|
||||||
|
// File: utente.ts
|
||||||
|
// Validazione lato frontend dei dati utente.
|
||||||
|
// @author: "villari.andrea@libero.it"
|
||||||
|
// @version: "1.0.0 2026-07-14"
|
||||||
|
//=======================================
|
||||||
|
|
||||||
|
import { UTENTE_CONSTRAINTS } from "./constraints";
|
||||||
|
import {
|
||||||
|
buildResult,
|
||||||
|
normalizeEmail,
|
||||||
|
normalizeString,
|
||||||
|
validateEmailFormat,
|
||||||
|
validateEnum,
|
||||||
|
validateMaxLength,
|
||||||
|
validateMinLength,
|
||||||
|
validateRequired,
|
||||||
|
type FieldErrors,
|
||||||
|
type ValidationResult,
|
||||||
|
} from "./primitives";
|
||||||
|
import type { Ruolo, Utente } from "@/types";
|
||||||
|
|
||||||
|
export interface UtenteFormInput {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
ruolo: Ruolo | "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UtenteCreatePayload {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
ruolo: Ruolo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UtenteUpdatePayload {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
email: string;
|
||||||
|
ruolo: Ruolo;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginFormInput {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateNomeCognomeEmail(
|
||||||
|
data: Pick<UtenteFormInput, "nome" | "cognome" | "email">,
|
||||||
|
errors: FieldErrors
|
||||||
|
): void {
|
||||||
|
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"nome",
|
||||||
|
data.nome,
|
||||||
|
UTENTE_CONSTRAINTS.nome.maxLength,
|
||||||
|
"Nome"
|
||||||
|
);
|
||||||
|
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"cognome",
|
||||||
|
data.cognome,
|
||||||
|
UTENTE_CONSTRAINTS.cognome.maxLength,
|
||||||
|
"Cognome"
|
||||||
|
);
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
UTENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePasswordField(
|
||||||
|
errors: FieldErrors,
|
||||||
|
password: string,
|
||||||
|
options: { required: boolean }
|
||||||
|
): void {
|
||||||
|
if (options.required) {
|
||||||
|
validateRequired(errors, "password", password, "Password obbligatoria");
|
||||||
|
}
|
||||||
|
if (!password) return;
|
||||||
|
validateMinLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
password,
|
||||||
|
UTENTE_CONSTRAINTS.password.minLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
password,
|
||||||
|
UTENTE_CONSTRAINTS.password.maxLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emptyUtenteForm(): UtenteFormInput {
|
||||||
|
return {
|
||||||
|
nome: "",
|
||||||
|
cognome: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
ruolo: "Dipendente",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function utenteToForm(utente: Utente): UtenteFormInput {
|
||||||
|
return {
|
||||||
|
nome: utente.nome,
|
||||||
|
cognome: utente.cognome,
|
||||||
|
email: utente.email,
|
||||||
|
password: "",
|
||||||
|
ruolo: utente.ruolo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLoginForm(
|
||||||
|
raw: LoginFormInput
|
||||||
|
): ValidationResult<LoginFormInput> {
|
||||||
|
const data: LoginFormInput = {
|
||||||
|
email: normalizeEmail(raw.email),
|
||||||
|
password: raw.password?.toString() ?? "",
|
||||||
|
};
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
UTENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
validateRequired(errors, "password", data.password, "Password obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
data.password,
|
||||||
|
UTENTE_CONSTRAINTS.password.maxLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUtenteCreate(
|
||||||
|
raw: UtenteFormInput
|
||||||
|
): ValidationResult<UtenteCreatePayload> {
|
||||||
|
const data: UtenteCreatePayload = {
|
||||||
|
nome: normalizeString(raw.nome),
|
||||||
|
cognome: normalizeString(raw.cognome),
|
||||||
|
email: normalizeEmail(raw.email),
|
||||||
|
password: raw.password?.toString() ?? "",
|
||||||
|
ruolo: normalizeString(raw.ruolo) as Ruolo,
|
||||||
|
};
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
|
||||||
|
validateNomeCognomeEmail(data, errors);
|
||||||
|
validatePasswordField(errors, data.password, { required: true });
|
||||||
|
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
|
||||||
|
validateEnum(
|
||||||
|
errors,
|
||||||
|
"ruolo",
|
||||||
|
data.ruolo,
|
||||||
|
UTENTE_CONSTRAINTS.ruolo.allowedValues,
|
||||||
|
"Ruolo"
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUtenteUpdate(
|
||||||
|
raw: UtenteFormInput
|
||||||
|
): ValidationResult<UtenteUpdatePayload> {
|
||||||
|
const data: UtenteUpdatePayload = {
|
||||||
|
nome: normalizeString(raw.nome),
|
||||||
|
cognome: normalizeString(raw.cognome),
|
||||||
|
email: normalizeEmail(raw.email),
|
||||||
|
ruolo: normalizeString(raw.ruolo) as Ruolo,
|
||||||
|
};
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
|
||||||
|
validateNomeCognomeEmail(
|
||||||
|
{
|
||||||
|
nome: data.nome ?? "",
|
||||||
|
cognome: data.cognome ?? "",
|
||||||
|
email: data.email ?? "",
|
||||||
|
},
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
validateEnum(
|
||||||
|
errors,
|
||||||
|
"ruolo",
|
||||||
|
data.ruolo,
|
||||||
|
UTENTE_CONSTRAINTS.ruolo.allowedValues,
|
||||||
|
"Ruolo"
|
||||||
|
);
|
||||||
|
|
||||||
|
const password = raw.password?.toString() ?? "";
|
||||||
|
if (password) {
|
||||||
|
data.password = password;
|
||||||
|
validatePasswordField(errors, password, { required: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue