Logica applicativa — dominio edu
🎯 Cosa fa
La logica applicativa del dominio edu si distribuisce in tre
punti:
- Service condivisi in
TrainingHub.Shared/Services/— un roster di service per orchestrazione, lettura analitica, import, compliance, fatturazione. Vivono in Shared perché usati da più applicazioni (BackOffice, Import). - QueryModifiers in
BackOffice/Services/QueryModifiers/edu/— hook pre/post sui CRUD di entità edu specifiche. - Code-behind
.razor.csdei CRUD e delle pagine custom — logica UI specifica.
🔧 Servizi condivisi
Orchestrazione sessioni
ISessionPlannerService
Cuore del wizard di pianificazione: crea sessione + corsi erogati + appuntamenti + iscritti + docenti + stima costi, con rilevamento conflitti.
Tipi principali (in Shared/Services/SessionPlanner/):
SessionPlannerState— stato persistito del wizard (id session, data, topic, corsi pianificati, appointment IDs, iscrizioni, docenti).SessionCoursePlan— il "draft" di un corso erogato dentro la session (orari per-corso, iscritti previsti).ComplianceEnrollmentRequest/ComplianceEnrollmentResult— input/output per l'iscrizione bulk da scadenziario / coda richieste.ConflictInfoconConflictKindenum:teacher_double_booked,location_double_booked,enrollment_overflow,teacher_threshold,worker_double_enrolled,lesson_skipped. SeveritàWarning/Info.CostBreakdown— stima costi (Iscrizioni + Docenze + Aule) per lo step Riepilogo.SessionPlannerSeedKind— origine del seed iniziale:FromScratch,FromTrainingSession,FromTrainingSessionClone,FromRequests,FromWorkers.
Uso tipico:
SessionPlannerPopup(wizard UI) chiama draft + step methods.AppointmentsCalendar.razor.cschiamaDetectConflictsAsyncdopo il salvataggio di un appuntamento e mostra toast warning.
IAppointmentsCalendarService
Data provider della vista calendario /appointments-calendar.
public interface IAppointmentsCalendarService
{
Task<IEnumerable<appointmentsCalendar>> GetAppointmentsAsync(
DateTime periodStart, DateTime periodEnd,
Guid? courseId = null, Guid? locationId = null,
Guid? teacherId = null, Guid? trainingSessionId = null);
Task<SessionContextInfo?> GetSessionContextAsync(Guid trainingSessionId);
Task<IEnumerable<appointmentAttendee>> GetAttendeesAsync(Guid appointmentId);
}
SessionContextInfo aggrega per il pannello dettaglio: label
sessione, corsi erogati con topic+color, stato, conteggi iscritti
vs appuntamenti completati.
Compliance e requisiti formativi
ITrainingExpirationService
Calcola la compliance formativa (read prevalentemente da
workerCompletionsCache):
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);
ITrainingRequirementsService
Espone gli status formativi (workerTrainingStatus) per singolo
lavoratore o per intera azienda. Cita la vista
edu.vw_workerTrainingStatus.
edu.vw_workerTrainingStatus mostra una sola variante attiva per
coppia (lavoratore × topic):
- Se il regime di aggiornamento è dovuto (base completata,
requireUpdates = 1, base non da rifare) → espone solo la variante aggiornamento (isUpdate = 1). - Altrimenti → espone solo la variante base (
isUpdate = 0). - La base si ri-espone se mancante o se
fullRetrainingYearsè superato (il lavoratore deve rifare tutto dall'inizio).
La materializzazione di queste informazioni avviene tramite
edu.sp_refreshWorkerCompletions (vedi sotto).
ICompanyTrainingMatrixService
Costruisce la matrice formativa azienda: righe = lavoratori
(con role + jobs), colonne = topic richiesti dall'azienda, celle
= status + giorni residui + flag aggiornamento. Restituisce
TrainingMatrixData con percentuale compliance aggregata.
IRiskInheritanceService
Propagazione livello di rischio tra entità:
- Cambio ATECO su azienda → aggiorna rischio della mansione → aggiorna rischio del lavoratore.
- Invocato da
CompaniesQueryModifier(dominioreg) eJobsRisksQueryModifier. - Logica di aggregazione vive nelle estensioni statiche del model
worker(worker.UpdateRiskLevel).
Erogazione e iscrizione
IEnrollmentService
Iscrive un lavoratore a un corso erogato dentro una sessione, gestendo i casi di duplicato/coda d'attesa/capienza:
Task<EnrollmentOutcome> EnrollWorkerAsync(
Guid workerId, Guid trainingSessionId, Guid courseId,
bool acceptWaitlist = false, string? notes = null,
CancellationToken cancellationToken = default);
public enum EnrollmentResult
{ Enrolled, Waitlisted, Duplicate, Full, CourseNotInSession }
Usato dallo step "Iscritti" del session planner e dalla gestione coda richieste.
IAttendanceService
Gestione presenze per appuntamento: inizializza, salva draft,
upload documento firmato, chiusura digitale. Espone
AttendanceViewModel per la UI.
ITrainingCompletionService
EvaluateForAppointmentAsync(appointmentId): alla chiusura di un
appuntamento valuta i workerTrainingDetails correlati e, se
tutti gli appuntamenti del trainingSession sono chiusi, calcola
percentuale presenza cumulativa per worker e imposta lo status
(Completed/Failed) + completeDate. Idempotente.
Docenti e firma
ITeacherEngagementService
Engagement (incarico docente) per coppia (trainingSession, teacher):
CreateOrRecalcAsync— crea o ricalcola fingerprint dell'engagement; idempotente se il fingerprint non cambia.SignClickThroughAsync/SignUploadAsync— firma click-through (genera PDF "timbrato" e lo salva suoss.documents) o caricamento PDF già firmato fuori dall'app. Concorrenza ottimistica viafingerprintExpected(StaleEngagementException).CancelEngagementAsync— revoca incarico / sessione annullata; la version corrente, se signed, passa asuperseded.
ITeacherAreaService
Area docente self-service: visibilità sessioni assegnate, download lettere, conferma engagement.
Import e dati pregressi
IPriorTrainingService
Import Excel di crediti formativi pregressi: parse + validate +
bulk insert in priorTrainings. Espone LoadSystemDataAsync
per caching reference data.
ITrainingImportService
Import generale di registri formativi (storico). Vedi
TrainingImportService.{cs,Models.cs,Parser.cs,Processing.cs}
per la struttura a sub-file.
Notifiche e alert
IAlertService
Generazione alert applicativi (banner / counter UI). Lavora di
concerto con le librerie 3SD Mola/Ploc per la persistenza dei
trigger.
🧩 QueryModifiers edu
| File | Scope | Cosa fa |
|---|---|---|
AppointmentsQueryModifier.cs | appointments | Hook su CRUD appuntamenti |
AppointmentsTeachersQueryModifier.cs | appointmentsTeachers | Hook M:N appuntamento↔docente, propaga aggiornamenti su teacherEngagements |
TrainingSessionsQueryModifier.cs | trainingSessions | Hook sessione (validazione cross-corso, eventi notifica) |
TrainingSessionsCoursesQueryModifier.cs | trainingSessionsCourses | Pulizia preventiva su DELETE: rimuove gli appointmentsCourses figli (FK NO ACTION per multi-path cascade) prima di cancellare il corso erogato |
TeachersQueryModifier.cs | teachers | Hook anagrafica docente |
WorkerTrainingDetailsQueryModifier.cs | workerTrainingDetails | Refresh di workerCompletionsCache dopo modifiche; invocazione TrainingCompletionService |
PriorTrainingsQueryModifier.cs | priorTrainings | Hook su formazione pregressa |
TrainingVariantsQueryModifier.cs | trainingVariants | Hook su varianti normative |
TrainingVariantsOverlapsQueryModifier.cs | trainingVariantsOverlaps | Hook su sovrapposizioni varianti |
Registrati in Program.cs come IQueryModifier<T> con
Brighela.SimpleCRUD.
🧩 Cache workerCompletionsCache e SP di refresh
Tabella materializzata che aggrega i completamenti formativi per
lavoratore con stato (ok, expiring, expired, missing).
- Aggiornata da QueryModifiers dopo modifiche a
workerTrainingDetails,priorTrainings,trainingVariants, e analoghi. - Letta da
ITrainingExpirationService/ITrainingRequirementsService/ICompanyTrainingMatrixServiceper performance: evita join pesanti a runtime.
edu.sp_refreshWorkerCompletions — logica calcolo scadenze
Lo SP esegue TRUNCATE + INSERT sulla cache. La logica per la data efficace differisce tra base e aggiornamento:
Base (isUpdate = 0): singolo completamento con ore frequentate
≥ minimumHours. Invariata.
Aggiornamento (isUpdate = 1) — rolling window:
La data efficace non è più calcolata su cicli fissi consecutivi ancorati alla base (un buco passato inchiodava la scadenza alla base di partenza). La nuova semantica è:
La data efficace è l'ultima finestra
(L − validitySpan, L]in cui le ore di aggiornamento frequentate raggiungonominimumHours, doveLscorre all'indietro ancorato all'ultimo completamento valido.
In pratica: si cerca la finestra più recente in cui il lavoratore ha totalizzato le ore minime richieste, indipendentemente da buchi precedenti. Un gap storico non degrada la scadenza corrente — conta solo l'ultimo blocco di ore valide.
Il gate minimumHours resta invariato: se le ore non raggiungono il
minimo nella finestra, lo status rimane missing/expired.
🧩 Code-behind pattern
Pagine page-level scritte a mano
Pages/AppointmentsCalendar/*— vista calendario page-level (filtri, viste mese/settimana/giorno, colori datrainingTopics.color, conflict detection post-save).Components/edu/SessionPlanner/SessionPlannerPopup*— wizard 7-step (Step1Session, Step2Courses, Step3Schedule, Step4Program, Step4Teachers, Step5Workers, Step6Review come partial classes).
Forms con cascade
CourseForm.razor.cs— cascade sutrainingVariantId→ pre-popola campi del corso.LocationForm.razor.cs— combobox headquarters (reg) per associazione aula → sede.AppointmentForm.razor.cs— solo dati appuntamento (data, aula, link). Il "contenuto" viene dalla matriceappointmentsCourses(quali corsi nello slot) e dai relativiappointmentsCoursesArguments(argomenti per slot × corso).
📦 Dipendenze
Runtime:
ISimpleCRUDService— CRUD base- Service Shared elencati sopra
- Librerie 3SD
Mola/Ploc/TiraPlocper notifiche e trigger Oss.Documentsper allegati (engagement PDF, document upload)
Cross-dominio interni:
edu.locations↔reg.companies(gestore aula opzionale)edu.courses,edu.teacherCosts↔reg.organizersworkerCompletionsCache↔job.worker(dominio lavoratori)edu.trainingSessions.responsibleGroupId↔core.recipientGroups
📁 File chiave
Shared/Services/SessionPlanner/— 9 file (ISessionPlannerService.cs,SessionPlannerService.cs, state/plan/result/conflict/cost types,SessionPlannerSeedKind.cs)Shared/Services/{IAppointmentsCalendarService, ITrainingExpirationService, IRiskInheritanceService, ITrainingRequirementsService, IEnrollmentService, ICompanyTrainingMatrixService}.cs+ implementazioniShared/Services/Attendance/IAttendanceService.csShared/Services/TrainingCompletion/ITrainingCompletionService.csShared/Services/Engagement/{ITeacherEngagementService.cs, EngagementFingerprint.cs}Shared/Services/TeacherArea/ITeacherAreaService.csShared/Services/PriorTraining/IPriorTrainingService.csShared/Services/Pricing/PricingHelpers.csShared/Services/BusinessRules/{IBusinessRule.cs, BusinessRuleResult.cs, Rules/}BackOffice/Services/AppointmentsCalendarService.cs(impl)BackOffice/Services/QueryModifiers/edu/*.cs— 9 modifier
⚠️ Debito tecnico
- Cache
workerCompletionsCachecoerenza. Aggiornato da più hook in path diversi; in caso di fallimento parziale il cache può divergere dalla realtà. Valutare rebuild periodico o flag "stale" per detect. - Service in Shared ma logica edu-centrica. Il bound fra
TrainingHub.SharedeTrainingHub.DataLayer.eduè stretto: Shared dipende dal DataLayer di dominio. Accettabile per dimensione attuale ma genera coupling. - QueryModifiers sparsi senza visione d'insieme. 9 modifier
in
edu/+ alcuni inreg/,job/,inv/. Manca documentazione centrale "chi aggiorna cosa quando". - Test contract drift sui mock SQL. Tutti i test usano
It.IsAny<string>()per i parametri SQL: un typo su nome colonna o parametro non viene catturato. Migrare aMatch.Create<string>(s => s.Contains(...))o smoke runtime test. (VediBACKLOG.md.) - Race condition
SuggestTeachersAsyncBulkInsert. Click concorrente su due tab può causare PK violation suappointmentsTeachers. Wrap in transaction o INSERT WHERE NOT EXISTS per riga. (VediBACKLOG.md.) -
ITrainingExpirationServicecon parametri string. Il parametrostatusè stringa invece di enumWorkerTrainingStatusValue: meno type-safe. - Business Rules Engine aggregator. Le regole
WorkerFinalRiskLevelRuleeTrainingExemptionRulesono inBusinessRules/Rules/ma l'aggregatorIBusinessRuleEnginee la sostituzione delle query SQL inline restano fuori scope. (VediBACKLOG.md.)
🔗 Vedi anche
- Panoramica dominio
- Schema DB — viste e tabelle materializzate
- Componenti UI — calendario e session planner
- Dominio
reg: logica applicativa — cascade reg → edu via RiskInheritance