Passa al contenuto principale

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 courseSessionstrainingSessions).

🗺️ Tabelle core

TabellaRuolo
edu.categoriesTassonomia piatta dei corsi
edu.coursesDefinizione corso template organizer-agnostic (FK trainingVariants)

Le tabelle edu.lessons e edu.lessonsCourses sono state droppate dal redesign 2026-05-08. La struttura didattica vive ora su trainingVariants / trainingArguments (fuori scope di questa pagina) + appointmentsCoursesArguments per gli argomenti effettivamente trattati nello slot (granularità slot × corso × argomento dal redesign 2026-05-18 "arguments-per-slot", che ha droppato la precedente trainingSessionsCoursesArguments).

Erogazione

TabellaRuolo
edu.trainingSessionsSessione 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.trainingSessionsCoursesM: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.trainingSessionsTeachersDocenti agganciati alla session (default ereditato dagli appointment se questi non hanno override)
edu.appointmentsOccorrenza fisica (finestra oraria) all'interno di una session. FK trainingSessions NOT NULL, locations override opzionale
edu.appointmentsCoursesM: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.appointmentsCoursesArgumentsRegistro 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.trainingVariantsArgumentsCatalogo 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.locationsAule (indirizzo inline; FK opzionale reg.companies come gestore)
edu.locationsTrainingVariantsM:N aula ↔ variant per filtrare aule abilitate alle prove pratiche (variant con requiresEquippedRoom = 1)

Docenti

TabellaRuolo
edu.teachersAnagrafica docenti (label = computed firstName + ' ' + lastName)
edu.appointmentsTeachersM:N appuntamento ↔ docente (override su default trainingSessionsTeachers, con hourlyAmount opzionale)
edu.teacherCostsCosto orario per docente per organizzatore
edu.teacherSkillsSkill matrix docente ↔ variante normativa (trainingVariantId, score). Granularità a variant (non più a lesson, ridenominata dal redesign 2026-05-08)

🪟 Viste

VistaRuolo
edu.vw_appointmentsCalendarBacking 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_appointmentsDataDati appuntamenti aggregati (totale ore, partecipanti, ecc.). Colonne aggiuntive: isFunded, fundedProjectId, fundedProjectCode, fundedProjectShortLabel
edu.vw_appointmentAttendeesPartecipanti per appuntamento
edu.vw_workerTrainingStatusStato 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_trainingSessionAppointmentsTeachersVista filtrata di appointmentsTeachers per trainingSessionId (usata negli step del session planner)
edu.vw_workersTrainingsDataDati formazione per lavoratore
edu.vw_companyComplianceStatusStato compliance per azienda
edu.vw_trainingExpirationsFormazioni in scadenza
edu.vw_availableTrainingSessionsSessioni disponibili (per iscrizione)

La fundedness in vw_appointmentsCalendar e vw_appointmentsData non è denormalizzata: si deriva via join su fin.editions (shared PK con edu.trainingSessions). Vedi docs/superpowers/specs/2026-05-18-edu-fin-bridge-design.md.

vw_workerEffectiveRisks e vw_workersData non sono viste edu: vivono nello schema job (TrainingHub.Database/job/Views/). Vedi schema DB job.

🔗 Relazioni

Referenze esterne:

  • edu.trainingSessions.organizerSlugreg.organizers(slug)
  • edu.courses.trainingVariantIdedu.trainingVariants(id) (subsystem normative)
  • edu.trainingSessions.responsibleGroupIdcore.recipientGroups(id) (notifiche operatore)
  • edu.trainingSessions.trainingTopicIdedu.trainingTopics(id)
  • edu.locations.companyIdreg.companies(id) (opzionale, gestore aula)
  • edu.teacherCosts.organizerSlugreg.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 dal trainingRequest o 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 >= startDateTime
    • maxPeople > 0
    • status 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). Vincolo CK_trainingSessionsCourses_window forza 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_appCourse su (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_windowPair forza la coppia start/end consistente (entrambi NULL oppure entrambi valorizzati con endDateTime > 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 solo label/description come 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 trainingSessions e teachers
  • 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 matrice appointmentsCourses (quali corsi in questo slot) e dai relativi appointmentsCoursesArguments (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 da CK_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 appointments e teachers
  • Campi: hourlyAmount (nullable: override sul costo standard da teacherCosts)
  • 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 su active = 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.

enrollmentCosts ha 2 FK opzionali: invoiceLineId (→ inv.invoiceLines per la fatturazione cliente) e expenseId (→ fin.expenses per la rendicontazione FondItalia). I due possono coesistere (stesso costo fatturato a cliente E rendicontato a fondo). Il costType ammette 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

  • appointments.courseSessionId nullable. Risolto in edu-sessions-redesign 2026-05-08: ora è trainingSessionId NOT NULL.
  • Drop appointments.lessonId. Risolto dallo stesso redesign: il contenuto deriva da trainingSessionsCourses + appointmentsCourses + appointmentsCoursesArguments (granularità slot×corso×argomento dal redesign 2026-05-18 "arguments-per-slot", che ha droppato la precedente trainingSessionsCoursesArguments).
  • teacherSkills.lessonId. Sostituito da trainingVariantId per allinearsi alla granularità varianti.
  • Nessun check su sovrapposizione appuntamenti. A livello DB i constraint restano informativi, ma a livello applicativo ISessionPlannerService.DetectConflictsAsync rileva: 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.categories senza FK visibili da courses. La correlazione categoria ↔ corso passa per trainingVariants: da documentare chiaramente nel subsystem varianti.
  • courses.maxPeople vs trainingSessions.maxPeople. Due limiti distinti — validazione di coerenza non applicata a DB, solo in service SessionPlanner (conflict enrollment_overflow).
  • teachers.label computed. Colonna calcolata da firstName + lastName: efficiente ma non indicizzabile. Se serve ricerca, aggiungere indice full-text o persisted computed column.
  • Cache workerCompletionsCache non atomica. Aggiornata da trigger/hook vari: coerenza in caso di failure non chiara.

🔗 Vedi anche