Model Escola BetàniaPatmos · Model de dades Fase 1A ← Pantalles i prototips

Diagrames entitat-relació

Model de dades Fase 1A

La base de dades de GNA migra cap a aquesta, no al costat. Per això el model no es dissenya només contra els 25 casos d'ús: es dissenya contra les pantalles del portal antic, perquè la prova no és «cobreix la Fase 1A» sinó «podem tornar a mostrar el que ja tenien».

Diccionari d'entitats complet Abans i després de GNA Capa de migració Decisions obertes
Proposta de disseny, pendent de ratificació. Els diagrames mostren l'estructura i les relacions; el llistat complet de columnes, tipus i notes viu a ENTITATS.md. Cap columna comuna (id, org_id, created_at, updated_at) no es repeteix als diagrames. Set decisions encara canvien taules — la primera, la fidelitat de l'històric, dóna forma a mitja base de dades.

Cada diagrama té els seus controls: Ajusta a l'amplada per veure'l sencer, 100 % per llegir-lo, i + / − per als passos intermedis. A les comparacions, A dalt i a baix posa els dos esquemes un sota l'altre, que és on es llegeixen bé.

Com llegir els diagrames

SímbolSignificat
PK · FK · UKclau primària · clau forana · clau única (dins del centre)
||--o{un a molts, i el costat «molts» pot ser zero
||--||un a un
}o--||molts opcionals a un obligatori
enumvalors tancats, en català; el llistat és a la nota de la columna
legacy_font · legacy_codiprocedència a GNA. Hi són a totes les entitats migrades i no es dibuixen: el codi de GNA només és únic dins de la seva taula, per això calen les dues
Vista general

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
Abans i després

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.

AspecteGNA · abansModel nou · desprésPer 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…) usuarisfamiliesalumnes é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
Abans i després

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.

GNA · abansranures numerades, cap relació declarada
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"
Model noutraduccions com a files, relacions declarades
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 GNAAl model nou
titol_1activitat_traduccions.name
titol_2 (text peu foto)activitat_imatge_traduccions.caption
text_1activitat_traduccions.description
codi_seccio 1 / 2activitats.seccio = extraescolars / altres
activitat_per_interns_o_externs 0 / 1 / 2activitats.audience = tots / interns / externs
material, documents (cadenes)activitat_materials, activitat_documentacions
SIS_activitats.titols_elementsno existeix — el nombre de camps ja no és una dada
cursos (columna òrfena)no es migra
Abans i després

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.

GNA · abans49 columnes, i taules bessones per secció
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"
Model nouuna fila per dia, una sola forma
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 GNAAl model nou
dia_N, hora_inici_N, hora_fi_N, cupo_N, places_N, places_minimes_N, preu_Nuna fila d'horari_dies per dia
preuc1..3 + w_activitats_horaris_combinacionshorari_combinacions + horari_combinacio_dies
w_activitats_horaris_estiu_nou i la seva _blocs_preusles mateixes horaris + horari_blocs + horari_bloc_franges
hora_inici_1..3 / preu_1..3 dels blocshorari_bloc_franges, ordre 1 a 3
horari_convenir + w_horaris_convenir_taulahoraris.kind = a_convenir + inscripcio_preferencies_horari
preus per tipus d'usuari repartits per la fitxapreu_trams, amb caiguda al tram base
w_activitats_horaris_combiancions (DELETE mal escrit, codi mort)no es migra — mai va esborrar res
Abans i després

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.

GNA · abans54 columnes, quatre taules, arxiu físic
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"
Model nouuna taula viva, l'estat com a historial
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 GNAAl model nou
4 taules (inscripcions, _historic, _estiu, _estiu_historic)1 taula inscripcions + inscripcio_estats
eliminada + DELETE durstate = baixa + baixa_effective_at
dni_pare, nom_pare, email_pare, tot el joc _mareusuari_idusuaris, i familia_usuaris.relation
nom_fill, cognom1_fill, cognom2_fill, codi_fillalumne_idalumnes
alergies, obs_alimentacio, obs_natacio, catsalutalumne_salut + alumnes.health_card_number
dies, hores, cursos (cadenes)inscripcio_dies, una fila per dia
material (cadena d'ids)inscripcio_materials
horari_convenir1..3inscripcio_preferencies_horari.ordre
temporada (text calculat al PHP)temporada_idtemporades
user, passes descarten — cap credencial es migra
inscripcions_cupos, accio_inscripcionsdecisió oberta — veure la secció Pendent
Abans i després

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.

GNA · abanstres verificadors, un d'ells mort · contrasenyes en clar
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"
Model nouun compte, Cognito, cap contrasenya nostra
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 GNAAl model nou
registre_estiu (extern) + ClickEdu (intern)usuaris.kind + usuaris.auth_source
registre_antics_alumnesno 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.passwordes descarten a la importació
fills_registre.codi_parealumnes.familia_idfamilies
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ísrols, permisos, rol_permisos, usuari_rols
cap columna de tenantorg_id + filtre global
Abans i després

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.

GNA · abanstres peces, repetides nou vegades
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"
Model noudues peces, un sol patró
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 GNAAl model nou
w_material + _idiomes + SIS_materialmaterials + material_traduccions
w_documentacio (una sola categoria)documentacions.kind = requerida / lliurament
w_cursos, w_etapescursos + etapes, amb next_curs_id per a la promoció
w_tipus_activitats (fitat per codi_seccio)tipus_activitats.seccio, encara fitat
w_blocsblocs, lligats a la temporada
SIS_*.titols_elementsno existeix — cap configuració decideix una estructura
w_instruments i tot el seu jocdecisió oberta — veure la secció Pendent
cap catàleg de consentimentsconsentiments + consentiment_atorgaments
Identitat

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.

orgsusuarisrolspermisosrol_permisosusuari_rols
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"
Persones

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.

familiesfamilia_usuarisalumnesalumne_salut
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"
Calendari

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.

temporadesetapescursosfestius*_traduccions
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

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.

activitatsactivitat_traduccionsactivitat_cursosactivitat_imatgesactivitat_responsablestipus_activitatsmaterialsautoritzacionsdocumentacionsactivitat_adjunts
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

Horaris, places i preus

Aquí és on GNA es desplega pitjor: w_activitats_horarisdia_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.

horarishorari_dieshorari_combinacionshorari_combinacio_diesblocshorari_blocshorari_bloc_frangespreu_trams
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

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.

inscripcionsinscripcio_diesinscripcio_blocsinscripcio_preferencies_horariinscripcio_materialsinscripcio_autoritzacionsinscripcio_documentsinscripcio_estats
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

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.

motius_absenciaabsencies
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

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.

pagamentspagament_liniesdevolucionsdescomptes
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"
Comunicacions

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.

plantilles_emailplantilla_email_traduccionsenviaments
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ó

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.

configuracionsconsentimentsconsentiment_atorgamentstextos_legalssollicituds_rgpd
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

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.

audit_logsexportacionsprocessosexecucions_proces
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"
ClickEdu

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.

clickedu_sincronitzacionsclickedu_cache
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"
Migració

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 latin1inscripcions inclosa: el text català és exactament on apareix el mojibake, i cal detectar els valors ja doblement codificats abans de convertir.

legacy_*migracio_execucionsmigracio_filesmigracio_incidencies
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"
Pendent

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 tocaQuè 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.