Passa al contenuto principale

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):

AreaTabellaRuolo
Catalogoedu.categoriesCategorie tematiche (piatte)
Catalogoedu.coursesDefinizione corso template organizer-agnostic (FK trainingVariants)
Erogazioneedu.trainingSessionsSessione formativa: blocco fisico (giornata/giornate). FK organizers NOT NULL (ente erogatore). Stati: planned/open/inProgress/completed/cancelled
Erogazioneedu.trainingSessionsCoursesM:N session↔corso (co-erogazione: una giornata può ospitare più corsi). Nessuna colonna oraria — gli orari per corso stanno su appointmentsCourses
Erogazioneedu.appointmentsOccorrenza fisica all'interno di una session (FK trainingSessions NOT NULL, locations override)
Erogazioneedu.locationsAule (indirizzo inline; FK opzionale reg.companies come gestore)
Erogazioneedu.locationsTrainingVariantsM:N aula↔variant per filtrare aule abilitate alle prove pratiche (variant con requiresEquippedRoom = 1)
Docentiedu.teachersAnagrafica docenti
Docentiedu.appointmentsTeachersM:N appuntamento↔docente, unica fonte dei docenti di una sessione (con unitAmount + costModefixed/daily/hourly)
Docentiedu.teacherCostsCosto per docente per organizzatore: unitAmount + costMode (hourly o daily)
Docentiedu.teacherSkillsSkill matrix docente↔variant normativa (trainingVariantId, score)

Sottodomini adiacenti, dentro edu ma fuori dalle 12 core:

SottodominioOggettiRuolo
Coda richiesteedu.trainingRequests, edu.vw_trainingRequestsData, Components/CRUD/edu/TrainingRequest.razorRichieste 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
Convocazioniedu.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 sessioneedu.sessionDocuments, edu.sessionDocumentTypes, trainingSessions.documentsManagedAtBridge M2M verso oss.documents (Pattern A) più il catalogo dei tipi gestibili. documentsManagedAt valorizzato = sessione «gestita», tutto il wizard in sola lettura
Catalogo/listinoedu.catalogs, Components/CRUD/edu/Catalog.razorEdizioni del listino corsi: testata (etichetta, validità, titolare) con il documento allegato. Non è il catalogo dei corsi, è il suo documento commerciale
Registri presenzeedu.appointmentRegisters, Components/CRUD/edu/AppointmentRegister.razorRegistro 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 indisponibiliedu.locationUnavailabilitiesPeriodi di indisponibilità di un'aula (default: sempre disponibile). Sorgente del conflitto location_unavailable
Obblighi manuali e facoltativeedu.workersRequiredTrainings, edu.vw_workerRequiredTopics, vw_workerOptionalTrainings, vw_workersRequiredTrainingsDataLa 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.lessonsCourses e edu.courseSessionsArguments sono state droppate.
  • edu.courseSessions è stata rinominata edu.trainingSessions con DATETIME invece di DATE e l'aggiunta di locationId ereditato dagli appointment.
  • La relazione 1:1 courseSession → course è stata sostituita dalla M:N trainingSessionsCourses per supportare la co-erogazione (più corsi nella stessa giornata fisica).
  • edu.appointmentArguments è stata rinominata edu.trainingSessionsCoursesArguments (FK a trainingSessionsCourses).
  • edu.teacherSkills.lessonId è stato sostituito da trainingVariantId (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.appointmentsCourses refactor: aggiunta PK surrogata id, la coppia (appointmentId, trainingSessionsCourseId) è ora UNIQUE INDEX. Abilita FK singola dalla tabella argomenti.
  • edu.trainingSessionsCoursesArguments è stata droppata e sostituita da edu.appointmentsCoursesArguments (FK a appointmentsCourses): 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):

AreaTabelle principaliPagina utente
Worker training journeyworkersTrainings, workerTrainingDetails, trainingDetailAttempts, attendances, workerCompletionsCache, certificates, enrollmentCosts, priorTrainingsScheda formativa lavoratore
Argomenti formativitrainingArguments, trainingTopics, trainingVariants, variantTopicMap, trainingVariantsArguments, trainingTopicDependencies, appointmentsCoursesArguments, trainingArgumentChecks, trainingVariantsOverlaps, appointmentChecksArgomenti e varianti
Nominenominations, workersNominations, nominationsTrainingTopicsNomine e incarichi
Ruoli & qualificherolesRequiredTrainings, rolesForbiddenTrainings, qualificationsExemptions, qualificationsRequiredTrainingsRuoli e requisiti
Attrezzature & credenzialiequipments, workerEquipments, workerCredentialsAttrezzature

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/

