Passa al contenuto principale

Logica applicativa — dominio edu

🎯 Cosa fa

La logica applicativa del dominio edu si distribuisce in tre punti:

  1. 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).
  2. QueryModifiers in BackOffice/Services/QueryModifiers/edu/ — hook pre/post sui CRUD di entità edu specifiche.
  3. Code-behind .razor.cs dei 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.
  • ConflictInfo con ConflictKind enum: 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.cs chiama DetectConflictsAsync dopo 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 (dominio reg) e JobsRisksQueryModifier.
  • 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 su oss.documents) o caricamento PDF già firmato fuori dall'app. Concorrenza ottimistica via fingerprintExpected (StaleEngagementException).
  • CancelEngagementAsync — revoca incarico / sessione annullata; la version corrente, se signed, passa a superseded.

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

FileScopeCosa fa
AppointmentsQueryModifier.csappointmentsHook su CRUD appuntamenti
AppointmentsTeachersQueryModifier.csappointmentsTeachersHook M:N appuntamento↔docente, propaga aggiornamenti su teacherEngagements
TrainingSessionsQueryModifier.cstrainingSessionsHook sessione (validazione cross-corso, eventi notifica)
TrainingSessionsCoursesQueryModifier.cstrainingSessionsCoursesPulizia preventiva su DELETE: rimuove gli appointmentsCourses figli (FK NO ACTION per multi-path cascade) prima di cancellare il corso erogato
TeachersQueryModifier.csteachersHook anagrafica docente
WorkerTrainingDetailsQueryModifier.csworkerTrainingDetailsRefresh di workerCompletionsCache dopo modifiche; invocazione TrainingCompletionService
PriorTrainingsQueryModifier.cspriorTrainingsHook su formazione pregressa
TrainingVariantsQueryModifier.cstrainingVariantsHook su varianti normative
TrainingVariantsOverlapsQueryModifier.cstrainingVariantsOverlapsHook 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 / ICompanyTrainingMatrixService per 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 raggiungono minimumHours, dove L scorre 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 da trainingTopics.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 su trainingVariantId → 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 matrice appointmentsCourses (quali corsi nello slot) e dai relativi appointmentsCoursesArguments (argomenti per slot × corso).

📦 Dipendenze

Runtime:

  • ISimpleCRUDService — CRUD base
  • Service Shared elencati sopra
  • Librerie 3SD Mola / Ploc / TiraPloc per notifiche e trigger
  • Oss.Documents per allegati (engagement PDF, document upload)

Cross-dominio interni:

  • edu.locationsreg.companies (gestore aula opzionale)
  • edu.courses, edu.teacherCostsreg.organizers
  • workerCompletionsCachejob.worker (dominio lavoratori)
  • edu.trainingSessions.responsibleGroupIdcore.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 + implementazioni
  • Shared/Services/Attendance/IAttendanceService.cs
  • Shared/Services/TrainingCompletion/ITrainingCompletionService.cs
  • Shared/Services/Engagement/{ITeacherEngagementService.cs, EngagementFingerprint.cs}
  • Shared/Services/TeacherArea/ITeacherAreaService.cs
  • Shared/Services/PriorTraining/IPriorTrainingService.cs
  • Shared/Services/Pricing/PricingHelpers.cs
  • Shared/Services/BusinessRules/{IBusinessRule.cs, BusinessRuleResult.cs, Rules/}
  • BackOffice/Services/AppointmentsCalendarService.cs (impl)
  • BackOffice/Services/QueryModifiers/edu/*.cs — 9 modifier

⚠️ Debito tecnico

  • Cache workerCompletionsCache coerenza. 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.Shared e TrainingHub.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 in reg/, 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 a Match.Create<string>(s => s.Contains(...)) o smoke runtime test. (Vedi BACKLOG.md.)
  • Race condition SuggestTeachersAsync BulkInsert. Click concorrente su due tab può causare PK violation su appointmentsTeachers. Wrap in transaction o INSERT WHERE NOT EXISTS per riga. (Vedi BACKLOG.md.)
  • ITrainingExpirationService con parametri string. Il parametro status è stringa invece di enum WorkerTrainingStatusValue: meno type-safe.
  • Business Rules Engine aggregator. Le regole WorkerFinalRiskLevelRule e TrainingExemptionRule sono in BusinessRules/Rules/ ma l'aggregator IBusinessRuleEngine e la sostituzione delle query SQL inline restano fuori scope. (Vedi BACKLOG.md.)

🔗 Vedi anche