Dominio edu — Panoramica sviluppatore
🎯 Cosa fa
Il dominio edu (education / training) è il cuore applicativo di
TrainingHub: catalogo formativo, erogazione dei corsi, partecipanti,
certificati, qualifiche, nomine. Ha 54 tabelle — le più di tutto
il solution.
Questa pagina copre le 12 tabelle core (elencate sotto): catalogo + erogazione + docenti. Le restanti 42 tabelle sono elencate come "fuori scope" e saranno documentate in spec successive.
🗺️ Mappa moduli
Database — TrainingHub.Database/edu/
Core in questa documentazione (12):
| Area | Tabella | Ruolo |
|---|---|---|
| Catalogo | edu.categories | Categorie tematiche (piatte) |
| Catalogo | edu.courses | Definizione corso template organizer-agnostic (FK trainingVariants) |
| Erogazione | edu.trainingSessions | Sessione formativa: blocco fisico (giornata/giornate). FK organizers NOT NULL (ente erogatore). Stati: planned/open/inProgress/completed/cancelled |
| Erogazione | edu.trainingSessionsCourses | M:N session↔corso (co-erogazione: una giornata può ospitare più corsi). Nessuna colonna oraria — gli orari per corso stanno su appointmentsCourses |
| Erogazione | edu.appointments | Occorrenza fisica all'interno di una session (FK trainingSessions NOT NULL, locations override) |
| Erogazione | edu.locations | Aule (indirizzo inline; FK opzionale reg.companies come gestore) |
| Erogazione | edu.locationsTrainingVariants | M:N aula↔variant per filtrare aule abilitate alle prove pratiche (variant con requiresEquippedRoom = 1) |
| Docenti | edu.teachers | Anagrafica docenti |
| Docenti | edu.appointmentsTeachers | M:N appuntamento↔docente, unica fonte dei docenti di una sessione (con unitAmount + costMode — fixed/daily/hourly) |
| Docenti | edu.teacherCosts | Costo per docente per organizzatore: unitAmount + costMode (hourly o daily) |
| Docenti | edu.teacherSkills | Skill matrix docente↔variant normativa (trainingVariantId, score) |
Sottodomini adiacenti, dentro edu ma fuori dalle 12 core:
| Sottodominio | Oggetti | Ruolo |
|---|---|---|
| Coda richieste | edu.trainingRequests, edu.vw_trainingRequestsData, Components/CRUD/edu/TrainingRequest.razor | Richieste deferite di iscrizione: una pending per (worker, variante), con organizzatore preferito opzionale. Il session planner le pesca allo step Iscritti e le marca fulfilled alla conferma — tutte quelle del worker per la variante del corso, non solo le pescate |
| Convocazioni | edu.convocationsSent, edu.vw_convocationData, Shared/Services/Convocation/ | Invio delle mail di convocazione e loro audit: una riga per (worker × sessione) con il canale di scatenamento (scheduled, on-demand-session, on-demand-worker) e lo snapshot dell'email usata. La vista produce una riga per (worker × appointment del corso) con orario effettivo, sede e docenti dell'appointment |
| Documenti di sessione | edu.sessionDocuments, edu.sessionDocumentTypes, trainingSessions.documentsManagedAt | Bridge M2M verso oss.documents (Pattern A) più il catalogo dei tipi gestibili. documentsManagedAt valorizzato = sessione «gestita», tutto il wizard in sola lettura |
| Catalogo/listino | edu.catalogs, Components/CRUD/edu/Catalog.razor | Edizioni del listino corsi: testata (etichetta, validità, titolare) con il documento allegato. Non è il catalogo dei corsi, è il suo documento commerciale |
| Registri presenze | edu.appointmentRegisters, Components/CRUD/edu/AppointmentRegister.razor | Registro per appuntamento: signedDocumentId (scansione firmata) e digitalClosedAt/digitalClosedByUserId. La chiusura è la fonte di verità dello stato «completato» dell'appuntamento — edu.appointments non ha colonna status |
| Aule indisponibili | edu.locationUnavailabilities | Periodi di indisponibilità di un'aula (default: sempre disponibile). Sorgente del conflitto location_unavailable |
| Obblighi manuali e facoltative | edu.workersRequiredTrainings, edu.vw_workerRequiredTopics, vw_workerOptionalTrainings, vw_workersRequiredTrainingsData | La sesta fonte di obbligo (dichiarazione per singolo lavoratore), la classificazione required/forbidden/exempted/prerequisiteMissing e i completamenti non dovuti |
Cambio modello del 2026-05-08 (edu-sessions-redesign):
- Le tabelle
edu.lessons,edu.lessonsCourseseedu.courseSessionsArgumentssono state droppate.edu.courseSessionsè stata rinominataedu.trainingSessionsconDATETIMEinvece diDATEe l'aggiunta dilocationIdereditato dagli appointment.- La relazione 1:1
courseSession → courseè stata sostituita dalla M:NtrainingSessionsCoursesper supportare la co-erogazione (più corsi nella stessa giornata fisica).edu.appointmentArgumentsè stata rinominataedu.trainingSessionsCoursesArguments(FK atrainingSessionsCourses).edu.teacherSkills.lessonIdè stato sostituito datrainingVariantId(granularità a variant).- Spec di riferimento:
docs/superpowers/specs/_archive/2026-05-08-edu-sessions-redesign-design.md.Cambio modello del 2026-05-18 (arguments-per-slot):
edu.appointmentsCoursesrefactor: aggiunta PK surrogataid, la coppia(appointmentId, trainingSessionsCourseId)è ora UNIQUE INDEX. Abilita FK singola dalla tabella argomenti.edu.trainingSessionsCoursesArgumentsè stata droppata e sostituita daedu.appointmentsCoursesArguments(FK aappointmentsCourses): la granularità passa da sessione + corso a slot × corso × argomento, abilitando il "registro lezioni" per giornata.- Nuova
edu.trainingVariantsArguments(N:N variante ↔ argomento): catalogo degli argomenti tipici di una variante formativa, usato dal popup "Inserisci da catalogo" nello Step 4 del wizard.- Spec di riferimento:
docs/superpowers/specs/_archive/2026-05-18-arguments-per-slot-design.md.
Estensioni coperte lato utente (dettaglio dev prioritizzato come lavoro futuro):
| Area | Tabelle principali | Pagina utente |
|---|---|---|
| Worker training journey | workersTrainings, workerTrainingDetails, trainingDetailAttempts, attendances, workerCompletionsCache, certificates, enrollmentCosts, priorTrainings | Scheda formativa lavoratore |
| Argomenti formativi | trainingArguments, trainingTopics, trainingVariants, variantTopicMap, trainingVariantsArguments, trainingTopicDependencies, appointmentsCoursesArguments, trainingArgumentChecks, trainingVariantsOverlaps, appointmentChecks | Argomenti e varianti |
| Nomine | nominations, workersNominations, nominationsTrainingTopics | Nomine e incarichi |
| Ruoli & qualifiche | rolesRequiredTrainings, rolesForbiddenTrainings, qualificationsExemptions, qualificationsRequiredTrainings | Ruoli e requisiti |
| Attrezzature & credenziali | equipments, workerEquipments, workerCredentials | Attrezzature |
Lo schema DB dettagliato di queste tabelle estensione non è incluso in questa sezione: Schema DB copre solo le 12 core. Documentazione dev estesa pianificata come lavoro futuro.
Service layer — TrainingHub.Shared/Services/
| File | Ruolo |
|---|---|
ITrainingExpirationService.cs / TrainingExpirationService.cs | Calcola compliance formativa per azienda/lavoratore: attestati in scadenza, mancanti, statistiche |
IRiskInheritanceService.cs / RiskInheritanceService.cs | Sola lettura: due SELECT su job.vw_workerEffectiveRisks (per lavoratore e per azienda). Non propaga nulla — la propagazione è worker.UpdateRiskLevel, vedi dominio job |
SessionPlanner/ISessionPlannerService.cs / SessionPlannerService.cs | Orchestrazione wizard a 8 step (sessione + corsi + appuntamenti + iscritti + docenti + costi) e rilevamento conflitti (DetectConflictsAsync) usati anche dal calendario al salvataggio appuntamento |
IAppointmentsCalendarService.cs / AppointmentsCalendarService.cs | Data provider del calendario: GetAppointmentsAsync con filtri Corso/Sede/Docente/Sessione + range temporale, GetSessionContextAsync per il pannello dettaglio |
I primi due sono in TrainingHub.Shared perché usati anche da TrainingHub.Import oltre che dalla BackOffice. Il SessionPlanner è in Shared perché i suoi tipi (SessionPlannerState, SessionCoursePlan, ComplianceEnrollmentRequest/Result) sono consumati anche dai test.
QueryModifiers — TrainingHub.BackOffice/Services/QueryModifiers/edu/
| File | Ruolo |
|---|---|
| Sono dieci: |
| File | Ruolo |
|---|---|
TeachersQueryModifier.cs | Hook CRUD teachers |
AppointmentsQueryModifier.cs | Hook CRUD appointments |
AppointmentsTeachersQueryModifier.cs | Hook CRUD appointmentsTeachers |
CoursesQueryModifier.cs | Hook CRUD courses |
TrainingSessionsQueryModifier.cs | Hook CRUD trainingSessions |
TrainingSessionsCoursesQueryModifier.cs | Hook CRUD trainingSessionsCourses |
PriorTrainingsQueryModifier.cs | Hook CRUD priorTrainings (fuori scope) |
TrainingVariantsQueryModifier.cs | Hook CRUD trainingVariants (fuori scope) |
TrainingVariantsOverlapsQueryModifier.cs | Hook CRUD trainingVariantsOverlaps (fuori scope) |
WorkerTrainingDetailsQueryModifier.cs | Hook CRUD workerTrainingDetails (fuori scope) |
UI CRUD — TrainingHub.BackOffice/Components/CRUD/edu/
64 entità CRUD auto-generate. Pattern identico a inv e reg (vedi
componenti UI inv). Le 12 core di
questa spec: Category, Course, TrainingSession,
TrainingSessionsCourse, TrainingSessionsTeacher, Appointment,
Location, LocationsTrainingVariant, Teacher, AppointmentsTeacher,
TeacherCost, TeacherSkill.
Componenti specifici non standard:
Pages/AppointmentsCalendar/— pagina calendario appuntamenti scritta a mano (route/appointments-calendar). Sub-componenti:AppointmentsCalendar.razor(host),CalendarGrid.razor(vista mese/settimana/giorno),AppointmentDetailPanel.razor(pannello laterale). Integrata conSessionPlannerPopup(pulsante "Nuova Sessione") eAppointmentFormPopup(click su slot libero).Components/edu/SessionPlanner/— wizard a 8 step di pianificazione sessione formativa (SessionPlannerPopup.razor+ Step1Session/ Step2Courses/Step3Schedule/Step4Teachers/Step5Workers/Step6Review).AppointmentsData.razor— vista dati appuntamenti aggregata
Del CRUD generato legacy sul calendario resta solo il conf
edu/_conf/vw_appointmentsCalendar.dxgrid.conf.json: il componenteCRUD/edu/AppointmentsCalendar.razornon esiste più.
🔧 API pubblica
ITrainingExpirationService
public interface ITrainingExpirationService
{
Task<IEnumerable<trainingExpiration>> GetExpiringTrainingsAsync(
Guid? companyId = null,
string? status = null);
Task<ComplianceStats> GetComplianceStatsAsync(Guid? companyId = null);
}
public record ComplianceStats(
int TotalWorkers,
int CompliantWorkers,
int ExpiredCount,
int ExpiringCount,
int MissingCount,
int Expiring30Count,
int Expiring60Count,
int RequiresFullRetrainingCount,
int Insufficient = 0);
Usato da Company.razor.cs (dominio reg) per il pannello di
compliance aggregata. Lo stato status filtra su ok, expiring,
expired, missing (vedi WorkerTrainingStatusValue in Enums.cs).
IRiskInheritanceService
Legge, non propaga: GetEffectiveRisksAsync(workerId) e
GetEffectiveRisksByCompanyAsync(companyId) sono due SELECT su
job.vw_workerEffectiveRisks. È iniettato solo da Worker.razor.cs,
Company.razor.cs e Program.cs; nessun QueryModifier lo usa.
La propagazione del livello di rischio è
worker.UpdateRiskLevel (TrainingHub.Shared/DataLayer/job/workers.cs:7),
che reg/CompaniesQueryModifier.cs:78 chiama dopo un update azienda —
vedi dominio job → logica applicativa.
🧩 Pattern chiave
Vista calendario custom
Pages/AppointmentsCalendar/AppointmentsCalendar.razor è una vista
calendario degli appuntamenti scritta a mano (non CRUD-generata).
Caratteristiche:
- Viste Mese / Settimana / Giorno con navigazione previous/Oggi/next.
- Filtri Corso / Sede / Docente / Sessione (combobox riempiti da
SimpleCRUD.GetListAsync). Il filtro Corso restringe a cascata l'elenco Sessioni via sub-query suedu.trainingSessionsCourses. - Colore appuntamento dal primo trainingTopic (alfabetico) della
session, derivato via
edu.trainingSessionsCourses → edu.courses → edu.trainingVariants.trainingTopicId → edu.trainingTopics.color. Background pastello viacolor-mixCSS, fallback grigio se topic senza colore. Legenda topic dinamica + legenda stati. - Click su slot libero → apre
AppointmentFormPopupper nuovo appuntamento; click su appuntamento →AppointmentDetailPanelcon azioni Edit / Duplica / Modifica sessione. - Pulsante "Nuova Sessione" apre
SessionPlannerPopup; al confirm ricarica gli appuntamenti del periodo. - Dopo il salvataggio di un appuntamento chiama
ISessionPlannerService.DetectConflictsAsynce mostra toast warning per ogniConflictKindrilevato (docente sovrapposto, sede occupata, iscritti oltre capienza, soglia docente, ecc.). - Query param
?teacherId=<guid>pre-popola il filtro docente (landing da Calendario docente).
Pattern utile da replicare per entità con forte dimensione temporale.
Cache materializzata
workerCompletionsCache (fuori scope) è una tabella materializzata
che aggrega i completamenti formativi per lavoratore. Refreshata da
QueryModifiers dopo operazioni che invalidano il cache (vedi
retrospettiva per riferimenti a lavori recenti sul cache).
Service di dominio per orchestrazione complessa
Per i CRUD semplici la logica vive in QueryModifier (TeachersQueryModifier)
e in .razor.cs custom. Per i flussi complessi sono stati estratti:
ISessionPlannerService— orchestrazione wizard a 8 step (sessione + corsi + appuntamenti + iscritti + docenti + costi) + rilevamento conflitti (DetectConflictsAsyncritornaConflictKindenum con sette valori:teacher_double_booked,location_double_booked,location_unavailable,enrollment_overflow,teacher_threshold,worker_double_enrolled,lesson_skipped).IAppointmentsCalendarService— data provider per la vista calendario.ITrainingExpirationService— compliance/scadenze.
📦 Dipendenze
Brighela.SimpleCRUD— CRUD base + QueryModifierOss.Filters,Tabiot.Blazor.*,DevExpress.Blazor— UI stack comune
Cross-dominio:
edu.trainingSessions.organizerSlug→reg.organizers(slug)(la sessione ha un organizer; il corso è un template organizer-agnostic)edu.locations.companyId→reg.companies(id)(opzionale)edu.teacherCosts.organizerSlug→reg.organizers(slug)edu.courses.trainingVariantId→edu.trainingVariants(id)(fuori scope)
📁 File chiave
Database/edu/Tables/*.sql(54)BackOffice/Components/CRUD/edu/*.razor{,.cs,.tt.cs}BackOffice/Services/QueryModifiers/edu/*.csShared/Services/ITrainingExpirationService.cs,IRiskInheritanceService.cs(+ implementazioni)Shared/Services/SessionPlanner/—ISessionPlannerService.cs,SessionPlannerService.cs,SessionPlannerState.cs,SessionCoursePlan.cs,ComplianceEnrollmentRequest/Result.csShared/Services/IAppointmentsCalendarService.cs+Shared/Services/AppointmentsCalendarService.cs(anche l'implementazione sta inShared)BackOffice/Components/Pages/AppointmentsCalendar/— vista calendario custom (AppointmentsCalendar.razor,CalendarGrid.razor,AppointmentDetailPanel.razor)BackOffice/Components/edu/SessionPlanner/— wizard a 8 step (SessionPlannerPopup.razor+ Step1..Step6 partial classes)
⚠️ Domande aperte / debito tecnico
-
trainingVariantIdobbligatorio ma variante fuori scope. Ogni corso richiede una variante normativa; la gestione delle varianti è in un altro sottodominio complesso (overlap, dipendenze). Creare corso senza aver prima configurato variante è bloccato. Ordine logico di setup da documentare. -
. Risolto in edu-sessions-redesign 2026-05-08: ora èappointments.courseSessionIdnullabletrainingSessionIdNOT NULL. - Cache
workerCompletionsCachenon atomica. Aggiornata da trigger/hook vari: coerenza in caso di failure non chiara. -
Validazione vincoli propedeutici. Risolto in edu-sessions-redesign 2026-05-08 droppandolessonsCourseslessonsCourses(i non è chiaro dove (in quale service/query) queste vengano applicate al flusso iscrizione-completamento. -
Auto-generazione appuntamenti da sessione. Coperto dalSessionPlannerService(wizard a 8 step): la sessione viene creata come bozza, i corsi erogati e gli appuntamenti vengono aggiunti via gli step del wizard.
🔗 Vedi anche
- Schema DB
- Componenti UI
- Logica applicativa
- Aggiungere un campo
- Guida utente: Panoramica formazione (docs-site-user)
- Dominio
reg: panoramica (aziende che iscrivono lavoratori) - Dominio
inv: panoramica (appuntamenti fatturati)