FileRuolo
ITrainingExpirationService.cs / TrainingExpirationService.csCalcola compliance formativa per azienda/lavoratore: attestati in scadenza, mancanti, statistiche
IRiskInheritanceService.cs / RiskInheritanceService.csSola 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.csOrchestrazione wizard a 8 step (sessione + corsi + appuntamenti + iscritti + docenti + costi) e rilevamento conflitti (DetectConflictsAsync) usati anche dal calendario al salvataggio appuntamento
IAppointmentsCalendarService.cs / AppointmentsCalendarService.csData 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/

FileRuolo
Sono dieci:
FileRuolo
TeachersQueryModifier.csHook CRUD teachers
AppointmentsQueryModifier.csHook CRUD appointments
AppointmentsTeachersQueryModifier.csHook CRUD appointmentsTeachers
CoursesQueryModifier.csHook CRUD courses
TrainingSessionsQueryModifier.csHook CRUD trainingSessions
TrainingSessionsCoursesQueryModifier.csHook CRUD trainingSessionsCourses
PriorTrainingsQueryModifier.csHook CRUD priorTrainings (fuori scope)
TrainingVariantsQueryModifier.csHook CRUD trainingVariants (fuori scope)
TrainingVariantsOverlapsQueryModifier.csHook CRUD trainingVariantsOverlaps (fuori scope)
WorkerTrainingDetailsQueryModifier.csHook 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 con SessionPlannerPopup (pulsante "Nuova Sessione") e AppointmentFormPopup (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 componente CRUD/edu/AppointmentsCalendar.razor non 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 su edu.trainingSessionsCourses.
  • Colore appuntamento dal primo trainingTopic (alfabetico) della session, derivato via edu.trainingSessionsCourses → edu.courses → edu.trainingVariants.trainingTopicId → edu.trainingTopics.color. Background pastello via color-mix CSS, fallback grigio se topic senza colore. Legenda topic dinamica + legenda stati.
  • Click su slot libero → apre AppointmentFormPopup per nuovo appuntamento; click su appuntamento → AppointmentDetailPanel con 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.DetectConflictsAsync e mostra toast warning per ogni ConflictKind rilevato (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 (DetectConflictsAsync ritorna ConflictKind enum 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 + QueryModifier
  • Oss.Filters, Tabiot.Blazor.*, DevExpress.Blazor — UI stack comune

Cross-dominio:

  • edu.trainingSessions.organizerSlugreg.organizers(slug) (la sessione ha un organizer; il corso è un template organizer-agnostic)
  • edu.locations.companyIdreg.companies(id) (opzionale)
  • edu.teacherCosts.organizerSlugreg.organizers(slug)
  • edu.courses.trainingVariantIdedu.trainingVariants(id) (fuori scope)

📁 File chiave

  • Database/edu/Tables/*.sql (54)
  • BackOffice/Components/CRUD/edu/*.razor{,.cs,.tt.cs}
  • BackOffice/Services/QueryModifiers/edu/*.cs
  • Shared/Services/ITrainingExpirationService.cs, IRiskInheritanceService.cs (+ implementazioni)
  • Shared/Services/SessionPlanner/ISessionPlannerService.cs, SessionPlannerService.cs, SessionPlannerState.cs, SessionCoursePlan.cs, ComplianceEnrollmentRequest/Result.cs
  • Shared/Services/IAppointmentsCalendarService.cs + Shared/Services/AppointmentsCalendarService.cs (anche l'implementazione sta in Shared)
  • 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

  • trainingVariantId obbligatorio 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.
  • appointments.courseSessionId nullable. Risolto in edu-sessions-redesign 2026-05-08: ora è trainingSessionId NOT NULL.
  • Cache workerCompletionsCache non atomica. Aggiornata da trigger/hook vari: coerenza in caso di failure non chiara.
  • Validazione vincoli propedeutici lessonsCourses. Risolto in edu-sessions-redesign 2026-05-08 droppando lessonsCourses (i non è chiaro dove (in quale service/query) queste vengano applicate al flusso iscrizione-completamento.
  • Auto-generazione appuntamenti da sessione. Coperto dal SessionPlannerService (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