Els dominis i la columna vertebral
Tres nivells que no s'han de barrejar mai: el compte que paga
(usuaris) → els familiars que s'inscriuen (alumnes, només fills)
→ les seves activitats (inscripcions). El titular del compte no és una
entitat inscribible.
flowchart LR ORG["orgs
centre · arrel multi-tenant"] USR["usuaris
compte que entra i paga"] ROL["rols · permisos
19 permisos, àmbit per rol"] FAM["families
familia_usuaris"] ALU["alumnes
+ alumne_salut"] ACA["temporades · etapes
cursos · festius"] CAT["activitats
+ traduccions, imatges, catàlegs"] HOR["horaris · horari_dies
combinacions · blocs · preu_trams"] INS["inscripcions
+ dies, blocs, docs, autoritzacions"] ABS["absencies"] PAG["pagaments
Stripe només externs"] COM["plantilles_email
enviaments"] CFG["configuracions
consentiments · textos_legals"] AUD["audit_logs · exportacions
processos"] CKE["clickedu_sincronitzacions
clickedu_cache"] MIG["legacy_*
migracio_files · incidencies"] ORG --> USR ORG --> ACA ORG --> CAT USR --> ROL USR --> FAM FAM --> ALU ACA --> CAT CAT --> HOR ALU --> INS HOR --> INS USR --> INS INS --> ABS INS --> PAG INS --> COM CFG --> INS INS --> AUD CKE --> ALU MIG --> INS MIG --> CAT MIG --> USR
Què canvia respecte de GNA
Tot el que hi ha a la columna abans està verificat contra el bolcat real
de Jaume — estructura només, cap fila llegida. Les xifres són del bolcat
(174 taules) i de les consultes SQL incrustades al PHP.
Cap d'aquests canvis és estètic. Cada un és una cosa que avui no es pot fer: una baixa i una alta atòmiques, un filtre de tenant, o simplement saber quantes columnes té una taula sense consultar una fila d'una altra.
| Aspecte | GNA · abans | Model nou · després | Per què importa |
|---|---|---|---|
| Motor | MyISAM a les 174 taules | PostgreSQL | MyISAM no té transaccions, i el canvi d'activitat ha de ser atòmic: mai 0 ni 2 inscripcions |
| Integritat referencial | 0 claus foranes | claus foranes a totes les relacions | mai no s'ha fet complir: hi haurà inscripcions apuntant a activitats esborrades i codi_fill sense fila |
| Codificació | 146 taules utf8 + 28 latin1, inscripcions inclosa |
UTF-8 a tot | el text català és exactament on apareix el mojibake; cal detectar els valors ja doblement codificats abans de convertir |
| Multi-centre | cap columna de tenant | org_id a cada entitat + filtre global |
no-negociable de l'arquitectura, i no es pot afegir després sense tocar cada consulta |
| Històric | arxiu físic: copia les files i les esborra de la taula viva | canvi d'estat amb data efectiva + inscripcio_estats |
4 taules d'inscripcions on codi és únic només dins de cada taula |
| Text per idioma | titol_1..N, i N viu com a dada a SIS_activitats |
files de traducció amb camps amb nom | un esquema el nombre de columnes del qual depèn d'una fila no es pot mantenir |
| Dies i preus d'un horari | dia_1..7 × 7 grups = 49 columnes |
horari_dies, una fila per dia |
consultar «qui té classe dimarts» avui és una unió de set columnes |
| Extraescolars i Altres | taules físicament diferents per secció (_estiu_nou) |
una sola forma + seccio |
tota regla nova s'ha d'escriure dues vegades, i una de les dues s'oblida |
| Credencials | en quatre llocs, una en clar, una a cada fila d'inscripció | Cognito · cap contrasenya a la nostra BD | totes les columnes de credencial es descarten a la importació |
| Identitat del pare i del fill | desnormalitzada sobre cada inscripció (dni_pare, nom_mare, nom_fill…) |
usuaris → families → alumnes |
és la decisió oberta #1: fidelitat exacta de l'històric o només llegibilitat |
| Dades de salut | a fills_registre i també a inscripcions |
alumne_salut, un sol lloc, amb consentiment |
dues còpies volen dir dues retencions i dues fuites possibles |
| Còpies dins de producció | 14 taules de còpia i proves amb noms indistingibles de les reals | llista d'exclusió explícita | inscripcions_copy al costat d'inscripcions és una importació errònia esperant a passar |
Activitats i text per idioma
A GNA una activitat no sap quants títols té: el nombre de columnes titol_N el
decideix SIS_activitats.titols_elements, una fila d'una altra taula. titol_2
està etiquetat «text peu foto» — no és un segon títol, és el peu de la fotografia,
i per això aterra a la imatge i no a l'activitat.
Els materials i els documents no són relacions: són cadenes d'identificadors separades per coixinets dins d'una columna de text. La migració les ha de partir.
erDiagram
SIS_activitats {
int titols_elements "quantes columnes titol_N existeixen"
int textos_elements "quantes columnes text_N existeixen"
int dates_elements
int links_elements
int galeria_elements
}
w_activitats {
int codi PK
int codi_seccio "1 extraescolars / 2 activitats"
int codi_categoria "sense clau forana"
date data_1_fins_4 "quatre ranures de data"
time hora_1_fins_4 "quatre ranures d hora"
text material "cadena d ids en una columna de text"
text documents "cadena d ids en una columna de text"
int activitat_per_interns_o_externs "1 intern / 2 extern / 0 tots dos"
int actiu
int validada
int finalitzada
int llistes_eliminades
int ordre
text cursos "orfe · cap Set_ hi escriu mai"
}
w_activitats_idiomes {
int codi_ref "relació no declarada"
text codi_idioma
text titol_1_fins_N "N el decideix SIS_activitats"
text text_1_fins_N
}
w_activitats_fotos {
int codi PK
text nom_foto
text nom_fotop
int principal
int ordre
}
w_activitats_fotos_idiomes {
int codi_ref
text titol_1 "text peu foto"
text preus_desde
}
w_tipus_activitats {
int codi PK
int codi_seccio "categories fitades per secció"
text emails_responsables
}
w_activitats ||..o{ w_activitats_idiomes : "codi_ref, sense clau forana"
SIS_activitats ||..o{ w_activitats_idiomes : "decideix quantes columnes"
w_activitats ||..o{ w_activitats_fotos : "codi_ref, sense clau forana"
w_activitats_fotos ||..o{ w_activitats_fotos_idiomes : "codi_ref, sense clau forana"
w_tipus_activitats ||..o{ w_activitats : "codi_categoria, sense clau forana"
erDiagram
activitats {
uuid id PK
uuid tipus_activitat_id FK
enum seccio "extraescolars / altres"
enum audience "interns / externs / tots"
date date_from
date date_to
enum state "esborrany / publicada / validada / finalitzada / arxivada"
}
activitat_traduccions {
uuid activitat_id FK
text locale "ca obligatori"
text name "era titol_1"
text description "era text_1"
text conditions
}
activitat_imatges {
uuid activitat_id FK
text storage_key
int ordre "l ordre del panell és l ordre del portal"
bool is_cover "era principal"
}
activitat_imatge_traduccions {
uuid activitat_imatge_id FK
text locale
text caption "era titol_2, el peu de foto"
}
activitat_materials {
uuid activitat_id FK
uuid material_id FK
bool required
}
activitat_documentacions {
uuid activitat_id FK
uuid documentacio_id FK
bool blocks_confirmation
}
tipus_activitats {
uuid id PK
enum seccio
}
materials {
uuid id PK
numeric price
}
documentacions {
uuid id PK
enum kind "requerida / lliurament"
}
tipus_activitats ||--o{ activitats : "categoritza"
activitats ||--o{ activitat_traduccions : "traduïda a"
activitats ||--o{ activitat_imatges : "il·lustrada per"
activitat_imatges ||--o{ activitat_imatge_traduccions : "peu de foto"
activitats ||--o{ activitat_materials : "requereix"
materials ||--o{ activitat_materials : "usat per"
activitats ||--o{ activitat_documentacions : "demana"
documentacions ||--o{ activitat_documentacions : "demanada per"
| A GNA | Al model nou |
|---|---|
titol_1 | activitat_traduccions.name |
titol_2 (text peu foto) | activitat_imatge_traduccions.caption |
text_1 | activitat_traduccions.description |
codi_seccio 1 / 2 | activitats.seccio = extraescolars / altres |
activitat_per_interns_o_externs 0 / 1 / 2 | activitats.audience = tots / interns / externs |
material, documents (cadenes) | activitat_materials, activitat_documentacions |
SIS_activitats.titols_elements | no existeix — el nombre de camps ja no és una dada |
cursos (columna òrfena) | no es migra |
Horaris, dies, places i preus
Un sol horari de GNA porta set grups de set columnes: dia, hora d'inici,
hora de fi, cupo, places, places mínimes i preu, multiplicat per _1 fins a _7.
Són 49 columnes en una taula filla, i qualsevol pregunta per dia és una unió de set columnes.
I la secció Altres no reutilitza res: té les seves pròpies taules
(_estiu_nou, _blocs_preus), amb hora_inici_1..3 i
preu_1..3. Dues formes per al mateix concepte volen dir que cada regla nova s'escriu dues
vegades — i que una de les dues s'oblida.
erDiagram
w_activitats_horaris {
int codi PK
int codi_ref
int codi_curs
int codi_horari
int dia_1 "fins a dia_7"
time hora_inici_1 "fins a hora_inici_7"
time hora_fi_1 "fins a hora_fi_7"
int cupo_1 "fins a cupo_7"
int places_1 "fins a places_7"
int places_minimes_1 "fins a places_minimes_7"
numeric preu_1 "fins a preu_7 · 49 columnes en total"
int horari_convenir
numeric preuc1 "preuc1 fins a preuc3"
int places_combinades
text places_combinades_cursos
}
w_activitats_horaris_combinacions {
int codi PK
int codi_ref
int codi_horari
int dia_1 "fins a dia_7 un altre cop"
numeric preu
}
w_activitats_horaris_estiu_nou {
int codi PK "taula PRÒPIA de la secció altres"
int codi_ref
int codi_curs
int activitat_sense_horari
int places_combinades
}
w_activitats_horaris_estiu_nou_blocs_preus {
int codi_activitats_horari_estiu_nou
int codi_bloc
int places
int cupos
time hora_inici_1 "fins a hora_inici_3"
time hora_fi_1 "fins a hora_fi_3"
numeric preu_1 "fins a preu_3"
}
w_horaris_convenir_taula {
int codi_ref
int codi_curs
int dia_1 "fins a dia_7 una tercera vegada"
time hora_inici_1 "fins a hora_inici_7"
}
w_blocs {
int codi PK
int ordre
}
w_activitats_horaris ||..o{ w_activitats_horaris_combinacions : "codi_horari, sense clau forana"
w_activitats_horaris_estiu_nou ||..o{ w_activitats_horaris_estiu_nou_blocs_preus : "sense clau forana"
w_blocs ||..o{ w_activitats_horaris_estiu_nou_blocs_preus : "codi_bloc, sense clau forana"
erDiagram
horaris {
uuid id PK
uuid activitat_id FK
uuid curs_id FK "NULL val per a tots els cursos"
enum kind "fix / a_convenir"
}
horari_dies {
uuid horari_id FK
int weekday "1 a 7 · una FILA per dia"
time time_from
time time_to
int places
int places_minimes
int cupo
numeric price
}
horari_combinacions {
uuid horari_id FK
text label
numeric price "era preuc1 fins a preuc3"
bool mandatory_days
}
horari_combinacio_dies {
uuid horari_combinacio_id FK
int weekday
}
blocs {
uuid id PK
date date_from
date date_to
}
horari_blocs {
uuid horari_id FK
uuid bloc_id FK
int places
int cupo
}
horari_bloc_franges {
uuid horari_bloc_id FK
int ordre "1 a 3"
time time_from
time time_to
numeric price
}
preu_trams {
uuid activitat_id FK
uuid horari_id FK
enum target "base / treballador / fill_treballador / familiar_treballador / extern"
enum mode "fix / percentatge"
numeric value
}
horaris ||--o{ horari_dies : "es fa els dies"
horaris ||--o{ horari_combinacions : "ofereix combinacions"
horari_combinacions ||--o{ horari_combinacio_dies : "inclou"
horaris ||--o{ horari_blocs : "per període"
blocs ||--o{ horari_blocs : "aplicat a"
horari_blocs ||--o{ horari_bloc_franges : "amb franges"
horaris ||--o{ preu_trams : "tarifa"
| A GNA | Al model nou |
|---|---|
dia_N, hora_inici_N, hora_fi_N, cupo_N, places_N, places_minimes_N, preu_N | una fila d'horari_dies per dia |
preuc1..3 + w_activitats_horaris_combinacions | horari_combinacions + horari_combinacio_dies |
w_activitats_horaris_estiu_nou i la seva _blocs_preus | les mateixes horaris + horari_blocs + horari_bloc_franges |
hora_inici_1..3 / preu_1..3 dels blocs | horari_bloc_franges, ordre 1 a 3 |
horari_convenir + w_horaris_convenir_taula | horaris.kind = a_convenir + inscripcio_preferencies_horari |
| preus per tipus d'usuari repartits per la fitxa | preu_trams, amb caiguda al tram base |
w_activitats_horaris_combiancions (DELETE mal escrit, codi mort) | no es migra — mai va esborrar res |
Inscripcions i històric
És la taula més rica de GNA i la que pitjor envelleix: 54 columnes amb
els dos progenitors i el fill desnormalitzats sobre cada fila, credencials incloses, i
dades de salut duplicades des de fills_registre.
L'històric no és un registre: és un trasllat físic. «Esborrar llistes»
copia les files cap a inscripcions_historic i les esborra de la taula viva —
sense transacció, perquè MyISAM no en té. Multiplicat per la secció d'estiu, l'històric d'una família
viu repartit en quatre taules on codi només és únic dins de cada una.
erDiagram
inscripcions {
int codi PK "únic NOMÉS dins d aquesta taula"
int eliminada "bandera de baixa"
date data_eliminada
text user "credencial"
text pass "amb el comentari pwd de l autor"
text dni_pare
text nom_pare
text cognoms_pare
text email_pare
text telefon_pare
text dni_mare "i tot el joc _mare repetit"
text nom_mare
text email_mare
text adresa
text codi_postal
int codi_fill "sense fila garantida a fills_registre"
text nom_fill
text cognom1_fill
text cognom2_fill
text catsalut "targeta sanitària"
text alergies "salut, també aquí"
text obs_alimentacio
text obs_natacio
text temporada "text, calculat al PHP"
text dies "cadena"
text hores "cadena"
text cursos "cadena"
text horari_convenir1 "fins a 3"
text material "cadena d ids"
text ip
numeric total
text comentaris
}
inscripcions_historic {
int codi PK "arxiu FÍSIC, no auditoria"
text mateixes_columnes "hi arriba per còpia, i s esborra de la viva"
}
inscripcions_estiu {
int codi PK "variant de la secció altres"
text sense_codi_combinacio "ni hores, ni horari_convenir1..3"
text string_horaris_blocs "afegides"
text string_horari
}
inscripcions_estiu_historic {
int codi PK
text quarta_taula "el mateix patró un altre cop"
}
inscripcions_cupos {
int codi PK "gairebé un clon d inscripcions"
}
accio_inscripcions {
text usuari "TERCER lloc amb credencials"
text password
}
inscripcions ||..o{ inscripcions_historic : "còpia i esborrat"
inscripcions_estiu ||..o{ inscripcions_estiu_historic : "còpia i esborrat"
erDiagram
usuaris {
uuid id PK
enum kind "intern / extern / treballador"
}
families {
uuid id PK
}
alumnes {
uuid id PK
uuid familia_id FK
date birth_date
}
alumne_salut {
uuid alumne_id PK "la salut, en UN sol lloc"
text allergies
text obs_alimentacio
text obs_natacio
}
inscripcions {
uuid id PK
uuid temporada_id FK "era un text calculat al PHP"
uuid alumne_id FK "era nom_fill i cognom1_fill"
uuid activitat_id FK
uuid horari_id FK
uuid usuari_id FK "era dni_pare, nom_pare, email_pare"
enum state "la baixa és un estat, no un DELETE"
date baixa_effective_at
uuid previous_inscripcio_id FK "cadena del canvi atòmic"
numeric total
inet ip
}
inscripcio_dies {
uuid inscripcio_id FK
int weekday "era la cadena dies"
time time_from
time time_to
}
inscripcio_blocs {
uuid inscripcio_id FK
uuid bloc_id FK "era string_horaris_blocs"
}
inscripcio_estats {
uuid inscripcio_id FK
enum from_state
enum to_state
timestamptz at "substitueix les taules _historic"
uuid by_usuari_id FK
text reason
}
inscripcio_materials {
uuid inscripcio_id FK
uuid material_id FK "era una cadena d ids"
int qty
}
inscripcio_preferencies_horari {
uuid inscripcio_id FK
int ordre "era horari_convenir1 fins a 3"
bool assigned
}
usuaris ||--o{ families : "titular de"
families ||--o{ alumnes : "té fills"
alumnes ||--|| alumne_salut : "dades de salut"
alumnes ||--o{ inscripcions : "inscrit a"
usuaris ||--o{ inscripcions : "paga"
inscripcions ||--o{ inscripcio_dies : "els dies"
inscripcions ||--o{ inscripcio_blocs : "els períodes"
inscripcions ||--o{ inscripcio_materials : "amb material"
inscripcions ||--o{ inscripcio_preferencies_horari : "preferències"
inscripcions ||--o{ inscripcio_estats : "historial"
inscripcions }o--|| inscripcions : "substitueix"
| A GNA | Al model nou |
|---|---|
4 taules (inscripcions, _historic, _estiu, _estiu_historic) | 1 taula inscripcions + inscripcio_estats |
eliminada + DELETE dur | state = baixa + baixa_effective_at |
dni_pare, nom_pare, email_pare, tot el joc _mare | usuari_id → usuaris, i familia_usuaris.relation |
nom_fill, cognom1_fill, cognom2_fill, codi_fill | alumne_id → alumnes |
alergies, obs_alimentacio, obs_natacio, catsalut | alumne_salut + alumnes.health_card_number |
dies, hores, cursos (cadenes) | inscripcio_dies, una fila per dia |
material (cadena d'ids) | inscripcio_materials |
horari_convenir1..3 | inscripcio_preferencies_horari.ordre |
temporada (text calculat al PHP) | temporada_id → temporades |
user, pass | es descarten — cap credencial es migra |
inscripcions_cupos, accio_inscripcions | decisió oberta — veure la secció Pendent |
Usuaris, autenticació i credencials
GNA prova tres verificadors en ordre: registre_antics_alumnes,
registre_estiu i ClickEdu. El primer no existeix — el codi el consulta, però
al bolcat no hi ha cap taula amb aquest nom, de manera que aquest camí no ha autenticat mai ningú. Els que
funcionen són dos, i són exactament la definició d'intern i extern:
taula local = extern, ClickEdu = intern.
Les contrasenyes són el pitjor del sistema antic: registre_estiu.password és
text en clar, la generada viatja com a paràmetre d'URL i s'imprimeix
a la pàgina de confirmació, i n'hi ha còpies a inscripcions.pass,
accio_inscripcions.password i bet_users.password. Cap arriba al model nou.
erDiagram
registre_antics_alumnes {
text usuari "CAP TAULA amb aquest nom al bolcat"
text password "primer verificador · mort"
}
registre_estiu {
int codi PK "AUTO_INCREMENT 201.355"
text usuari
text password "TEXT EN CLAR"
text dni
text nom
text cognom1
text cognom2
text email
text data_naixement
text fills
}
fills_registre {
int codi PK "AUTO_INCREMENT 111.996"
int codi_pare "sense clau forana"
text nom
text data_naixement
text malalties "salut de menors"
text obs_alimentacio
text obs_natacio
}
antics_alumnes {
int codi PK
text nif
text promocio
text email
}
clickedu_info {
text user "clau del cau"
text token
text credentials
text info
text students_data
text stages_data
}
bet_users {
int id PK "usuaris de Joomla"
text username
text password
}
registre_estiu ||..o{ fills_registre : "codi_pare, sense clau forana"
registre_estiu ||..o{ bet_users : "còpia a Joomla, l única amb resum"
erDiagram
usuaris {
uuid id PK
enum kind "intern / extern / treballador"
enum auth_source "clickedu / local"
text clickedu_username UK "un únic tipus d identificador"
text cognito_sub UK
text email UK
text dni UK "amb lletra de control"
enum status "actiu / pendent_validacio / suspes"
bool is_staff
text password_hash "no autentica · futura migració"
}
rols {
uuid id PK
text code UK "grup de Cognito"
}
permisos {
uuid id PK
text code UK "catàleg tancat de 19"
enum level "baix fins a molt_alt"
}
rol_permisos {
uuid rol_id FK
uuid permis_id FK
enum scope "el més ampli guanya"
}
usuari_rols {
uuid usuari_id FK
uuid rol_id FK
}
families {
uuid id PK
text clickedu_family_id UK
}
familia_usuaris {
uuid familia_id FK
uuid usuari_id FK
enum relation "pare / mare / tutor"
bool custody_exclusive
}
alumnes {
uuid id PK
uuid familia_id FK
text clickedu_student_id UK
enum origin "clickedu / local"
}
alumne_salut {
uuid alumne_id PK "retenció i consentiment propis"
text allergies
text malalties
bool share_with_monitors
}
clickedu_cache {
uuid usuari_id FK
jsonb payload "cap credencial"
timestamptz expires_at
}
usuaris ||--o{ usuari_rols : "té assignats"
rols ||--o{ usuari_rols : "assignat a"
rols ||--o{ rol_permisos : "atorga"
permisos ||--o{ rol_permisos : "atorgat per"
usuaris ||--o{ familia_usuaris : "pertany a"
families ||--o{ familia_usuaris : "té titulars"
families ||--o{ alumnes : "té fills"
alumnes ||--|| alumne_salut : "dades de salut"
usuaris ||--o| clickedu_cache : "cau de sessió"
| A GNA | Al model nou |
|---|---|
registre_estiu (extern) + ClickEdu (intern) | usuaris.kind + usuaris.auth_source |
registre_antics_alumnes | no existeix i no ha existit mai — no es migra |
registre_estiu.password (en clar) | Cognito · password_hash no autentica |
inscripcions.pass, accio_inscripcions.password, bet_users.password | es descarten a la importació |
fills_registre.codi_pare | alumnes.familia_id → families |
fills_registre.malalties, obs_* | alumne_salut, amb consentiment explícit |
clickedu_info (blobs JSON per login) | clickedu_cache, amb caducitat i retenció |
| cap rol ni permís | rols, permisos, rol_permisos, usuari_rols |
| cap columna de tenant | org_id + filtre global |
El patró dels catàlegs
Tots els catàlegs de GNA repeteixen la mateixa figura de tres peces: una taula base, una
taula _idiomes amb ranures numerades, i una fila de configuració SIS_<nom>
que decideix quantes ranures hi ha. Es repeteix per a tipus d'activitat, horaris per defecte,
horaris a convenir, material, documentació, cursos, etapes, blocs i instruments.
Al model nou el patró és de dues peces i sempre el mateix: catàleg + traduccions.
El que era configuració d'estructura passa a ser configuració de negoci a configuracions, on
pertany.
erDiagram
SIS_material {
int titols_elements "decideix l estructura de la taula d idiomes"
int textos_elements
int categories
}
w_material {
int codi PK
int codi_seccio
int codi_categoria
numeric preu
int peu_minim
int peu_maxim
int actiu
int ordre
}
w_material_idiomes {
int codi_ref "sense clau forana"
text codi_idioma
text titol_1_fins_N
text text_1_fins_N
}
w_documentacio {
int codi PK "el mateix patró"
}
w_cursos {
int codi PK "el mateix patró"
}
w_etapes {
int codi PK "el mateix patró"
}
w_horaris_defecte {
int codi PK "el mateix patró"
}
w_instruments {
int codi PK "un catàleg sencer que la Fase 1A no modela"
}
SIS_material ||..o{ w_material_idiomes : "decideix quantes columnes"
w_material ||..o{ w_material_idiomes : "codi_ref, sense clau forana"
erDiagram
materials {
uuid id PK
text code
numeric price
int min_qty "era peu_minim"
int max_qty "era peu_maxim"
int ordre
bool active
}
material_traduccions {
uuid material_id FK
text locale
text name
}
documentacions {
uuid id PK
enum kind "requerida / lliurament"
}
documentacio_traduccions {
uuid documentacio_id FK
text locale
text name
text instructions
}
autoritzacions {
uuid id PK
}
autoritzacio_traduccions {
uuid autoritzacio_id FK
text locale
text text
}
motius_absencia {
uuid id PK
}
motiu_absencia_traduccions {
uuid motiu_absencia_id FK
text locale
text name
}
configuracions {
text key UK "la configuració és de negoci, no d estructura"
jsonb value
enum group "calendari / dates_inscripcio / descomptes / cataleg / textos_legals / idioma"
}
materials ||--o{ material_traduccions : "traduït a"
documentacions ||--o{ documentacio_traduccions : "traduïda a"
autoritzacions ||--o{ autoritzacio_traduccions : "traduïda a"
motius_absencia ||--o{ motiu_absencia_traduccions : "traduït a"
| A GNA | Al model nou |
|---|---|
w_material + _idiomes + SIS_material | materials + material_traduccions |
w_documentacio (una sola categoria) | documentacions.kind = requerida / lliurament |
w_cursos, w_etapes | cursos + etapes, amb next_curs_id per a la promoció |
w_tipus_activitats (fitat per codi_seccio) | tipus_activitats.seccio, encara fitat |
w_blocs | blocs, lligats a la temporada |
SIS_*.titols_elements | no existeix — cap configuració decideix una estructura |
w_instruments i tot el seu joc | decisió oberta — veure la secció Pendent |
| cap catàleg de consentiments | consentiments + consentiment_atorgaments |
Tenant, identitat i autorització
L'identificador d'accés és sempre un nom d'usuari de ClickEdu, un únic
mecanisme i no quatre. intern vol dir té compte de ClickEdu; extern
és registre local — és l'única definició operativa de la distinció, i és la que hereta GNA, on el verificador
local autentica els externs i ClickEdu els interns.
Els permisos efectius són la unió dels rols; amb dos àmbits diferents
guanya el més ampli; un permís no es resta mai — es treu el rol. El catàleg
de 19 permisos és tancat i contractual, i és global: no porta org_id.
erDiagram
orgs {
text name
text code UK
text timezone
text default_locale "ca"
bool active
}
usuaris {
uuid org_id FK
enum kind "intern / extern / treballador"
enum auth_source "clickedu / local"
text clickedu_username UK "identificador únic d accés"
text cognito_sub UK
text email UK "únic dins del centre"
text dni UK "amb lletra de control · duplicats només contra la nostra BD"
text given_name
text surname_1
text surname_2
enum status "actiu / pendent_validacio / suspes"
bool is_staff "treballador del centre"
text password_hash "mai autentica · Post-Auth Lambda, futura migració"
}
rols {
uuid org_id FK
text code UK "grup de Cognito"
text name
bool is_predefined "11 rols predefinits"
timestamptz suspended_at "suspensió temporal"
uuid cloned_from_rol_id FK
}
permisos {
text code UK "catàleg tancat de 19"
text name
enum level "baix / mitja / mitja_alt / alt / molt_alt"
}
rol_permisos {
uuid rol_id FK
uuid permis_id FK
enum scope "si / parcial / lectura / assignades / propies / del_dia / disponibles / crear / agregat"
}
usuari_rols {
uuid usuari_id FK
uuid rol_id FK
timestamptz assigned_at
uuid assigned_by_usuari_id FK
}
orgs ||--o{ usuaris : "conté"
orgs ||--o{ rols : "conté"
usuaris ||--o{ usuari_rols : "té assignats"
rols ||--o{ usuari_rols : "assignat a"
rols ||--o{ rol_permisos : "atorga"
permisos ||--o{ rol_permisos : "atorgat per"
rols }o--|| rols : "clonat de"
Famílies i persones inscribibles
Les confirmacions van als dos tutors excepte si ClickEdu marca custòdia
exclusiva. Una entitat inscribible sense curs_id vàlid no es pot inscriure,
per molt visible que sigui al selector: les activitats controlen disponibilitat, places i preus per curs.
alumne_salut és una taula separada a propòsit — retenció, xifrat
i consentiment propis. A GNA aquestes dades són a fills_registre i també
desnormalitzades sobre cada fila d'inscripcions; aquí viuen en un sol lloc i la inscripció
no en guarda còpia.
erDiagram
families {
uuid org_id FK
text clickedu_family_id UK
text label "per a la cerca administrativa"
}
familia_usuaris {
uuid familia_id FK
uuid usuari_id FK
enum relation "pare / mare / tutor"
bool is_primary
bool custody_exclusive "si no, la confirmació va als dos tutors"
}
alumnes {
uuid org_id FK
uuid familia_id FK
text given_name
text surname_1
text surname_2
date birth_date "filtre per edat i regles de curs"
text dni
uuid curs_id FK "sense curs vàlid no es pot inscriure"
text clickedu_student_id UK
enum origin "clickedu / local"
text health_card_number "catsalut · dada sensible"
bool active
}
alumne_salut {
uuid alumne_id PK "1 a 1 · retenció i consentiment propis"
text allergies
text obs_alimentacio
text obs_natacio
text malalties
bool share_with_monitors "consentiment explícit"
}
usuaris {
uuid id PK
enum kind "intern / extern / treballador"
}
cursos {
uuid id PK
text code
}
families ||--o{ familia_usuaris : "té titulars"
usuaris ||--o{ familia_usuaris : "pertany a"
families ||--o{ alumnes : "té fills"
alumnes ||--|| alumne_salut : "dades de salut"
cursos ||--o{ alumnes : "matriculats a"
Estructura acadèmica i calendari
El +1 de la promoció interna es declara, no es calcula: cada curs apunta
al següent amb next_curs_id, i l'últim d'una etapa apunta al primer de la següent o a
NULL. La promoció avança els alumnes només a la nostra BD — ClickEdu
segueix sent el font de veritat fins a la promoció oficial de setembre.
L'etapa Altres conté els cursos d'adults del catàleg (Externs · Antics alumnes i familiars · Altres). Les dates d'inscripció per defecte són per curs, i una activitat les pot sobreescriure.
erDiagram
temporades {
uuid org_id FK
text code UK "2026-2027"
date date_from
date date_to
bool is_current
date promotion_date "per defecte 1 de juny"
}
etapes {
uuid org_id FK
text code UK
int ordre
bool active
}
cursos {
uuid org_id FK
uuid etapa_id FK
text code UK
int ordre
uuid next_curs_id FK "el +1 de la promoció es declara"
timestamptz enrolment_opens_at "per defecte per curs"
timestamptz enrolment_closes_at
bool active
}
festius {
uuid temporada_id FK
date date
text name
}
etapa_traduccions {
uuid etapa_id FK
text locale "ca obligatori · es i en amb fallback"
text name
}
curs_traduccions {
uuid curs_id FK
text locale
text name
}
temporades ||--o{ festius : "té festius"
etapes ||--o{ cursos : "agrupa"
etapes ||--o{ etapa_traduccions : "traduïda a"
cursos ||--o{ curs_traduccions : "traduït a"
cursos }o--|| cursos : "promociona cap a"
Catàleg d'activitats
L'eix del catàleg és Extraescolars / Altres, no «Estiu»: a GNA
codi_seccio val 1 o 2 i el sufix _estiu és un nom de fitxer, mai una
etiqueta. Les categories estan fitades per secció, i el desplegable també ho ha d'estar.
L'audiència (interns / externs / tots) viu sobre
l'activitat, no sobre l'usuari — és així a GNA i es manté. A w_activitats els
materials i documents són cadenes d'identificadors separades per #: la migració
les ha de partir cap a les taules d'unió.
titol_2 de GNA és el peu de foto — aterra a
activitat_imatge_traduccions.caption, no a un segon títol de l'activitat. L'ordre de les
imatges al panell és l'ordre al portal.
erDiagram
activitats {
uuid org_id FK
uuid temporada_id FK
uuid tipus_activitat_id FK
enum seccio "extraescolars / altres"
enum audience "interns / externs / tots"
date date_from
date date_to
timestamptz enrolment_opens_at "NULL hereta del curs"
timestamptz enrolment_closes_at
enum state "esborrany / publicada / validada / finalitzada / arxivada"
bool published_admin_only "només alta manual"
bool competition
enum payment_card "obligatori / opcional / no_aplica · només externs"
int min_period_value "NULL hereta de la configuració global"
enum min_period_unit "cap / dia / setmana / mes / trimestre / curs"
bool places_shared "compartides entre cursos o per curs"
uuid duplicated_from_activitat_id FK
bool active
}
activitat_traduccions {
uuid activitat_id FK
text locale
text name "obligatori en ca"
text description
text conditions
}
tipus_activitats {
uuid org_id FK
enum seccio "categories fitades per secció"
text code UK
text responsible_name
int ordre
bool active
}
tipus_activitat_responsables {
uuid tipus_activitat_id FK
text email
}
activitat_cursos {
uuid activitat_id FK
uuid curs_id FK
}
activitat_imatges {
uuid activitat_id FK
text storage_key "S3"
int ordre "és l ordre del portal"
bool is_cover
}
activitat_imatge_traduccions {
uuid activitat_imatge_id FK
text locale
text caption "destí de titol_2 de GNA"
}
activitat_responsables {
uuid activitat_id FK
text email
enum kind "responsable / competicio"
}
materials {
uuid org_id FK
text code
numeric price
int min_qty "peu_minim"
int max_qty "peu_maxim"
bool active
}
activitat_materials {
uuid activitat_id FK
uuid material_id FK
bool required
}
autoritzacions {
uuid org_id FK
text code
bool active
}
activitat_autoritzacions {
uuid activitat_id FK
uuid autoritzacio_id FK
bool mandatory "obligatòria bloqueja la confirmació"
}
documentacions {
uuid org_id FK
text code
enum kind "requerida / lliurament"
bool active
}
activitat_documentacions {
uuid activitat_id FK
uuid documentacio_id FK
bool required
bool blocks_confirmation "els PDF no bloquegen per defecte"
}
activitat_adjunts {
uuid activitat_id FK
text storage_key
text name
bool prefill "s omple amb les dades de la inscripció"
}
temporades {
uuid id PK
text code
}
temporades ||--o{ activitats : "conté"
tipus_activitats ||--o{ activitats : "categoritza"
tipus_activitats ||--o{ tipus_activitat_responsables : "notifica a"
activitats ||--o{ activitat_traduccions : "traduïda a"
activitats ||--o{ activitat_cursos : "oberta a"
activitats ||--o{ activitat_imatges : "il·lustrada per"
activitat_imatges ||--o{ activitat_imatge_traduccions : "peu de foto"
activitats ||--o{ activitat_responsables : "notifica a"
activitats ||--o{ activitat_materials : "requereix"
materials ||--o{ activitat_materials : "usat per"
activitats ||--o{ activitat_autoritzacions : "demana"
autoritzacions ||--o{ activitat_autoritzacions : "demanada per"
activitats ||--o{ activitat_documentacions : "demana"
documentacions ||--o{ activitat_documentacions : "demanada per"
activitats ||--o{ activitat_adjunts : "adjunta"
activitats }o--|| activitats : "duplicada de"
Horaris, places i preus
Aquí és on GNA es desplega pitjor: w_activitats_horaris té
dia_1..7 × (hora inici, hora fi, cupo, places, places mínimes, preu) =
49 columnes en una sola taula filla, i la secció Altres ho fa amb
taules físicament diferents. Les dues formes es plegen aquí en una.
La capacitat s'emmagatzema; l'ocupació es calcula. No hi ha comptadors de places ocupades — es desincronitzen. Amb el pic previst de 300 inscripcions en 3 minuts, la comprovació de places ha de ser atòmica dins la transacció que crea la inscripció.
Dos comportaments del portal antic que es mantenen deliberadament: els preus només
es mostren amb la sessió iniciada, i la disponibilitat és un indicador derivat
(verd/vermell, recompte exacte només per sota de 5, +5 per sobre). A GNA els valors crus
viuen dins d'un popover comentat, és a dir, deliberadament ocults.
erDiagram
horaris {
uuid activitat_id FK
uuid curs_id FK "NULL val per a tots els cursos"
enum kind "fix / a_convenir"
int places
int places_minimes
int cupo
int ordre
bool active
}
horari_dies {
uuid horari_id FK
int weekday "1 a 7 · substitueix dia_1..7"
time time_from
time time_to
int places "capacitat · l ocupació es calcula"
int places_minimes
int cupo
numeric price
}
horari_combinacions {
uuid horari_id FK
text label
numeric price "preuc1..3 de GNA"
bool mandatory_days "dies obligatoris"
}
horari_combinacio_dies {
uuid horari_combinacio_id FK
int weekday
}
blocs {
uuid org_id FK
uuid temporada_id FK
text code
date date_from
date date_to
int ordre
}
horari_blocs {
uuid horari_id FK
uuid bloc_id FK
int places
int cupo
}
horari_bloc_franges {
uuid horari_bloc_id FK
int ordre "1 a 3 · normalitza hora_inici_1..3"
time time_from
time time_to
numeric price
}
preu_trams {
uuid activitat_id FK
uuid horari_id FK "NULL val per a tota l activitat"
enum target "base / treballador / fill_treballador / familiar_treballador / extern"
enum mode "fix / percentatge"
numeric value "sense tram definit s aplica el base"
}
activitats {
uuid id PK
enum seccio "extraescolars / altres"
}
cursos {
uuid id PK
text code
}
activitats ||--o{ horaris : "té"
cursos ||--o{ horaris : "restringeix"
horaris ||--o{ horari_dies : "es fa els dies"
horaris ||--o{ horari_combinacions : "ofereix combinacions"
horari_combinacions ||--o{ horari_combinacio_dies : "inclou"
horaris ||--o{ horari_blocs : "per període"
blocs ||--o{ horari_blocs : "aplicat a"
horari_blocs ||--o{ horari_bloc_franges : "amb franges"
activitats ||--o{ preu_trams : "tarifa"
horaris ||--o{ preu_trams : "tarifa pròpia"
Inscripcions
A GNA inscripcions són 54 columnes amb els dos progenitors i el fill
desnormalitzats sobre cada fila, i l'històric és un arxiu físic, no un registre
d'auditoria: «esborrar llistes» copia les files i les esborra de la taula viva. Aquí
l'històric no es mou de taula — el porta inscripcio_estats.
La baixa no esborra. A GNA és un DELETE dur sense cap
comprovació, i el camí d'estiu deixa horaris i inscripcions òrfens. Aquí és un canvi d'estat amb data
efectiva, i la plaça s'allibera pel càlcul d'ocupació.
El canvi d'activitat és atòmic — mai 0 ni 2 inscripcions, i mai dos càrrecs dins del mateix mes. Això és, tot sol, la raó per la qual no es pot reutilitzar l'esquema de GNA: MyISAM no té transaccions.
Les autoritzacions són caselles amb traçabilitat i sense PIN: IP més segell
de temps és el que substitueix el PIN. inscripcio_dies materialitza els dies escollits perquè
fan falta per validar solapaments i per tornar a dibuixar l'històric tal com el va veure la família.
erDiagram
inscripcions {
uuid org_id FK
uuid temporada_id FK
uuid alumne_id FK
uuid activitat_id FK
uuid horari_id FK
uuid horari_combinacio_id FK
uuid usuari_id FK "el compte titular, qui paga"
uuid created_by_usuari_id FK "pot ser administració"
bool es_administrativa "permet excedir restriccions amb avís"
text admin_reason "motiu registrat de l excepció"
enum state "preinscripcio / pendent_de_completar / confirmada / pendent_validacio_documents / baixa / cancellada"
numeric price_base
numeric discount_pct
text discount_reason "treballador / multi_fill / multi_activitat"
numeric total
timestamptz enrolled_at
date effective_from
date effective_to
timestamptz baixa_requested_at
date baixa_effective_at "segons el dia límit configurat"
text baixa_reason "el motiu que escriu la família"
uuid previous_inscripcio_id FK "cadena del canvi d activitat"
inet ip "traçabilitat de les autoritzacions"
text comments
}
inscripcio_dies {
uuid inscripcio_id FK
int weekday
time time_from
time time_to
}
inscripcio_blocs {
uuid inscripcio_id FK
uuid bloc_id FK "les setmanes escollides"
}
inscripcio_preferencies_horari {
uuid inscripcio_id FK
int ordre "1 a 3"
uuid horari_id FK
text note
bool assigned "la coordinació assigna"
}
inscripcio_materials {
uuid inscripcio_id FK
uuid material_id FK
int qty
numeric unit_price
}
inscripcio_autoritzacions {
uuid inscripcio_id FK
uuid autoritzacio_id FK
bool granted
timestamptz granted_at
uuid granted_by_usuari_id FK
inet ip "IP i segell de temps substitueixen el PIN"
}
inscripcio_documents {
uuid inscripcio_id FK
uuid documentacio_id FK
text storage_key "S3 xifrat, URL signada"
text file_hash
timestamptz uploaded_at
enum state "pendent / pujat / validat / rebutjat"
uuid reviewed_by_usuari_id FK
text review_note
}
inscripcio_estats {
uuid inscripcio_id FK
enum from_state
enum to_state
timestamptz at
uuid by_usuari_id FK "NULL és el sistema"
text reason
}
alumnes {
uuid id PK
date birth_date
}
activitats {
uuid id PK
enum state "esborrany / publicada / validada / finalitzada / arxivada"
}
horaris {
uuid id PK
enum kind "fix / a_convenir"
}
alumnes ||--o{ inscripcions : "inscrit a"
activitats ||--o{ inscripcions : "rep"
horaris ||--o{ inscripcions : "assignat a"
inscripcions ||--o{ inscripcio_dies : "els dies"
inscripcions ||--o{ inscripcio_blocs : "els períodes"
inscripcions ||--o{ inscripcio_preferencies_horari : "preferències a convenir"
inscripcions ||--o{ inscripcio_materials : "amb material"
inscripcions ||--o{ inscripcio_autoritzacions : "autoritzacions"
inscripcions ||--o{ inscripcio_documents : "documentació"
inscripcions ||--o{ inscripcio_estats : "historial d estats"
inscripcions }o--|| inscripcions : "substitueix"
Absències comunicades
Només la comunicació prèvia i el catàleg de motius. El marcatge d'assistència (passa-llista) és Fase 1B. Per defecte l'assistència queda com a justificada, i una absència comunicada inhibeix l'avís immediat a secretaria de primària. El temps mínim d'antelació, els cursos que disparen l'avís i els destinataris són configuració global.
erDiagram
motius_absencia {
uuid org_id FK
text code
int ordre
bool active
}
motiu_absencia_traduccions {
uuid motiu_absencia_id FK
text locale
text name
}
absencies {
uuid org_id FK
uuid alumne_id FK
uuid inscripcio_id FK
date date
uuid motiu_absencia_id FK
text note
timestamptz communicated_at
uuid communicated_by_usuari_id FK
}
alumnes {
uuid id PK
}
inscripcions {
uuid id PK
enum state
}
motius_absencia ||--o{ motiu_absencia_traduccions : "traduït a"
motius_absencia ||--o{ absencies : "motiu de"
alumnes ||--o{ absencies : "absent"
inscripcions ||--o{ absencies : "de la sessió"
Pagaments
Els interns no paguen a la plataforma. El seu cobrament el gestionen
secretaria i ClickEdu: per a ells el mètode és fora_de_plataforma i no hi ha proveïdor.
Stripe és només per a externs, amb Elements, 3D Secure quan cal, i subscripció quan
el cobrament és mensual.
Una cistella paga diverses inscripcions — d'aquí pagament_linies. Els
descomptes són configuració, no codi: treballador (50 % per defecte), multi-fill i multi-activitat.
Un canvi amb impacte econòmic genera registre d'auditoria.
erDiagram
pagaments {
uuid org_id FK
uuid usuari_id FK
enum method "targeta / transferencia / fora_de_plataforma"
enum provider "stripe o cap"
numeric amount
enum state "pendent / pagat / fallit / retornat"
text stripe_payment_intent_id
text stripe_subscription_id "recurrent mensual"
timestamptz paid_at
}
pagament_linies {
uuid pagament_id FK
uuid inscripcio_id FK
numeric amount
}
devolucions {
uuid pagament_id FK
numeric amount
text reason
text stripe_refund_id
timestamptz at
uuid by_usuari_id FK
}
descomptes {
uuid org_id FK
enum code "treballador / multi_fill / multi_activitat"
enum mode "fix / percentatge"
numeric value "treballador 50 per cent per defecte"
bool active
}
usuaris {
uuid id PK
enum kind "intern paga fora de la plataforma"
}
inscripcions {
uuid id PK
numeric total
}
usuaris ||--o{ pagaments : "paga"
pagaments ||--o{ pagament_linies : "cobreix"
inscripcions ||--o{ pagament_linies : "pagada per"
pagaments ||--o{ devolucions : "retorna"
Plantilles i enviaments
L'administració edita tots els cossos dels correus sense desenvolupament,
en tres idiomes, amb marcadors des d'un selector: {{nom_alumne}}, {{activitat}},
{{data}}, {{horari}}, {{text_activitat}} — l'últim viu a l'activitat
i se substitueix o s'elimina.
Amb el pic d'inscripcions la confirmació pot arribar amb ~30 s de retard: la cua
és part del disseny, i en_cua és un estat visible, no un error. L'administració
pot desactivar l'enviament automàtic temporalment per a proves.
erDiagram
plantilles_email {
uuid org_id FK
text code UK
enum event "confirmacio / baixa / recordatori / assignacio_horari / preinscripcio / document_pendent"
bool active
bool auto_send "desactivable per a proves"
}
plantilla_email_traduccions {
uuid plantilla_id FK
text locale
text subject
text body_html
}
enviaments {
uuid plantilla_id FK
text to_email
uuid inscripcio_id FK
uuid usuari_id FK
enum state "en_cua / enviat / fallit"
text provider_message_id "SES"
timestamptz sent_at
text error
}
inscripcions {
uuid id PK
}
plantilles_email ||--o{ plantilla_email_traduccions : "traduïda a"
plantilles_email ||--o{ enviaments : "genera"
inscripcions ||--o{ enviaments : "notificada per"
Configuració, consentiments i privacitat
Res codificat. Tot paràmetre operatiu s'edita en execució: dates del curs, dia de promoció, festius, dia límit d'alta i baixa, marge entre individual i grup amb unitat, període mínim amb unitat, descomptes, catàlegs editables, textos legals i idioma. La pantalla ha de mostrar un exemple calculat en viu: sense això la família no entén què vol dir el número.
Els consentiments són un catàleg, no una llista al codi, amb àmbit (registre, inscripció o ambdós) i obligatorietat. Els atorgaments guarden IP i segell de temps, i la revocació és el dret d'oposició. No hi ha pantalla d'RGPD a part: el registre d'auditoria és el sistema de registre.
erDiagram
configuracions {
uuid org_id FK
text key UK
jsonb value
enum group "calendari / dates_inscripcio / descomptes / cataleg / textos_legals / idioma"
uuid updated_by_usuari_id FK
}
consentiments {
uuid org_id FK
text code
enum ambit "registre / inscripcio / ambdos"
bool mandatory
int ordre
bool active
}
consentiment_traduccions {
uuid consentiment_id FK
text locale
text name
text text
}
consentiment_atorgaments {
uuid consentiment_id FK
uuid usuari_id FK
uuid alumne_id FK "p. ex. compartir al·lèrgies amb els monitors"
bool granted
timestamptz at
inet ip
timestamptz revoked_at "dret d oposició i limitació"
}
textos_legals {
uuid org_id FK
enum code "rgpd / condicions / cookies"
}
text_legal_traduccions {
uuid text_legal_id FK
text locale
text body_html
}
sollicituds_rgpd {
uuid usuari_id FK
enum kind "acces / rectificacio / oposicio / supressio"
enum state "rebuda / en_analisi / aprovada / denegada / executada"
timestamptz requested_at
timestamptz resolved_at "revisió manual, 30 dies com a màxim"
uuid resolved_by_usuari_id FK
text legal_retention_note "què es conserva per obligació legal"
text export_storage_key "JSON i PDF llegible en menys d un minut"
}
usuaris {
uuid id PK
}
consentiments ||--o{ consentiment_traduccions : "traduït a"
consentiments ||--o{ consentiment_atorgaments : "atorgat com"
usuaris ||--o{ consentiment_atorgaments : "atorga"
usuaris ||--o{ sollicituds_rgpd : "exerceix drets"
textos_legals ||--o{ text_legal_traduccions : "traduït a"
Auditoria, exportacions i processos
S'hi registra tota acció de nivell alt i molt alt. El registre és immutable, la retenció és de cinc anys com a mínim, i consultar-lo també queda registrat.
Tota exportació queda registrada. El llistat d'inscripcions s'exporta al preu unitari de la data d'exportació i no calcula sessions facturables ni aplica descomptes ni excepcions — això és Fase 1B.
erDiagram
audit_logs {
uuid org_id FK
uuid usuari_id FK
inet ip
text action
text entity_type
uuid entity_id
enum permission_level "tot alt i molt_alt"
jsonb before
jsonb after
timestamptz at "immutable · retenció 5 anys"
}
exportacions {
uuid org_id FK
uuid usuari_id FK
enum kind "inscripcions / usuaris / pagaments / audit"
jsonb filters
int row_count
text storage_key
timestamptz at
}
processos {
uuid org_id FK
text code UK "promocio_cursos / sincronitzacio_clickedu / recordatoris_documents / expiracio_activitats"
text schedule "editable"
bool enabled
timestamptz last_run_at
timestamptz next_run_at
}
execucions_proces {
uuid proces_id FK
timestamptz started_at
timestamptz finished_at
enum state "ok / parcial / fallida"
jsonb summary
text error
}
usuaris {
uuid id PK
}
usuaris ||--o{ audit_logs : "actua"
usuaris ||--o{ exportacions : "exporta"
processos ||--o{ execucions_proces : "s executa"
Sincronització amb ClickEdu
Només lectura. Res del que es guarda aquí és font de veritat i res s'hi escriu. Un compte de ClickEdu no es pot convertir en local, i les seves dades es visualitzen sense editar-se.
Una sessió costa 1 login + 1 credentials + 1 getInfo,
i després 5 crides per fill: l'inici de sessió d'una família de tres fills són 18 crides.
GNA ja ho cachejava (clickedu_info); aquí també, amb caducitat explícita i sabent que
el cau conté dades personals i té retenció pròpia. API caiguda → 3 reintents i avís;
dades incoherents → registre i avís, mai sobreescriure.
erDiagram
clickedu_sincronitzacions {
uuid org_id FK
enum trigger "login / manual"
timestamptz started_at
timestamptz finished_at
enum state "ok / parcial / fallida"
int students_seen
int changes_applied
jsonb errors "3 reintents i avís · mai sobreescriure"
}
clickedu_cache {
uuid usuari_id FK
jsonb payload "alumnes, cursos, etapes, famílies, contacte, al·lèrgies · cap credencial"
timestamptz fetched_at
timestamptz expires_at "conté dades personals · retenció pròpia"
}
usuaris {
uuid id PK
text clickedu_username UK
}
alumnes {
uuid id PK
text clickedu_student_id UK
enum origin "clickedu / local"
}
usuaris ||--o| clickedu_cache : "cau de sessió"
clickedu_sincronitzacions ||--o{ alumnes : "actualitza"
Capa d'aterratge i reconciliació
No es reutilitza l'esquema de GNA. L'objectiu és que la transició sigui
transparent per a l'usuari, no que les taules s'assemblin. Quatre bloquejos verificats contra el bolcat
real: MyISAM no té transaccions i el canvi d'activitat ha de ser atòmic · GNA
no té columna de tenant i OrgId amb filtre global és no-negociable ·
dia_1..7 × 7 grups = 49 columnes en una taula filla · el nombre de columnes
titol_1..N és una dada (viu a SIS_activitats.titols_elements),
i un esquema el nombre de columnes del qual depèn d'una fila no es pot mantenir.
El camí: importar GNA literalment a legacy_* → transformar cap al model
net → conservar legacy_font i legacy_codi per reconciliar. El que fa
demostrable que no s'ha perdut res no és que els esquemes coincideixin, és el
mapatge camp a camp.
Amb 0 claus foranes en 174 taules, les files òrfenes són pràcticament
segures: inscripcions que apunten a activitats esborrades, codi_fill sense fila,
codi_curs_bloc_horari sense horari. I 146 taules són utf8 però 28 són
latin1 — inscripcions inclosa: el text català és exactament on apareix
el mojibake, i cal detectar els valors ja doblement codificats abans de convertir.
erDiagram
legacy_inscripcions {
text codi "totes les columnes com a text"
text tot_lo_altre "sense FK, sense enums, sense obligatorietat"
}
legacy_w_activitats {
text codi
text tot_lo_altre
}
legacy_registre_estiu {
text codi
text tot_lo_altre "les credencials es descarten a la importació"
}
migracio_execucions {
text source_hash "identifica quin bolcat es va llegir"
timestamptz started_at
timestamptz finished_at
enum state "en_curs / ok / amb_incidencies / fallida"
jsonb counts "llegides, creades, omeses, deduplicades per taula"
}
migracio_files {
uuid execucio_id FK
text legacy_font UK "el codi de GNA només és únic dins la seva taula"
text legacy_codi UK
text target_table
uuid target_id
enum action "creada / actualitzada / omesa / fusionada"
}
migracio_incidencies {
uuid execucio_id FK
text legacy_font
text legacy_codi
enum severity "avis / error"
enum code "referencia_penjada / duplicat / codificacio / valor_invalid / credencial_descartada"
jsonb detail
}
inscripcions {
uuid id PK
text legacy_font
text legacy_codi
}
activitats {
uuid id PK
text legacy_font
text legacy_codi
}
usuaris {
uuid id PK
text legacy_font
text legacy_codi
}
migracio_execucions ||--o{ migracio_files : "registra"
migracio_execucions ||--o{ migracio_incidencies : "reporta"
legacy_inscripcions ||--o{ migracio_files : "transformada en"
legacy_w_activitats ||--o{ migracio_files : "transformada en"
legacy_registre_estiu ||--o{ migracio_files : "transformada en"
migracio_files ||--o| inscripcions : "apunta a"
migracio_files ||--o| activitats : "apunta a"
migracio_files ||--o| usuaris : "apunta a"
Set decisions que encara canvien taules
Cap d'aquestes es pot resoldre dibuixant. La primera dóna forma a mitja base de dades.
| Decisió | A qui toca | Què depèn |
|---|---|---|
| Fidelitat de l'històric | client + arquitectura | Una inscripció del 2019 s'ha de tornar a dibuixar exactament com la va veure la família, o només ha de ser llegible? Si ha de ser exacta, les columnes desnormalitzades de GNA (nom_pare, dni_pare, nom_fill, adreça) viatgen cap aquí i el model les arrossega per sempre. |
| Camps obligatoris en crear una activitat | client | GNA no en té cap: es pot desar una activitat sense nom, sense tipus i sense dates. Mínim proposat: nom en ca, tipus, secció i rang de dates. |
| Recompte real de files | escola | registre_estiu ~201.000 files i fills_registre ~112.000 per a una escola de ~1.438 alumnes. Decideix si s'importa tot, una part o res. |
| Retenció de dades de salut i documents d'identitat | client + legal | alumne_salut, alumnes.health_card_number, usuaris.dni. Cal decidir-ho abans d'importar. |
| Nomenclatura | Paras · Jaume | Taules en català amb columnes en anglès (el que ja fa Activitat), o unificar. Cal decidir-ho abans de la primera migració EF, no després. |
| Definició dels estats d'inscripció | client | Tres estats no estan definits enlloc a l'especificació: pendent_de_completar, confirmada i PREINSCRIPCIÓ. Qui els posa, què els fa avançar, i què veu la família. |
| Superfícies de GNA no cobertes | client | Avisos dins el portal, un catàleg sencer d'instruments musicals, variants de quota i llista d'espera, el registre d'accions, i la maquinària CMS de l'època Joomla. Fins a cinc dominis nous — el detall és a ENTITATS.md §14. |