Schema DB edu
🎯 Cosa fa
Definisce il persistence layer della formazione: ~38 tabelle
nel dominio edu. Questa pagina documenta in dettaglio le core
del catalogo + erogazione + docenti, allineate al redesign
edu-sessions-redesign del 2026-05-08 (co-erogazione multi-corso,
drop lessons/lessonsCourses, rename courseSessions →
trainingSessions).
🗺️ Tabelle core
Catalogo
| Tabella | Ruolo |
|---|---|
edu.categories | Tassonomia piatta dei corsi |
edu.courses | Definizione corso template organizer-agnostic (FK trainingVariants) |
Le tabelle
edu.lessonseedu.lessonsCoursessono state droppate dal redesign 2026-05-08. La struttura didattica vive ora sutrainingVariants/trainingArguments(fuori scope di questa pagina) +appointmentsCoursesArgumentsper gli argomenti effettivamente trattati nello slot (granularità slot × corso × argomento dal redesign 2026-05-18 "arguments-per-slot", che ha droppato la precedentetrainingSessionsCoursesArguments).
Erogazione
| Tabella | Ruolo |
|---|---|
edu.trainingSessions | Sessione formativa: blocco fisico (giornata/giornate). FK NOT NULL organizers (ente che eroga). FK opzionali locations (aula default), trainingTopics (vincola i corsi), core.recipientGroups (notifiche). Stati: planned/open/inProgress/completed/cancelled |
edu.trainingSessionsCourses | M:N session ↔ corso (co-erogazione). Ogni riga è una "vista contrattuale" del corso erogato nella session, con finestra oraria opzionale per-corso (NULL = eredita dalla session) |
edu.trainingSessionsTeachers | Docenti agganciati alla session (default ereditato dagli appointment se questi non hanno override) |
edu.appointments | Occorrenza fisica (finestra oraria) all'interno di una session. FK trainingSessions NOT NULL, locations override opzionale |
edu.appointmentsCourses | M:N appointment ↔ trainingSessionsCourse: cella della matrice "programma" che dichiara quali corsi sono erogati in ogni slot della session. Finestra oraria opzionale per slot+corso (NULL = copre intero appointment) |
edu.appointmentsCoursesArguments | Registro effettivo degli argomenti trattati per ciascuna cella (appointment, corso). Granularità slot × corso × argomento (redesign 2026-05-18). Sostituisce la precedente trainingSessionsCoursesArguments (granularità sessione+corso) |
edu.trainingVariantsArguments | Catalogo degli argomenti tipici di una variante formativa: pre-popola il popup "Inserisci da catalogo" nello Step 4 del wizard sessioni. Pattern coerente con variantTopicMap |
edu.locations | Aule (indirizzo inline; FK opzionale reg.companies come gestore) |
edu.locationsTrainingVariants | M:N aula ↔ variant per filtrare aule abilitate alle prove pratiche (variant con requiresEquippedRoom = 1) |
Docenti
| Tabella | Ruolo |
|---|---|
edu.teachers | Anagrafica docenti (label = computed firstName + ' ' + lastName) |
edu.appointmentsTeachers | M:N appuntamento ↔ docente (override su default trainingSessionsTeachers, con hourlyAmount opzionale) |
edu.teacherCosts | Costo orario per docente per organizzatore |
edu.teacherSkills | Skill matrix docente ↔ variante normativa (trainingVariantId, score). Granularità a variant (non più a lesson, ridenominata dal redesign 2026-05-08) |
🪟 Viste
| Vista | Ruolo |
|---|---|
edu.vw_appointmentsCalendar | Backing della vista calendario /appointments-calendar: appuntamenti arricchiti con info session, sede, docenti aggregati, primo trainingTopic (per il colore). Colonne aggiuntive: isFunded, fundedProjectId, fundedProjectCode, fundedProjectShortLabel |
edu.vw_appointmentsData | Dati appuntamenti aggregati (totale ore, partecipanti, ecc.). Colonne aggiuntive: isFunded, fundedProjectId, fundedProjectCode, fundedProjectShortLabel |
edu.vw_appointmentAttendees | Partecipanti per appuntamento |
edu.vw_workerTrainingStatus | Stato formativo per coppia (lavoratore × formazione richiesta): status (ok/expiring/expired/missing), daysRemaining, ultima formazione erogata, fonte del requisito (ruolo/mansione/qualifica/attrezzatura/rischio/esenzione). Esposta come CRUD read-only WorkerTrainingStatus. Aggregata da job.vw_workerComplianceSummary |
edu.vw_trainingSessionAppointmentsTeachers | Vista filtrata di appointmentsTeachers per trainingSessionId (usata negli step del session planner) |
edu.vw_workersTrainingsData | Dati formazione per lavoratore |
edu.vw_companyComplianceStatus | Stato compliance per azienda |
edu.vw_trainingExpirations | Formazioni in scadenza |
edu.vw_availableTrainingSessions | Sessioni disponibili (per iscrizione) |
La fundedness in
vw_appointmentsCalendarevw_appointmentsDatanon è denormalizzata: si deriva via join sufin.editions(shared PK conedu.trainingSessions). Vedidocs/superpowers/specs/2026-05-18-edu-fin-bridge-design.md.
vw_workerEffectiveRisksevw_workersDatanon sono visteedu: vivono nello schemajob(TrainingHub.Database/job/Views/). Vedi schema DBjob.
🔗 Relazioni
Referenze esterne:
edu.trainingSessions.organizerSlug→reg.organizers(slug)edu.courses.trainingVariantId→edu.trainingVariants(id)(subsystem normative)edu.trainingSessions.responsibleGroupId→core.recipientGroups(id)(notifiche operatore)edu.trainingSessions.trainingTopicId→edu.trainingTopics(id)edu.locations.companyId→reg.companies(id)(opzionale, gestore aula)edu.teacherCosts.organizerSlug→reg.organizers(slug)
🗂️ Dettaglio tabelle
edu.courses
Template formativo organizer-agnostic: definisce il contenuto
didattico (variante normativa, etichetta, durata) indipendentemente
dall'ente erogatore. L'organizer è agganciato alla sessione
(trainingSessions.organizerSlug), così lo stesso corso può essere
erogato da enti diversi senza duplicazione anagrafica.
- PK:
id - FK:
trainingVariantId → edu.trainingVariants(id) - Campi:
code,label,description,active(default 1),elearning(default 0),inCatalog(default 1),maxPeople,extraInstructorThreshold - Check:
maxPeople > 0,extraInstructorThreshold > 0
edu.trainingSessions
Giornata (o giornate consecutive) di erogazione formativa, può
ospitare uno o più corsi in co-erogazione. L'organizer è
agganciato alla sessione: stesso courses template può essere
erogato da enti diversi in sessioni distinte.
- PK:
id - FK:
organizerSlug → reg.organizers(slug)(NOT NULL, ente organizzatore che eroga la sessione formativa; valorizzato al seed daltrainingRequesto manualmente al primo salvataggio)locationId → edu.locations(id)(opzionale, aula default)responsibleGroupId → core.recipientGroups(id)(opzionale, gruppo destinatari notifiche operatore)trainingTopicId → edu.trainingTopics(id)(opzionale; se valorizzato, vincola i corsi della sessione a quel topic)
- Campi:
organizerSlug(NVARCHAR(128), NOT NULL),startDateTime(required, DATETIME),endDateTime(nullable: NULL = single-block),maxPeople(capienza pratica della giornata),status,notes(1024 char) - Check:
endDateTime IS NULL OR endDateTime >= startDateTimemaxPeople > 0status IN ('planned','open','inProgress','completed','cancelled')
- Indici:
IX_trainingSessions_startDateTime,IX_trainingSessions_organizerSlug,IX_trainingSessions_locationId(filtered),IX_trainingSessions_trainingTopicId(filtered),IX_trainingSessions_responsibleGroupId(filtered)
edu.trainingSessionsCourses
M:N session ↔ corso: ogni riga rappresenta un corso erogato dentro la session, con finestra oraria opzionale per-corso (per co-erogazione di livelli diversi nello stesso giorno).
- PK:
id - FK:
trainingSessionId → edu.trainingSessions(id),courseId → edu.courses(id) - Unique:
(trainingSessionId, courseId)(un corso compare al più una volta per session) - Campi:
startDateTime,endDateTime(entrambi nullable: NULL = eredita dalla session). VincoloCK_trainingSessionsCourses_windowforza la coppia consistente. - Indice:
IX_trainingSessionsCourses_courseId
edu.appointmentsCourses
Cella della matrice "programma" (appointment × corso) che dichiara
quali corsi sono effettivamente erogati in ogni slot della session.
Riga della matrice di Step 4 del wizard SessionPlannerPopup.
- PK:
id(surrogato, dal redesign 2026-05-18; in precedenza la PK era composita(appointmentId, trainingSessionsCourseId)) - FK:
appointmentId → edu.appointments(id)(ON DELETE CASCADE)trainingSessionsCourseId → edu.trainingSessionsCourses(id)(NO ACTION per evitare multi-path cascade conflict)
- Unique:
UX_appointmentsCourses_appCoursesu(appointmentId, trainingSessionsCourseId)(la cella della matrice resta unica nonostante la PK surrogata) - Campi:
startDateTime,endDateTime(entrambi nullable: NULL = copre l'intero appointment; valorizzati = sotto-finestra che deve ricadere dentro l'appointment) - Check:
CK_appointmentsCourses_windowPairforza la coppia start/end consistente (entrambi NULL oppure entrambi valorizzati conendDateTime > startDateTime) - Indici:
UX_appointmentsCourses_appCourse(unique),IX_appointmentsCourses_trainingSessionsCourseId
edu.appointmentsCoursesArguments
Registro effettivo degli argomenti trattati per ciascuna coppia
(appointment, corso). Granularità slot × corso × argomento.
Sostituisce la vecchia trainingSessionsCoursesArguments (granularità
sessione + corso) per supportare il registro lezioni per giornata
(redesign 2026-05-18 "arguments-per-slot"). La cancellazione cascade
dalla parent appointmentsCourses garantisce coerenza quando la cella
(slot, corso) viene rimossa dalla matrice del programma.
- PK:
id - FK:
appointmentsCourseId → edu.appointmentsCourses(id)(ON DELETE CASCADE: la riga vive solo finché esiste la cella nella matrice)trainingArgumentId → edu.trainingArguments(id)(opzionale: NULL = riga ad hoc con sololabel/descriptioncome override)riskId → job.risks(id)(opzionale, override del rischio associato)
- Campi:
label(NVARCHAR(256), override / ad hoc),description(NVARCHAR(1024)) - Indice:
IX_appointmentsCoursesArguments_appointmentsCourseId
edu.trainingVariantsArguments
Catalogo degli argomenti tipici associati a una variante
formativa. Pre-popola il popup "Inserisci da catalogo" nello Step 4
del wizard sessioni (multi-select sugli argomenti suggeriti per la
variante del corso della cella). Pattern coerente con
edu.variantTopicMap (variante ↔ topic).
- PK composita:
(trainingVariantId, trainingArgumentId) - FK:
trainingVariantId → edu.trainingVariants(id)(ON DELETE CASCADE)trainingArgumentId → edu.trainingArguments(id)
- Campi:
sortOrder(INT, nullable: NULL = ordine alfabetico della label dell'argomento) - Indice:
IX_trainingVariantsArguments_trainingArgumentId
edu.trainingSessionsTeachers
Default docenti agganciati alla session: ereditato dagli
appointment che non hanno override su appointmentsTeachers.
- PK composita:
(trainingSessionId, teacherId) - FK: a
trainingSessionseteachers - Indice:
IX_trainingSessionsTeachers_teacherId
edu.appointments
Occorrenza fisica (finestra oraria) all'interno di una session.
- PK:
id - FK:
trainingSessionId → edu.trainingSessions(id)(NOT NULL dal redesign 2026-05-08)locationId → edu.locations(id)(nullable, override dell'aula default della session)
- Campi:
startDateTime,endDateTime(entrambi required),link(NVARCHAR MAX),notes(NVARCHAR MAX) - Check:
endDateTime > startDateTime - Indici:
IX_appointments_trainingSessionId,IX_appointments_locationId(filtered),IX_appointments_startDateTime
lessonIdè stato droppato. L'appuntamento non riferisce più una lezione specifica: il "contenuto" è derivato dalla matriceappointmentsCourses(quali corsi in questo slot) e dai relativiappointmentsCoursesArguments(registro lezioni per slot × corso).
edu.locations
Aule / spazi di erogazione. Entità autosufficiente: l'indirizzo è inline sull'aula stessa, non rimandato a una sede.
- PK:
id - FK:
companyId → reg.companies(id)(opzionale: gestore aula esterno; NULL = aula 3SD) - Campi anagrafici:
shortName,label(required),description,instructions,active,digital,capacity - Tariffa:
unitCost,costMode('hourly' | 'daily'), coppia consistente (entrambi NULL = aula gratuita) garantita daCK_locations_costPair - Contatti / fatturazione:
email,vatCode(P.IVA gestore),invoicingLabel - Indirizzo geocoded:
formattedAddress,country,province,city,zipCode,address,streetNumber,latitude,longitude
edu.locationsTrainingVariants
M:N aula ↔ variante normativa: utilizzata per filtrare le aule
abilitate alle prove pratiche di una variante (requiresEquippedRoom = 1).
edu.teachers
- PK:
id - Campi:
firstName,lastName,label(computed column:firstName || ' ' || lastName),active,notes
edu.appointmentsTeachers
- PK composita:
(appointmentId, teacherId) - FK: a
appointmentseteachers - Campi:
hourlyAmount(nullable: override sul costo standard dateacherCosts) - Indice:
IX_appointmentsTeachers_teacherId
edu.teacherCosts
- PK composita:
(teacherId, organizerSlug) - FK:
teacherId → edu.teachers(id),organizerSlug → reg.organizers(slug) - Campi:
hourlyAmount(required)
edu.teacherSkills
Skill matrix docente ↔ variante normativa: abilitazione del formatore per quella variante (granularità a variant dal redesign 2026-05-08, non più a lesson).
- PK composita:
(teacherId, trainingVariantId) - FK:
teacherId → edu.teachers(id),trainingVariantId → edu.trainingVariants(id) - Campi:
score(nullable),active(default 1) - Indice:
IX_teacherSkills_trainingVariantId(filtered suactive = 1)
edu.trainingTopics
Argomento didattico di alto livello (es. "Antincendio", "Primo
soccorso"). Usato come vincolo opzionale su trainingSessions e
come fonte del colore mostrato nel calendario.
- PK:
id - FK:
categoryId → edu.categories(id),riskId → job.risks(id)(opzionale) - Campi:
code,label,description,requireUpdates,regulated(default 1),fullRetrainingYears,ats(default 0),color(NVARCHAR(16), nullable; hex es.#e53935, usato dal calendario per il colore degli appuntamenti) - Check:
fullRetrainingYears > 0 - Indice:
IX_trainingTopics_riskId(filtered)
edu.categories
- PK:
id - Campi:
label
📚 Tabelle fuori scope
Worker training journey (5)
workersTrainings, workerTrainingDetails, trainingDetailAttempts,
attendances, workerCompletionsCache — registrazione iscrizioni,
presenze, esiti, materializzazione compliance.
Argomenti formativi (8)
trainingArguments, trainingTopics, trainingVariants,
variantTopicMap, trainingTopicDependencies,
trainingArgumentChecks, trainingVariantsOverlaps,
appointmentChecks — macchina normativa degli argomenti formativi,
varianti, sovrapposizioni.
Nomine (3)
nominations, workersNominations, nominationsTrainingTopics —
incarichi di ruolo (es. RSPP, ASPP, preposto) con vincoli formativi.
Certificati & qualifiche (5)
certificates, enrollmentCosts, priorTrainings,
qualificationsExemptions, qualificationsRequiredTrainings —
attestati, costi iscrizione, formazione pregressa, esenzioni e
requisiti.
enrollmentCostsha 2 FK opzionali:invoiceLineId(→inv.invoiceLinesper la fatturazione cliente) eexpenseId(→fin.expensesper la rendicontazione FondItalia). I due possono coesistere (stesso costo fatturato a cliente E rendicontato a fondo). IlcostTypeammette 6 valori:tuition,material,travel,other,staff_internal,staff_external.
Ruoli vs corsi (2)
rolesRequiredTrainings, rolesForbiddenTrainings — matrice ruoli
e corsi richiesti/vietati.
Attrezzature (3)
equipments, workerEquipments, workerCredentials — attrezzature
e credenziali necessarie.
📁 File chiave
TrainingHub.Database/edu/Tables/*.sql— ~38 tabelle- Indici espliciti: vedi sezione "Dettaglio tabelle"
⚠️ Debito tecnico
-
. Risolto in edu-sessions-redesign 2026-05-08: ora èappointments.courseSessionIdnullabletrainingSessionIdNOT NULL. -
Drop. Risolto dallo stesso redesign: il contenuto deriva daappointments.lessonIdtrainingSessionsCourses+appointmentsCourses+appointmentsCoursesArguments(granularità slot×corso×argomento dal redesign 2026-05-18 "arguments-per-slot", che ha droppato la precedentetrainingSessionsCoursesArguments). -
. Sostituito dateacherSkills.lessonIdtrainingVariantIdper allinearsi alla granularità varianti. -
Nessun check su sovrapposizione appuntamenti. A livello DB i constraint restano informativi, ma a livello applicativoISessionPlannerService.DetectConflictsAsyncrileva: docente sovrapposto, sede occupata, iscritti oltre capienza, soglia docente, lavoratore in più sessioni, lezione mancante. Esposto come toast warning dal calendario e dal wizard. -
edu.categoriessenza FK visibili dacourses. La correlazione categoria ↔ corso passa pertrainingVariants: da documentare chiaramente nel subsystem varianti. -
courses.maxPeoplevstrainingSessions.maxPeople. Due limiti distinti — validazione di coerenza non applicata a DB, solo in serviceSessionPlanner(conflictenrollment_overflow). -
teachers.labelcomputed. Colonna calcolata dafirstName + lastName: efficiente ma non indicizzabile. Se serve ricerca, aggiungere indice full-text o persisted computed column. - Cache
workerCompletionsCachenon atomica. Aggiornata da trigger/hook vari: coerenza in caso di failure non chiara.
🔗 Vedi anche
- Panoramica dominio
- Componenti UI
- Logica applicativa
- Dominio
reg: schema DB —headquarters,organizers,companiesreferenziate da edu - Spec di riferimento:
docs/superpowers/specs/_archive/2026-05-08-edu-sessions-redesign-design.md