diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3c0cc99 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# URL del backend API (senza slash finale) +VITE_API_URL=http://localhost:3011 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 003910e..d89c3e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,10 +30,10 @@ jobs: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} - commnand_timeout: 30m + command_timeout: 30m script: | echo "=== START $(date -Iseconds) ===" - cd /root/esameits24-26frontend # o backend + cd /root/esameits24-26frontend git pull --ff-only echo "=== DOCKER BUILD $(date -Iseconds) ===" docker compose --progress plain up -d --build diff --git a/@/components/ui/button.tsx b/@/components/ui/button.tsx deleted file mode 100644 index b033601..0000000 --- a/@/components/ui/button.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps) { - return ( - - ) -} - -export { Button, buttonVariants } diff --git a/@/lib/utils.ts b/@/lib/utils.ts deleted file mode 100644 index bd0c391..0000000 --- a/@/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/README.md b/README.md index c300135..d21a2c4 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,148 @@ -# React + TypeScript + Vite +# Academy Aziendale — Frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Interfaccia web CSR (Client Side Rendering) per la piattaforma formativa interna (esame ITS Web Developer Full Stack 24-26, Prova C). -Currently, two official plugins are available: +React + TypeScript, comunicazione con il backend tramite API REST e autenticazione cookie (Auth.js). -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) +## URL produzione -## React Compiler +| Risorsa | URL | +|---------|-----| +| Applicazione | https://esameits24-26frontend.andreavillari.it | +| Backend API | https://esameits24-26backend.andreavillari.it | +| Swagger | https://esameits24-26backend.andreavillari.it/api/docs | -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +## Stack tecnologico -## Expanding the ESLint configuration +- React 19 + TypeScript +- Vite 8 +- React Router 7 +- Tailwind CSS 4 + shadcn/ui (Base UI) +- Auth cookie-based con CSRF +- Tema chiaro/scuro (WCAG AA) -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +## Credenziali di test -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +Password per tutti gli utenti: `Password123!` - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +| Email | Ruolo | Cosa testare | +|-------|-------|--------------| +| `admin@academy.it` | Referente Academy | Catalogo corsi, assegnazioni, dipendenti, statistiche | +| `maria.bianchi@academy.it` | Dipendente | I miei corsi, completamento, dashboard | +| `luca.conti@academy.it` | Dipendente | — | +| `elena.rossi@academy.it` | Dipendente | — | - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +## Funzionalità per ruolo +### Dipendente + +- Dashboard con riepilogo assegnazioni e scadenze +- **I miei corsi** — elenco, filtri, dettaglio, segna come completato + +### Referente Academy + +- Dashboard con indicatori e link rapidi +- **Catalogo corsi** — crea, modifica, disattiva, elimina +- **Assegnazioni** — assegna corsi, modifica date, annulla +- **Dipendenti** — elenco e ricerca +- **Statistiche** — aggregati per mese, categoria e dipendente + +## Avvio in sviluppo + +### Prerequisiti + +- Node.js 20+ +- Backend in esecuzione (locale o produzione) + +### Configurazione + +Crea un file `.env` nella root del frontend (vedi `.env.example`): + +```env +VITE_API_URL=http://localhost:3011 ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +Per puntare al backend di produzione: -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +```env +VITE_API_URL=https://esameits24-26backend.andreavillari.it +``` -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +### Comandi + +```bash +npm ci +npm run dev +``` + +L'app è disponibile su http://localhost:5173 + +```bash +npm run build # build produzione in dist/ +npm run preview # anteprima build locale +npm run lint # ESLint +``` + +## Avvio con Docker + +```bash +docker compose up -d --build +``` + +Il container espone l'app su http://127.0.0.1:3010 (nginx). + +La variabile `VITE_API_URL` viene passata in fase di build (vedi `docker-compose.yml` e `dockerfile/Dockerfile`). + +## Tema chiaro / scuro + +L'applicazione supporta tre modalità (menu in sidebar e pagina login): + +- **Chiaro** +- **Scuro** — palette ottimizzata per ridurre affaticamento visivo (sfondo ~#121212, testo ~#E0E0E0, colori desaturati) +- **Sistema** — segue le preferenze del sistema operativo + +La preferenza è salvata in `localStorage` (`academy-theme`). + +## Struttura progetto ``` +src/ + api/client.ts Client HTTP verso il backend + components/ Layout, sidebar, route guard, UI shadcn + config/navigation.ts Voci menu per ruolo + context/AuthContext.tsx Stato autenticazione + lib/ Utility (date, formattazione) + pages/ Pagine dell'applicazione + types/ Tipi TypeScript condivisi + validation/ Validazione client-side (corsi, assegnazioni) +``` + +## Pagine e route + +| Route | Ruolo | Descrizione | +|-------|-------|-------------| +| `/login` | Guest | Accesso | +| `/dashboard` | Tutti | Home personalizzata per ruolo | +| `/i-miei-corsi` | Dipendente | Elenco corsi assegnati | +| `/i-miei-corsi/:id` | Dipendente | Dettaglio e completamento | +| `/corsi` | Referente | Catalogo CRUD | +| `/assegnazioni` | Referente | Gestione assegnazioni | +| `/dipendenti` | Referente | Elenco dipendenti | +| `/statistiche` | Referente | Riepiloghi formativi | + +## Validazione lato client + +I form di corsi e assegnazioni validano i dati prima dell'invio. Gli errori del server (status 400) vengono mostrati per campo tramite `ApiClientError` e toast Sonner. + +## Deploy + +Il deploy su VPS è automatizzato tramite Forgejo Actions (`.github/workflows/deploy.yml`): + +1. Build Vite su runner CI con `VITE_API_URL` di produzione +2. SSH sul VPS → `git pull` → `docker compose up -d --build` + +Il job deploy ha `command_timeout: 30m` per i build Docker sul VPS. + +## Autore + +Andrea Villari — esame ITS 24-26 diff --git a/src/App.css b/src/App.css deleted file mode 100644 index ebf97d1..0000000 --- a/src/App.css +++ /dev/null @@ -1,161 +0,0 @@ -.layout { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.header { - background: #1e3a5f; - color: #fff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -.header-inner { - max-width: 1100px; - margin: 0 auto; - padding: 0.75rem 1.5rem; - display: flex; - align-items: center; - gap: 1.5rem; - flex-wrap: wrap; -} - -.logo { - color: #fff; - font-weight: 700; - font-size: 1.1rem; - text-decoration: none; -} - -.header nav { - display: flex; - gap: 1rem; - flex: 1; -} - -.header nav a { - color: #cbd5e1; - text-decoration: none; - font-size: 0.95rem; -} - -.header nav a:hover { - color: #fff; -} - -.user-bar { - display: flex; - align-items: center; - gap: 1rem; - font-size: 0.9rem; -} - -.user-bar button { - background: transparent; - border: 1px solid #94a3b8; - color: #fff; - padding: 0.35rem 0.75rem; - border-radius: 6px; - cursor: pointer; -} - -.user-bar button:hover { - background: rgba(255, 255, 255, 0.1); -} - -.main { - flex: 1; - max-width: 1100px; - width: 100%; - margin: 0 auto; - padding: 2rem 1.5rem; -} - -.page h1 { - margin: 0 0 0.5rem; - color: #1e293b; -} - -.auth-page { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 2rem; - background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%); -} - -.auth-card { - background: #fff; - padding: 2rem; - border-radius: 12px; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); - width: 100%; - max-width: 420px; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.auth-card h1 { - margin: 0; - color: #1e3a5f; -} - -.auth-link { - text-align: center; - font-size: 0.9rem; - color: #64748b; -} - -label { - display: flex; - flex-direction: column; - gap: 0.35rem; - font-size: 0.9rem; - font-weight: 500; - color: #334155; -} - -input { - padding: 0.6rem 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 6px; - font-size: 1rem; - font-family: inherit; -} - -input:focus { - outline: 2px solid #3b82f6; - outline-offset: 1px; -} - -.btn { - display: inline-block; - padding: 0.6rem 1.2rem; - border-radius: 6px; - font-size: 0.95rem; - font-weight: 500; - text-decoration: none; - border: none; - cursor: pointer; - text-align: center; -} - -.btn-primary { - background: #2563eb; - color: #fff; -} - -.btn-primary:hover:not(:disabled) { - background: #1d4ed8; -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.error { color: #dc2626; margin: 0; } -.muted { color: #64748b; } -.loading { color: #64748b; font-style: italic; } diff --git a/src/App.tsx b/src/App.tsx index 01bc345..43852b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,8 @@ //======================================= // File: App.tsx -// Componete pagina principale dell'applicazione -// author: "villari.andrea@libero.it" -// version: "1.0.0 2026-07.14" +// Componente pagina principale dell'applicazione +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" //======================================= import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; diff --git a/src/api/client.ts b/src/api/client.ts index 199f118..cd5c1c2 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -48,10 +48,6 @@ async function request(path: string, options: RequestInit = {}): Promise { return data as T; } -export async function fetchHealth(): Promise<{ status: string }> { - return request("/api/health"); -} - export async function getCsrfToken(): Promise { const data = await request<{ csrfToken: string }>("/api/auth/csrf"); return data.csrfToken; @@ -284,17 +280,3 @@ export async function getStatisticheAcademy(filters?: { dipendenteId: filters?.dipendenteId, })}`); } - -export async function register(data: { - nome: string; - cognome: string; - email: string; - password: string; - confermaPassword: string; - ruolo: Ruolo; -}): Promise { - await request("/api/utenti/register", { - method: "POST", - body: JSON.stringify(data), - }); -} diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 2265691..b3e06ad 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -1,3 +1,10 @@ +//========================================= +// File: AppSidebar.tsx +// Componente per la gestione della sidebar +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//========================================= + import { NavLink, useNavigate } from "react-router-dom"; import { LogOut } from "lucide-react"; import ThemeToggle from "@/components/ThemeToggle"; diff --git a/src/components/GuestRoute.tsx b/src/components/GuestRoute.tsx index 72396a1..227c8c5 100644 --- a/src/components/GuestRoute.tsx +++ b/src/components/GuestRoute.tsx @@ -1,3 +1,10 @@ +//================================================= +// File: GuestRoute.tsx +// Componente per la gestione delle rotte pubbliche +// non protette. +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//================================================= import { Navigate } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 036586e..85bd160 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,3 +1,11 @@ +//========================================= +// File: Layout.tsx +// Componente per la gestione del layout di +// base di ogni pagina. +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//========================================= + import { Outlet } from "react-router-dom"; import AppSidebar from "@/components/AppSidebar"; diff --git a/src/components/PagePlaceholder.tsx b/src/components/PagePlaceholder.tsx deleted file mode 100644 index 8dd2ffa..0000000 --- a/src/components/PagePlaceholder.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; - -interface PagePlaceholderProps { - title: string; - description: string; - backTo?: string; -} - -export default function PagePlaceholder({ - title, - description, - backTo = "/dashboard", -}: PagePlaceholderProps) { - const navigate = useNavigate(); - - return ( - - - {title} - {description} - - -

