Schema DB edu
🎯 Cosa fa
Definisce il persistence layer della formazione: 54 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 courseSessions →
trainingSessions).
🗺️ Tabelle core
Catalogo
| Tabella | Ruolo |
|---|---|
edu.categories | Tassonomia piatta dei corsi |
edu.courses | Definizione corso template organizer-agnostic (FK trainingVariants) |
Le tabelle
edu.lessonseedu.lessonsCoursessono state droppate dal redesign 2026-05-08. La struttura didattica vive ora sutrainingVariants/trainingArguments(fuori scope di questa pagina) +appointmentsCoursesArgumentsper gli argomenti effettivamente trattati nello slot (granularità slot × corso × argomento dal redesign 2026-05-18 "arguments-per-slot", che ha droppato la precedentetrainingSessionsCoursesArguments).
Erogazione
| Tabella | Ruolo |
|---|---|
edu.trainingSessions | Sessione formativa: blocco fisico (giornata/giornate). FK NOT NULL organizers (ente che eroga). FK opzionali trainingTopics (vincola i corsi), core.recipientGroups (notifiche). locationId, startDateTime ed endDateTime non sono input: sono cache derivata dagli appointment (vedi sotto). Stati: planned/open/inProgress/completed/cancelled |
edu.trainingSessionsCourses | M:N session ↔ corso (co-erogazione). Ogni riga è una "vista contrattuale" del corso erogato nella session: solo id, trainingSessionId, courseId + UQ_trainingSessionsCourses. Nessuna finestra oraria — quella vive su appointmentsCourses |
edu.appointments | Occorrenza fisica (finestra oraria) all'interno di una session. FK trainingSessions NOT NULL, locations override opzionale |
edu.appointmentsCourses | M: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.appointmentsCoursesArguments | Registro 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.trainingVariantsArguments | Catalogo 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.locations | Aule (indirizzo inline; FK opzionale reg.companies come gestore) |
edu.locationsTrainingVariants | M:N aula ↔ variant per filtrare aule abilitate alle prove pratiche (variant con requiresEquippedRoom = 1) |
Docenti
| Tabella | Ruolo |
|---|---|
edu.teachers | Anagrafica docenti (label = computed lastName + ' ' + firstName — cognome prima) |
edu.appointmentsTeachers | M:N appuntamento ↔ docente: unica fonte dei docenti di una session, che si ricavano risalendo dagli appointment (con unitAmount + costMode opzionali) |
edu.teacherCosts | Costo per docente per organizzatore: unitAmount + costMode (hourly default, oppure daily) |
edu.teacherSkills | Skill matrix docente ↔ variante normativa (trainingVariantId, score). Granularità a variant (non più a lesson, ridenominata dal redesign 2026-05-08) |
🪟 Viste
| Vista | Ruolo |
|---|---|
edu.vw_appointmentsCalendar | Backing 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_appointmentsData | Dati appuntamenti aggregati (totale ore, partecipanti, ecc.). Colonne aggiuntive: isFunded, fundedProjectId, fundedProjectCode, fundedProjectShortLabel |
edu.vw_appointmentAttendees | Partecipanti per appuntamento |
edu.vw_workerTrainingStatus | Stato formativo per coppia (lavoratore × formazione richiesta): status (ok/expiring/expired/missing), daysRemaining, ultima formazione erogata, fonte del requisito (ruolo/mansione/qualifica/attrezzatura/rischio/esenzione), più effectiveHours/requiredHours/hoursShortfall (cancello ore: attestato valido ma sotto il minimo richiesto oggi — dimensione separata da status). Esposta come CRUD read-only WorkerTrainingStatus. Aggregata da job.vw_workerComplianceSummary |
edu.vw_trainingSessionAppointmentsTeachers | Vista filtrata di appointmentsTeachers per trainingSessionId (usata negli step del session planner) |
edu.vw_workersTrainingsData | Dati formazione per lavoratore |
edu.vw_companyComplianceStatus | Stato compliance per azienda. okCount e compliancePercent contano solo status = 'ok' AND hoursShortfall = 0; insufficientCount è il contatore delle integrazioni dovute. Lo scarto ore non viene addebitato quando il completamento è assolto da una formazione dichiarata equivalente in edu.trainingVariantsOverlaps (colonna dischargedByOverlap di edu.workerCompletionsCache) |
edu.vw_configurationGaps | Controlli di configurazione che fanno tacere il prospetto (checkKey: missingVariant, variantWithoutMinimumHours, variantWithoutValiditySpan). Backing della griglia generata /edu/vw_configurationGaps e del badge nella KPI bar della dashboard compliance |
edu.vw_trainingExpirations | Formazioni in scadenza |
edu.vw_availableTrainingSessions | Combinazioni (session, course) iscrivibili: planned/open, future, con il conteggio iscritti per corso |
edu.vw_workerRequiredTopics | Classificazione dell'obbligo per lavoratore attivo × argomento candidato: requirementState = required | forbidden | exempted | prerequisiteMissing. Una riga per worker×topic. Consumata da vw_workerTrainingStatus (solo required) e da vw_workerOptionalTrainings (tutto il resto) |
edu.vw_workerOptionalTrainings | Completamenti il cui argomento non è dovuto. Stessa semantica di scadenza degli obbligatori, regime aggiornamento incluso; notRequiredReason distingue facoltativa vera, vietata, esentata, prerequisito mancante. Esclusa dalla compliance. Una riga esce solo se il regime risolto ha evidenza |
edu.vw_workerEffectiveVariant | Variante risolta per lavoratore attivo × formazione × regime (match per CCNL, tag azienda/sede, livello di rischio). Espone minimumHours e validitySpan; nessun filtro di obbligatorietà o completamento — lo applicano i consumer. Materializzata in edu.workerEffectiveVariantCache |
edu.vw_workerTrainingDetailsData | Storico formativo denormalizzato per lavoratore: ore totali/frequentate, deadline per riga, flag isOptional |
edu.vw_workersRequiredTrainingsData | Dichiarazioni manuali di obbligo con la diagnosi del loro effetto: active, oppure la causa per cui non entrano nel piano (forbidden, exempt, waitingPrerequisite, noVariant, inactiveWorker) |
edu.vw_trainingVariantsData | Varianti con topic, categoria, rischio, livello, CCNL e tag azienda |
edu.vw_trainingRequestsData | Coda richieste denormalizzata per filtri e sort della griglia |
edu.vw_convocationData | Sorgente del template mail di convocazione: una riga per (worker × appointment del corso), con orario effettivo (sotto-finestra da appointmentsCourses, fallback sull'appointment), sede e docenti dell'appointment |
edu.vw_sessionCoursesSlots | Slot (appointment × corso di sessione) del session planner |
edu.vw_sessionEnrolledWorkers | Iscrizioni arricchite con azienda del lavoratore per lo step Iscritti. Write-back su edu.workerTrainingDetails via OriginalTable |
La fundedness in
vw_appointmentsCalendarevw_appointmentsDatanon è denormalizzata: si deriva via join sufin.editions(shared PK conedu.trainingSessions). Vedidocs/superpowers/specs/_archive/2026-05-18-edu-fin-bridge-design.md.
vw_workerEffectiveRisksevw_workersDatanon sono visteedu: vivono nello schemajob(TrainingHub.Database/job/Views/). Vedi schema DBjob.
🔗 Relazioni
Referenze esterne:
edu.trainingSessions.organizerSlug→reg.organizers(slug)edu.courses.trainingVariantId→edu.trainingVariants(id)(subsystem normative)edu.trainingSessions.responsibleGroupId→core.recipientGroups(id)(notifiche operatore)edu.trainingSessions.trainingTopicId→edu.trainingTopics(id)edu.locations.companyId→reg.companies(id)(opzionale, gestore aula)edu.teacherCosts.organizerSlug→reg.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,listPriceDECIMAL(15,2) NOT NULL default 0 (prezzo di listino per iscritto; lo sconto convenzione si applica a valle, in stima costi e fattura) - Check:
maxPeople > 0,extraInstructorThreshold > 0,listPrice >= 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 daltrainingRequesto manualmente al primo salvataggio)locationId → edu.locations(id)— cache, non input: è la location condivisa da tutti gli appointment della sessione. Il campo è nascosto nella form CRUD e viene sincronizzato server-side dal triggertr_appointments_aft_iud_syncSessionHeader; scriverlo direttamente viene sovrascritto al primo salvataggio di un appointment (extended property sutrainingSessions.sql:58-62). Stesso trattamento perstartDateTime(:46-50) edendDateTime(:52-56): «NON modificare direttamente: usa gli appointments»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),minPeople(soglia minima di iscritti),status,notes(1024 char),shortLabel(NVARCHAR(512), etichetta compatta calcolata daedu.fn_sessionShortLabel),documentsManagedAt(DATETIME nullable — timestamp con cui lo step Documenti del wizard marca la sessione «gestita»: valorizzato = tutto il wizard passa in sola lettura finché non si annulla la gestione) - Check:
endDateTime IS NULL OR endDateTime >= startDateTimemaxPeople > 0minPeople > 0status 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 (co-erogazione di livelli diversi nello stesso giorno).
- PK:
id - FK:
trainingSessionId → edu.trainingSessions(id),courseId → edu.courses(id) - Unique:
UQ_trainingSessionsCoursessu(trainingSessionId, courseId)— un corso compare al più una volta per session - Campi: nessuno oltre chiave e FK. ⚠️ La tabella non ha finestra
oraria: la co-erogazione a orari diversi si esprime sugli slot,
cioè su
appointmentsCourses.startDateTime/endDateTime. - 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_appCoursesu(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_windowPairforza la coppia start/end consistente (entrambi NULL oppure entrambi valorizzati conendDateTime > 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 sololabel/descriptioncome 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.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),locationUnitCostDECIMAL(15,2) elocationCostModeNVARCHAR(16) — override del costo aula per questo appuntamento; se NULL si usa quello dell'anagrafica aula. È l'unico punto in cui la modalità può esserefixed:edu.locationsammette solohourly/daily - Check:
endDateTime > startDateTimelocationCostMode IS NULL OR locationCostMode IN ('hourly','daily','fixed')- coppia costo/modalità coerente: o entrambi NULL o entrambi valorizzati
locationUnitCost IS NULL OR locationUnitCost >= 0
- 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 matriceappointmentsCourses(quali corsi in questo slot) e dai relativiappointmentsCoursesArguments(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 daCK_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 - FK:
userId → kset.users(id)(nullable) — il collegamento che abilita l'Area Docente: senza, il docente esiste come anagrafica ma non accede. È anche ciò che delimita cosa vede.companyId → reg.companies(id)(nullable) — azienda di riferimento del collaboratore/fornitore
- Campi:
firstName,lastName,label(computed column:lastName + ' ' + firstName, in quest'ordine),active,notes,fiscalCode,vatCode,isInternal(default 0),delegatesTraining(default 0 — il docente affida l'erogazione ad altri), più il blocco indirizzo geocodificato (country,province,city,zipCode,address,streetNumber,formattedAddress,latitude,longitude)
edu.appointmentsTeachers
- PK composita:
(appointmentId, teacherId) - FK: a
appointmentseteachers - Campi:
unitAmountDECIMAL(15,2) ecostModeNVARCHAR(16) (fixed|daily|hourly) — override sul costo standard dateacherCosts.CK_appointmentsTeachers_costModelimita i valori,CK_appointmentsTeachers_costPairimpone che i due campi siano entrambi valorizzati o entrambi nulli.fixedesiste solo qui: è l'importo a forfait per il singolo appuntamento. - Indice:
IX_appointmentsTeachers_teacherId
edu.teacherCosts
- PK composita:
(teacherId, organizerSlug) - FK:
teacherId → edu.teachers(id),organizerSlug → reg.organizers(slug) - Campi:
unitAmountDECIMAL NOT NULL ecostModeNOT NULL con default'hourly'(CK_teacherCosts_costMode:daily|hourly). La tariffa anagrafica può quindi essere giornaliera, non solo oraria; ilfixedè ammesso solo come override per appuntamento.
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 suactive = 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,position(INT nullable — ordinamento manuale delle colonne del prospetto formativo; NULL va in fondo, non in cima)
edu.locationUnavailabilities
Periodi in cui un'aula non è utilizzabile. Il default è l'assenza di righe: aula sempre disponibile.
- PK:
id - FK:
locationId → edu.locations(id) - Campi:
startDateTime,endDateTime(entrambi NOT NULL),allDay(default 0),reason,active(default 1) - Check:
endDateTime > startDateTime - È la sorgente del conflitto
location_unavailable(SessionPlannerService.cs:784-790): un appointment che si sovrappone a una riga attiva produce il warning, con motivo e finestra nel messaggio. Non blocca il salvataggio, come tutti gli altri conflitti.
edu.workersRequiredTrainings
Obbligo formativo dichiarato a mano su un singolo lavoratore, quando
non deriva da mansione, nomina, qualifica, attrezzatura o rischio. È la
sesta fonte di vw_workerTrainingStatus (origin = 'manual').
- FK: al lavoratore e all'argomento formativo
- Nell'ordinamento delle origini
manualviene per prima: se il topic è dovuto anche da un'altra fonte, l'etichetta resta stabile il giorno in cui quella fonte viene tolta. Non è una precedenza di appartenenza: il topic entra nel piano comunque, da qualunque fonte arrivi. - La diagnosi di perché una dichiarazione non produce effetto sta in
vw_workersRequiredTrainingsData(forbidden,exempt,waitingPrerequisite,noVariant,inactiveWorker): senza quella vista una riga dichiarata e inefficace sarebbe indistinguibile da una attiva.
📚 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.
trainingVariants.tagSlug(NVARCHAR(128) NULL) è confrontato con due sorgenti, non una: i tag dell'azienda (reg.companyTags, il default) e quelli della sede del lavoratore (reg.companyLocationTags, l'override). Vocabolario unico: entrambe pescano dal gruppocompany, quindi lo stesso slug può stare su tutti e due i livelli e basta una variante per valore, non una per asse. Stesso campo, due sorgenti — non una seconda colonna. NULL = applicabile ovunque. Filtro duro inedu.vw_workerEffectiveVariant: la variante si applica solo se una delle due sorgenti porta lo stesso tag, e in caso di match da entrambe prevale la sede. Il tier è deciso da quale sorgente contiene lo slug, mai dal gruppo del tag — che infatti ora è lo stesso per entrambe. Precedenza fra varianti candidate:ccnl → tag della sede → tag dell'azienda → variante universale (senza tag) → livello di rischio.
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.
enrollmentCostsha 2 FK opzionali:invoiceLineId(→inv.invoiceLinesper la fatturazione cliente) eexpenseId(→fin.expensesper la rendicontazione FondItalia). I due possono coesistere (stesso costo fatturato a cliente E rendicontato a fondo). IlcostTypeammette 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— 54 tabelleTrainingHub.Database/edu/Views/*.sql— 21 viste (vedi «Viste»)TrainingHub.Database/edu/Functions/— due TVF inline:fn_workerTrainingDeadline— scadenza, stato, giorni rimanenti e scadenza di riformazione completa. Senza accesso a tabelle: è l'unica definizione, condivisa davw_workerTrainingStatusevw_workerOptionalTrainings, così le due non possono divergere.fn_sessionShortLabel— etichetta compatta di una sessione (prefisso comune dei corsi + shortLabel dei livelli di rischio, con fallback aSTRING_AGG).
TrainingHub.Database/edu/Stored Procedures/— il refresh delle tre cache (sp_refreshWorkerCompletions,sp_refreshCompletionsForScope,sp_refreshEffectiveVariantForScope,sp_refreshVariantTopicMap), il reconcile (sp_reconcileWorkerCaches) esp_companyComplianceMatrix.TrainingHub.Database/edu/Triggers/tr_appointments_aft_iud_syncSessionHeader.sql— AFTER INSERT/UPDATE/DELETE suappointments: ricalcola le colonne cachetrainingSessions.{startDateTime,endDateTime,locationId}come MIN/MAX/COUNT-DISTINCT degli appointment. Tiene il DB coerente conSessionPlannerService.SyncSessionHeaderAsync: se tocchi una delle due, tocca anche l'altra.- Indici espliciti: vedi sezione "Dettaglio tabelle"
⚠️ Debito tecnico
-
. Risolto in edu-sessions-redesign 2026-05-08: ora èappointments.courseSessionIdnullabletrainingSessionIdNOT NULL. -
Drop. Risolto dallo stesso redesign: il contenuto deriva daappointments.lessonIdtrainingSessionsCourses+appointmentsCourses+appointmentsCoursesArguments(granularità slot×corso×argomento dal redesign 2026-05-18 "arguments-per-slot", che ha droppato la precedentetrainingSessionsCoursesArguments). -
. Sostituito dateacherSkills.lessonIdtrainingVariantIdper allinearsi alla granularità varianti. -
Nessun check su sovrapposizione appuntamenti. A livello DB i constraint restano informativi, ma a livello applicativoISessionPlannerService.DetectConflictsAsyncrileva sette casi: docente sovrapposto, sede sovrapposta, aula non disponibile (edu.locationUnavailabilities), iscritti oltre capienza, soglia docente, lavoratore in più sessioni, lezione mancante. Esposto come toast warning dal calendario e dal wizard. -
edu.categoriessenza FK visibili dacourses. La correlazione categoria ↔ corso passa pertrainingVariants: da documentare chiaramente nel subsystem varianti. -
courses.maxPeoplevstrainingSessions.maxPeople. Due limiti distinti — validazione di coerenza non applicata a DB, solo in serviceSessionPlanner(conflictenrollment_overflow). -
teachers.labelcomputed. Colonna calcolata dalastName + ' ' + firstName: efficiente ma non indicizzabile. Se serve ricerca, aggiungere indice full-text o persisted computed column. - Cache
workerCompletionsCachenon atomica. Aggiornata da trigger/hook vari: coerenza in caso di failure non chiara.
🔗 Vedi anche
- Panoramica dominio
- Componenti UI
- Logica applicativa
- Dominio
reg: schema DB —headquarters,organizers,companiesreferenziate da edu - Spec di riferimento:
docs/superpowers/specs/_archive/2026-05-08-edu-sessions-redesign-design.md