commit per adattare il data model alla web app Academy aziendale
All checks were successful
Deploy to VPS / build (push) Successful in 16s
Deploy to VPS / deploy (push) Successful in 18s

This commit is contained in:
AV77web 2026-07-14 10:31:30 +02:00
parent d5bd50ef39
commit 3e94fdb6d9
9 changed files with 340 additions and 112 deletions

View file

@ -1,13 +1,13 @@
-- ============================================================
-- Schema tabelle esameits24-26
-- Allineato alle API: /utenti,
-- Schema tabelle esameits24-26 (Academy aziendale)
-- Allineato alle API: /utenti, /corsi, /assegnazioni-corsi, /statistiche
-- ============================================================
USE `esameits24-26`;
-- ------------------------------------------------------------
-- Tabella UTENTI
-- Ruoli: Operatore | Amministratore
-- Tabella UTENTE
-- Ruoli: Dipendente | Referente Academy
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS utente (
UtenteID INT UNSIGNED NOT NULL AUTO_INCREMENT,
@ -15,14 +15,80 @@ CREATE TABLE IF NOT EXISTS utente (
Cognome VARCHAR(100) NOT NULL,
Email VARCHAR(255) NOT NULL,
PasswordHash VARCHAR(255) NOT NULL COMMENT 'Password salvata come hash (es. bcrypt)',
Ruolo ENUM("Amministratore", "Operatore") NOT NULL DEFAULT "Operatore",
Ruolo ENUM('Dipendente', 'Referente Academy') NOT NULL DEFAULT 'Dipendente',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (UtenteID),
UNIQUE KEY uk_utente_email (Email)
UNIQUE KEY uk_utente_email (Email),
KEY idx_utente_ruolo (Ruolo)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------
-- Tabella CORSO (catalogo Academy)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS corso (
CorsoID INT UNSIGNED NOT NULL AUTO_INCREMENT,
Titolo VARCHAR(200) NOT NULL,
Descrizione TEXT NULL,
Categoria VARCHAR(100) NOT NULL,
DurataOre SMALLINT UNSIGNED NOT NULL,
Obbligatorio TINYINT(1) NOT NULL DEFAULT 0,
Attivo TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (CorsoID),
KEY idx_corso_categoria (Categoria),
KEY idx_corso_attivo (Attivo),
CONSTRAINT chk_corso_durata_ore CHECK (DurataOre > 0)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------
-- Tabella ASSEGNAZIONE_CORSO
-- Stati: Assegnato | Completato | Scaduto | Annullato
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS assegnazione_corso (
AssegnazioneID INT UNSIGNED NOT NULL AUTO_INCREMENT,
CorsoID INT UNSIGNED NOT NULL,
DipendenteID INT UNSIGNED NOT NULL,
DataAssegnazione DATE NOT NULL,
DataScadenza DATE NOT NULL,
Stato ENUM('Assegnato', 'Completato', 'Scaduto', 'Annullato') NOT NULL DEFAULT 'Assegnato',
DataCompletamento DATE NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (AssegnazioneID),
KEY idx_assegnazione_corso (CorsoID),
KEY idx_assegnazione_dipendente (DipendenteID),
KEY idx_assegnazione_stato (Stato),
KEY idx_assegnazione_data_assegnazione (DataAssegnazione),
KEY idx_assegnazione_data_scadenza (DataScadenza),
CONSTRAINT fk_assegnazione_corso
FOREIGN KEY (CorsoID) REFERENCES corso (CorsoID)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_assegnazione_dipendente
FOREIGN KEY (DipendenteID) REFERENCES utente (UtenteID)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT chk_assegnazione_scadenza
CHECK (DataScadenza >= DataAssegnazione),
CONSTRAINT chk_assegnazione_completamento
CHECK (DataCompletamento IS NULL OR DataCompletamento >= DataAssegnazione),
CONSTRAINT chk_assegnazione_completato
CHECK (
(Stato = 'Completato' AND DataCompletamento IS NOT NULL)
OR (Stato <> 'Completato' AND DataCompletamento IS NULL)
)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;

View file

@ -1,9 +1,11 @@
-- ============================================================
-- Dati iniziali di test esameits24-26
-- Dati iniziali di test esameits24-26 (Academy aziendale)
-- Password per tutti gli utenti: Password123!
-- 1 Andrea Villari admin@example.it (amministratore)
-- 2 Maria Bianchi maria.bianchi@example.it (operatore)
-- 3 Luca Conti luca.conti@example.it (operatore)
--
-- 1 Andrea Villari admin@academy.it (Referente Academy)
-- 2 Maria Bianchi maria.bianchi@academy.it (Dipendente)
-- 3 Luca Conti luca.conti@academy.it (Dipendente)
-- 4 Elena Rossi elena.rossi@academy.it (Dipendente)
-- ============================================================
USE `esameits24-26`;
@ -12,6 +14,82 @@ USE `esameits24-26`;
-- Utenti
-- ------------------------------------------------------------
INSERT INTO utente (Nome, Cognome, Email, PasswordHash, Ruolo) VALUES
('Andrea', 'Villari', 'admin@example.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Amministratore'),
('Maria', 'Bianchi', 'maria.bianchi@example.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Operatore'),
('Luca', 'Conti', 'luca.conti@example.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Operatore');
('Andrea', 'Villari', 'admin@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Referente Academy'),
('Maria', 'Bianchi', 'maria.bianchi@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Dipendente'),
('Luca', 'Conti', 'luca.conti@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Dipendente'),
('Elena', 'Rossi', 'elena.rossi@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Dipendente');
-- ------------------------------------------------------------
-- Catalogo corsi
-- ------------------------------------------------------------
INSERT INTO corso (Titolo, Descrizione, Categoria, DurataOre, Obbligatorio, Attivo) VALUES
(
'Sicurezza sul lavoro',
'Norme base su rischi, DPI e procedure di emergenza in azienda.',
'Sicurezza',
8,
1,
1
),
(
'GDPR e privacy',
'Trattamento dei dati personali, diritti degli interessati e documentazione privacy.',
'Compliance',
4,
1,
1
),
(
'Comunicazione efficace',
'Tecniche di ascolto attivo, feedback e gestione dei conflitti nel team.',
'Soft Skills',
6,
0,
1
),
(
'Excel avanzato',
'Tabelle pivot, funzioni avanzate e automazioni per report aziendali.',
'IT',
10,
0,
1
),
(
'Primo soccorso (edizione 2023)',
'Corso storico non più disponibile per nuove assegnazioni.',
'Sicurezza',
4,
0,
0
);
-- ------------------------------------------------------------
-- Assegnazioni corsi
-- UtenteID: 2 Maria, 3 Luca, 4 Elena
-- CorsoID: 1 Sicurezza, 2 GDPR, 3 Comunicazione, 4 Excel, 5 Primo soccorso
-- ------------------------------------------------------------
INSERT INTO assegnazione_corso (
CorsoID,
DipendenteID,
DataAssegnazione,
DataScadenza,
Stato,
DataCompletamento
) VALUES
-- Maria: mix di stati per test filtri e statistiche maggio 2026
(1, 2, '2026-05-01', '2026-06-30', 'Assegnato', NULL),
(2, 2, '2026-04-10', '2026-05-20', 'Completato', '2026-05-12'),
(4, 2, '2026-01-15', '2026-03-01', 'Scaduto', NULL),
(3, 2, '2026-03-01', '2026-04-30', 'Annullato', NULL),
-- Luca: completamenti e corsi in corso
(1, 3, '2026-05-05', '2026-06-15', 'Completato', '2026-05-18'),
(2, 3, '2026-05-08', '2026-06-30', 'Assegnato', NULL),
(3, 3, '2026-02-01', '2026-03-15', 'Scaduto', NULL),
-- Elena: dati aggiuntivi per riepiloghi per categoria
(1, 4, '2026-05-10', '2026-07-01', 'Completato', '2026-05-25'),
(2, 4, '2026-05-12', '2026-06-20', 'Completato', '2026-05-28'),
(4, 4, '2026-05-15', '2026-07-15', 'Assegnato', NULL),
(3, 4, '2026-04-01', '2026-05-10', 'Completato', '2026-05-05');

View file

@ -0,0 +1,68 @@
-- ============================================================
-- Migrazione manuale per database già inizializzati
-- Eseguire se il volume MySQL esisteva prima dell'aggiornamento schema Academy.
-- ============================================================
USE `esameits24-26`;
-- Allinea enum ruoli utente (se la tabella esisteva con valori precedenti)
ALTER TABLE utente
MODIFY Ruolo ENUM('Dipendente', 'Referente Academy') NOT NULL DEFAULT 'Dipendente';
UPDATE utente SET Ruolo = 'Referente Academy' WHERE Ruolo IN ('Amministratore', 'Refernete Academy');
UPDATE utente SET Ruolo = 'Dipendente' WHERE Ruolo = 'Operatore';
-- Catalogo corsi
CREATE TABLE IF NOT EXISTS corso (
CorsoID INT UNSIGNED NOT NULL AUTO_INCREMENT,
Titolo VARCHAR(200) NOT NULL,
Descrizione TEXT NULL,
Categoria VARCHAR(100) NOT NULL,
DurataOre SMALLINT UNSIGNED NOT NULL,
Obbligatorio TINYINT(1) NOT NULL DEFAULT 0,
Attivo TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (CorsoID),
KEY idx_corso_categoria (Categoria),
KEY idx_corso_attivo (Attivo),
CONSTRAINT chk_corso_durata_ore CHECK (DurataOre > 0)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
-- Assegnazioni corsi
CREATE TABLE IF NOT EXISTS assegnazione_corso (
AssegnazioneID INT UNSIGNED NOT NULL AUTO_INCREMENT,
CorsoID INT UNSIGNED NOT NULL,
DipendenteID INT UNSIGNED NOT NULL,
DataAssegnazione DATE NOT NULL,
DataScadenza DATE NOT NULL,
Stato ENUM('Assegnato', 'Completato', 'Scaduto', 'Annullato') NOT NULL DEFAULT 'Assegnato',
DataCompletamento DATE NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (AssegnazioneID),
KEY idx_assegnazione_corso (CorsoID),
KEY idx_assegnazione_dipendente (DipendenteID),
KEY idx_assegnazione_stato (Stato),
KEY idx_assegnazione_data_assegnazione (DataAssegnazione),
KEY idx_assegnazione_data_scadenza (DataScadenza),
CONSTRAINT fk_assegnazione_corso
FOREIGN KEY (CorsoID) REFERENCES corso (CorsoID)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_assegnazione_dipendente
FOREIGN KEY (DipendenteID) REFERENCES utente (UtenteID)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT chk_assegnazione_scadenza
CHECK (DataScadenza >= DataAssegnazione),
CONSTRAINT chk_assegnazione_completamento
CHECK (DataCompletamento IS NULL OR DataCompletamento >= DataAssegnazione),
CONSTRAINT chk_assegnazione_completato
CHECK (
(Stato = 'Completato' AND DataCompletamento IS NOT NULL)
OR (Stato <> 'Completato' AND DataCompletamento IS NULL)
)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;

View file

@ -4,6 +4,7 @@
USE `esameits24-26`;
INSERT IGNORE INTO utente (Nome, Cognome, Email, PasswordHash, Ruolo) VALUES
('Andrea', 'Villari', 'admin@example.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Amministratore'),
('Maria', 'Bianchi', 'maria.bianchi@example.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Operatore'),
('Luca', 'Conti', 'luca.conti@example.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Operatore');
('Andrea', 'Villari', 'admin@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Referente Academy'),
('Maria', 'Bianchi', 'maria.bianchi@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Dipendente'),
('Luca', 'Conti', 'luca.conti@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Dipendente'),
('Elena', 'Rossi', 'elena.rossi@academy.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'Dipendente');

View file

@ -17,13 +17,6 @@ export function mapDbError(error: unknown): DbErrorResponse | null {
const message = err.sqlMessage ?? err.message ?? "";
if (err.code === "ER_DUP_ENTRY") {
if (message.includes("uk_cliente_email")) {
return {
status: 409,
error: "Email già registrata",
fields: { email: "Email già registrata" },
};
}
if (message.includes("uk_utente_email")) {
return {
status: 409,
@ -31,39 +24,36 @@ export function mapDbError(error: unknown): DbErrorResponse | null {
fields: { email: "Email già registrata" },
};
}
if (message.includes("uk_chiaveConsegna")) {
return {
status: 409,
error: "Chiave consegna già registrata",
fields: { chiaveConsegna: "Chiave consegna già registrata" },
};
}
return { status: 409, error: "Valore duplicato" };
}
if (err.code === "ER_SIGNAL_EXCEPTION" || err.errno === 1644) {
if (message.includes("DataRitiro")) {
return {
status: 400,
error: "Data ritiro non valida",
fields: { dataRitiro: "Data ritiro non può essere anteriore alla data odierna" },
};
}
if (message.includes("DataConsegna")) {
return {
status: 400,
error: "Data consegna non valida",
fields: { dataConsegna: "Data consegna non può essere anteriore alla data odierna" },
};
}
}
if (err.code === "ER_CHECK_CONSTRAINT_VIOLATED") {
if (message.includes("chk_consegna_almeno_una_data")) {
if (message.includes("chk_corso_durata_ore")) {
return {
status: 400,
error: "Inserire almeno una data di ritiro o di consegna",
fields: { _form: "Inserire almeno una data di ritiro o di consegna" },
error: "Durata corso non valida",
fields: { durataOre: "La durata prevista deve essere maggiore di zero" },
};
}
if (message.includes("chk_assegnazione_scadenza")) {
return {
status: 400,
error: "Date assegnazione non valide",
fields: { dataScadenza: "La data di scadenza non può essere precedente alla data di assegnazione" },
};
}
if (message.includes("chk_assegnazione_completamento")) {
return {
status: 400,
error: "Data completamento non valida",
fields: { dataCompletamento: "La data di completamento non può essere precedente alla data di assegnazione" },
};
}
if (message.includes("chk_assegnazione_completato")) {
return {
status: 400,
error: "Stato assegnazione non coerente",
fields: { stato: "Lo stato Completato richiede una data di completamento" },
};
}
}
@ -71,16 +61,23 @@ export function mapDbError(error: unknown): DbErrorResponse | null {
if (err.code === "ER_ROW_IS_REFERENCED_2") {
return {
status: 409,
error: "Impossibile eliminare: esistono riferimenti collegati",
error: "Impossibile eliminare: esistono assegnazioni collegate al corso",
};
}
if (err.code === "ER_NO_REFERENCED_ROW_2") {
if (message.includes("fk_consegna_cliente")) {
if (message.includes("fk_assegnazione_corso")) {
return {
status: 400,
error: "Cliente non valido",
fields: { clienteId: "Cliente non trovato" },
error: "Corso non valido",
fields: { corsoId: "Corso non trovato" },
};
}
if (message.includes("fk_assegnazione_dipendente")) {
return {
status: 400,
error: "Dipendente non valido",
fields: { dipendenteId: "Dipendente non trovato" },
};
}
return { status: 400, error: "Riferimento non valido" };

View file

@ -1,3 +1,10 @@
//====================================================
// File: auth.ts
// File middleware per gestionre dell'autenticazione
// e le autorizzazini dei dipendenti/referenti academy
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-07-14"
//====================================================
import { getSession } from "@auth/express";
import type { NextFunction, Request, Response } from "express";
import { authConfig } from "../auth/config.js";
@ -18,14 +25,17 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
next();
}
export function requireAmministratore(req: Request, res: Response, next: NextFunction) {
if (req.user?.ruolo !== "Amministratore") {
res.status(403).json({ error: "Operazione riservata agli amministratori" });
export function requireReferenteAcademy(req: Request, res: Response, next: NextFunction) {
if (req.user?.ruolo !== "Referente Academy") {
res.status(403).json({ error: "Operazione riservata ai referenti Academy" });
return;
}
next();
}
/** @deprecated Usare requireReferenteAcademy */
export const requireAmministratore = requireReferenteAcademy;
declare global {
namespace Express {
interface Request {

View file

@ -1,9 +1,15 @@
//=============================================
// File: index.ts
// Script per la gestine degli endpoint /utente
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-07-14"
//=============================================
import bcrypt from "bcryptjs";
import { Router } from "express";
import type { ResultSetHeader, RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import { mapDbError } from "../errors/db.js";
import { requireAmministratore, requireAuth } from "../middleware/auth.js";
import { requireAuth, requireReferenteAcademy } from "../middleware/auth.js";
import type { Ruolo } from "../types/index.js";
import { validateBody } from "../validation/middleware.js";
import {
@ -41,7 +47,7 @@ async function findUtenteById(id: number): Promise<UtenteRow | undefined> {
return rows[0];
}
router.use(requireAuth, requireAmministratore);
router.use(requireAuth, requireReferenteAcademy);
router.get("/", async (_req, res) => {
const [rows] = await pool.execute<UtenteRow[]>(

View file

@ -1,8 +1,18 @@
import { CONSEGNA_CONSTRAINTS, UTENTE_CONSTRAINTS } from "../validation/constraints.js";
//==============================================
// File: index.ts
// Script con i tipi relativi alle varie entità
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-07-14"
//==============================================
import {
ASSEGNAZIONE_CONSTRAINTS,
UTENTE_CONSTRAINTS,
} from "../validation/constraints.js";
export type Ruolo = (typeof UTENTE_CONSTRAINTS.ruolo.allowedValues)[number];
export type StatoConsegna = (typeof CONSEGNA_CONSTRAINTS.stato.allowedValues)[number];
export type StatoAssegnazione =
(typeof ASSEGNAZIONE_CONSTRAINTS.stato.allowedValues)[number];
export interface Utente {
UtenteID: number;
@ -13,23 +23,22 @@ export interface Utente {
Ruolo: Ruolo;
}
export interface Cliente {
ClienteID: number;
Nome: string;
Cognome: string;
Via: string;
Comune: string;
Provincia: string;
Telefono: string;
Email: string;
Note: string | null;
export interface Corso {
CorsoID: number;
Titolo: string;
Descrizione: string | null;
Categoria: string;
DurataOre: number;
Obbligatorio: boolean;
Attivo: boolean;
}
export interface Consegna {
ConsegnaID: number;
ClienteID: number;
DataRitiro: Date | null;
DataConsegna: Date | null;
Stato: StatoConsegna;
ChiaveConsegna: string;
export interface AssegnazioneCorso {
AssegnazioneID: number;
CorsoID: number;
DipendenteID: number;
DataAssegnazione: Date;
DataScadenza: Date;
Stato: StatoAssegnazione;
DataCompletamento: Date | null;
}

View file

@ -2,7 +2,7 @@
// File: constraints.ts
// Vincoli derivati da initdb/01_schema.sql.
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-06-18"
// version: "1.0.0 2026-14-07"
//=========================================
export const UTENTE_CONSTRAINTS = {
@ -10,31 +10,24 @@ export const UTENTE_CONSTRAINTS = {
cognome: { maxLength: 100, required: true },
email: { maxLength: 255, required: true },
password: { minLength: 8, maxLength: 72, required: true },
ruolo: {allowedValues: ["Operatore", "Amministratore"],
required: true,
}
} as const;
export const CLIENTE_CONSTRAINTS = {
nome: { maxLength: 100, required: true },
cognome: { maxLength: 100, required: true },
via: {maxLength: 100, required: true},
comune: {maxLength: 100, required: true},
provincia: {maxLength: 2, required: true},
telefono: { maxLength: 30, required: true },
email: { maxLength: 255, required: true },
note: { required: false },
} as const;
export const CONSEGNA_CONSTRAINTS = {
dataRitiro: { required: false },
dataConsegna: { required: false },
stato: {
allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"],
ruolo: {
allowedValues: ["Dipendente", "Referente Academy"] as const,
required: true,
},
chiaveConsegna: { maxLength: 8, required: true },
clienteId: { required: true },
} as const;
} as const;
export const CORSO_CONSTRAINTS = {
titolo: { maxLength: 200, required: true },
descrizione: { maxLength: 65535, required: false },
categoria: { maxLength: 100, required: true },
durataOre: { min: 1, max: 65535, required: true },
obbligatorio: { required: false },
attivo: { required: false },
} as const;
export const ASSEGNAZIONE_CONSTRAINTS = {
stato: {
allowedValues: ["Assegnato", "Completato", "Scaduto", "Annullato"] as const,
required: true,
},
} as const;