- Questa sezione verrà implementata nel prossimo passo. -

- -
-
- ); -} diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 269349f..f418afe 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,3 +1,12 @@ +//================================================== +// File: ProtectedRoute.tsx +// Componente per la gestione delle rotte protette da +// controlli sull'autorizzazione in base al ruolo +// dell'utente. +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//================================================== + import { Navigate } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import type { Ruolo } from "../types"; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index e6cf6fc..648f2da 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,3 +1,10 @@ +//====================================== +// File: AuthContext.tsx +// Componente per la gestione useContext +// del Login, del Logout e del refresh +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//====================================== import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react"; import { getMe, login as apiLogin, logout as apiLogout } from "../api/client"; import type { Utente } from "../types"; diff --git a/src/lib/assegnazione.ts b/src/lib/assegnazione.ts index 76eda7f..f1952f2 100644 --- a/src/lib/assegnazione.ts +++ b/src/lib/assegnazione.ts @@ -1,7 +1,7 @@ //============================================== // File: assegnazione.ts -// Componente per la gestione delle Assegnazioni -// dei corso ai dipendenti +// Funzioni helper per la gestione delle assegnazioni +// dei corsi ai dipendenti // @author: "villari.andrea@libero.it" // @version: "1.0.0 2026-07-14" //============================================== diff --git a/src/pages/Assegnazioni.tsx b/src/pages/Assegnazioni.tsx index e7f38aa..c768f6b 100644 --- a/src/pages/Assegnazioni.tsx +++ b/src/pages/Assegnazioni.tsx @@ -1,5 +1,5 @@ //========================================================= -// File: Asseganzioni.tsx +// File: Assegnazioni.tsx // componente per la gestione delle assegnazioni dei corsi // ai dipendenti da parte dei Referenti Academy. // @author: "villari.andrea@libero.it" diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 3f77ece..d93257e 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,9 +1,9 @@ //======================================= -// File: Daschborad.tsx -// Componete Dashborad dell'applicazione -// author: "villari.andrea@libero.it" -// version: "1.0.0 2026-07.14" -//====================================== +// File: Dashboard.tsx +// Componente Dashboard dell'applicazione +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//======================================= import { useEffect, useMemo, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { diff --git a/src/pages/MioCorsoDettaglio.tsx b/src/pages/MioCorsoDettaglio.tsx index be63d58..2b41afb 100644 --- a/src/pages/MioCorsoDettaglio.tsx +++ b/src/pages/MioCorsoDettaglio.tsx @@ -1,6 +1,6 @@ //==================================== // File: MioCorsoDettaglio.tsx -// componenete con il dettaglio corso +// Componente con il dettaglio corso // @author: "villari.andrea@libero.it" // @version: "1.0.0 2026-07-14" //==================================== diff --git a/src/pages/Statistiche.tsx b/src/pages/Statistiche.tsx index 42f249b..ddbe282 100644 --- a/src/pages/Statistiche.tsx +++ b/src/pages/Statistiche.tsx @@ -3,7 +3,7 @@ // Componente per la gestione delle statistiche // da parte dei Referenti Academy // @author: "villari.andrea@libero.it" -// @version "1.0.0 2026-07-14" +// @version: "1.0.0 2026-07-14" //============================================= import { useCallback, useEffect, useMemo, useState } from "react"; diff --git a/src/types/index.ts b/src/types/index.ts index 28133b7..17f212b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,8 +1,8 @@ //======================================= // File: index.ts -// Script per la gestione dei tipi utente -// author: "villari.andrea@libero.it" -// version: "1.0.0 2026-07-14" +// Script per la gestione dei tipi utente +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" //======================================= export type Ruolo = "Dipendente" | "Referente Academy"; diff --git a/src/validation/assegnazione.ts b/src/validation/assegnazione.ts index ebb17e9..3721102 100644 --- a/src/validation/assegnazione.ts +++ b/src/validation/assegnazione.ts @@ -1,7 +1,14 @@ -//======================================= +//===================================================== +// File: assegnazione.ts +// script per la gestione della validazione lato +// frontend delle assegnazioni dei corsi ai dipendenti +// da parte dei Referenti Academy +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//===================================================== + import { - buildResult, - normalizePositiveInt, + buildResult, normalizePositiveInt, normalizeString, validateDate, validatePositiveInt, diff --git a/src/validation/constraints.ts b/src/validation/constraints.ts index 3bb4842..015870e 100644 --- a/src/validation/constraints.ts +++ b/src/validation/constraints.ts @@ -1,21 +1,10 @@ //=================================================== // File: constraints.ts // Vincoli derivati da initdb/01_schema.sql (backend). -// author: "villari.andrea@libero.it" -// version: "1.0.0 2026-07-14" +// @author: "villari.andrea@libero.it" +// @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 = { titolo: { maxLength: 200, required: true }, descrizione: { maxLength: 65535, required: false }, @@ -24,10 +13,3 @@ export const CORSO_CONSTRAINTS = { obbligatorio: { required: false }, attivo: { required: false }, } as const; - -export const ASSEGNAZIONE_CONSTRAINTS = { - stato: { - allowedValues: ["Assegnato", "Completato", "Scaduto", "Annullato"] as const, - required: true, - }, -} as const; diff --git a/src/validation/corso.ts b/src/validation/corso.ts index 17ac1d2..99c8f85 100644 --- a/src/validation/corso.ts +++ b/src/validation/corso.ts @@ -1,5 +1,5 @@ //======================================= -// File: cors.ts +// File: corso.ts // Script per la validazione dei dati del // corso lato frontend. // @author: "villari.andrea@libero.it" diff --git a/src/validation/primitives.ts b/src/validation/primitives.ts index 9d6edd1..84c6549 100644 --- a/src/validation/primitives.ts +++ b/src/validation/primitives.ts @@ -1,8 +1,8 @@ //========================================== // File: primitives.ts -// Funzioni di validazione dei dati prmitivi -// author: "villari.andrea@libero.it" -// version: "1.0.0 2026-06-18" +// Funzioni di validazione dei dati primitivi +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" //========================================== export type FieldErrors = Record; diff --git a/src/validation/utente.ts b/src/validation/utente.ts deleted file mode 100644 index 335a521..0000000 --- a/src/validation/utente.ts +++ /dev/null @@ -1,228 +0,0 @@ -//================================================ -// File: utnte.ts -// Script per la validazione dei dati Utente lato -// frontend -// @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"; - -export type Ruolo = (typeof UTENTE_CONSTRAINTS.ruolo.allowedValues)[number]; - -export interface LoginInput { - email: string; - password: string; -} - -export interface UtenteCreateInput { - nome: string; - cognome: string; - email: string; - password: string; - ruolo: Ruolo; -} - -export type UtenteUpdateInput = Partial; - -export function normalizeLoginInput(raw: Record): LoginInput { - return { - email: normalizeEmail(raw.email), - password: raw.password?.toString() ?? "", - }; -} - -export function validateLoginInput( - raw: Record -): ValidationResult { - const data = normalizeLoginInput(raw); - 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); -} - -function validateUtenteProfileFields( - data: UtenteCreateInput, - 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); - - validateRequired(errors, "password", data.password, "Password obbligatoria"); - validateMinLength( - errors, - "password", - data.password, - UTENTE_CONSTRAINTS.password.minLength, - "Password" - ); - validateMaxLength( - errors, - "password", - data.password, - UTENTE_CONSTRAINTS.password.maxLength, - "Password" - ); - - validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio"); - validateEnum( - errors, - "ruolo", - data.ruolo, - UTENTE_CONSTRAINTS.ruolo.allowedValues, - "Ruolo" - ); -} - -export function validateUtenteCreate( - raw: Record -): ValidationResult { - const data: UtenteCreateInput = { - 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 = {}; - - validateUtenteProfileFields(data, errors); - - return buildResult(data, errors); -} - -export function validateUtenteUpdate( - raw: Record -): ValidationResult { - const errors: FieldErrors = {}; - const data: UtenteUpdateInput = {}; - - if ("nome" in raw) { - data.nome = normalizeString(raw.nome); - validateRequired(errors, "nome", data.nome, "Nome obbligatorio"); - validateMaxLength( - errors, - "nome", - data.nome, - UTENTE_CONSTRAINTS.nome.maxLength, - "Nome" - ); - } - - if ("cognome" in raw) { - data.cognome = normalizeString(raw.cognome); - validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio"); - validateMaxLength( - errors, - "cognome", - data.cognome, - UTENTE_CONSTRAINTS.cognome.maxLength, - "Cognome" - ); - } - - if ("email" in raw) { - data.email = normalizeEmail(raw.email); - validateRequired(errors, "email", data.email, "Email obbligatoria"); - validateMaxLength( - errors, - "email", - data.email, - UTENTE_CONSTRAINTS.email.maxLength, - "Email" - ); - validateEmailFormat(errors, "email", data.email); - } - - if ("password" in raw) { - const password = raw.password?.toString() ?? ""; - if (password) { - data.password = password; - validateMinLength( - errors, - "password", - password, - UTENTE_CONSTRAINTS.password.minLength, - "Password" - ); - validateMaxLength( - errors, - "password", - password, - UTENTE_CONSTRAINTS.password.maxLength, - "Password" - ); - } - } - - if ("ruolo" in raw) { - data.ruolo = normalizeString(raw.ruolo) as Ruolo; - validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio"); - validateEnum( - errors, - "ruolo", - data.ruolo, - UTENTE_CONSTRAINTS.ruolo.allowedValues, - "Ruolo" - ); - } - - if (Object.keys(data).length === 0) { - errors._form = "Nessun campo da aggiornare"; - } - - return buildResult(data, errors); -}