corretti i commenti iniziali dei file
All checks were successful
Deploy to VPS / build (push) Successful in 30s
Deploy to VPS / deploy (push) Successful in 23s

This commit is contained in:
AV77web 2026-07-14 15:09:47 +02:00
parent fafedd26a7
commit af35df94d3
26 changed files with 205 additions and 615 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
# URL del backend API (senza slash finale)
VITE_API_URL=http://localhost:3011

View file

@ -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

View file

@ -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<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

189
README.md
View file

@ -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

View file

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

View file

@ -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";

View file

@ -48,10 +48,6 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
return data as T;
}
export async function fetchHealth(): Promise<{ status: string }> {
return request("/api/health");
}
export async function getCsrfToken(): Promise<string> {
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<void> {
await request("/api/utenti/register", {
method: "POST",
body: JSON.stringify(data),
});
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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 (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4 text-sm text-muted-foreground">
Questa sezione verrà implementata nel prossimo passo.
</p>
<Button variant="outline" onClick={() => navigate(backTo)}>
Torna alla dashboard
</Button>
</CardContent>
</Card>
);
}

View file

@ -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";

View file

@ -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";

View file

@ -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"
//==============================================

View file

@ -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"

View file

@ -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 {

View file

@ -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"
//====================================

View file

@ -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";

View file

@ -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"
// @author: "villari.andrea@libero.it"
// @version: "1.0.0 2026-07-14"
//=======================================
export type Ruolo = "Dipendente" | "Referente Academy";

View file

@ -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,

View file

@ -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;

View file

@ -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"

View file

@ -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<string, string>;

View file

@ -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<UtenteCreateInput>;
export function normalizeLoginInput(raw: Record<string, unknown>): LoginInput {
return {
email: normalizeEmail(raw.email),
password: raw.password?.toString() ?? "",
};
}
export function validateLoginInput(
raw: Record<string, unknown>
): ValidationResult<LoginInput> {
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<string, unknown>
): ValidationResult<UtenteCreateInput> {
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<string, unknown>
): ValidationResult<UtenteUpdateInput> {
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);
}