@tekkare/romulus 0.1.253 → 0.1.255

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Libelles des vocabulaires de l'index des cas apres vaccination.
3
+ *
4
+ * Les listes fermees viennent du pipeline (ai-pipelines, backend/app/vaccine.py) :
5
+ * gravite, evolution, avis des auteurs, dose suivante, type de cas, delai. Les
6
+ * cles de produit et d'agent sont derivees du nom ecrit dans l'article ; une
7
+ * cle que ce module ne connait pas s'affiche mise en forme, jamais vide.
8
+ */
9
+ import type { PatientLang } from "./labels";
10
+
11
+ type Labels = Record<string, { fr: string; en: string }>;
12
+
13
+ /** `sars_cov_2` → « Sars cov 2 » : le dernier recours, pour une cle inconnue. */
14
+ function prettify(value: string): string {
15
+ return value.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
16
+ }
17
+
18
+ /** Le libelle d'une valeur, ou la valeur mise en forme quand le vocabulaire ne la connait pas. */
19
+ export function vaccineLabelOf(labels: Labels, value: string | number | null | undefined, lang: PatientLang): string {
20
+ if (value === null || value === undefined || value === "") return "";
21
+ const key = String(value);
22
+ return labels[key]?.[lang] ?? prettify(key);
23
+ }
24
+
25
+ export const SERIOUSNESS_LABELS: Labels = {
26
+ death: { fr: "Décès", en: "Died" },
27
+ life_threatening: { fr: "Pronostic vital engagé", en: "Life-threatening" },
28
+ hospitalisation: { fr: "Hospitalisation", en: "Admitted to hospital" },
29
+ disability: { fr: "Séquelle invalidante", en: "Lasting disability" },
30
+ medically_important: { fr: "Soins médicaux nécessaires", en: "Needed medical care" },
31
+ not_serious: { fr: "Non grave", en: "Not serious" },
32
+ };
33
+
34
+ /** Les gravites qui font un cas « grave » au sens de la pharmacovigilance. */
35
+ export const SERIOUS_KEYS = ["death", "life_threatening", "hospitalisation", "disability"];
36
+
37
+ export const EVENT_OUTCOME_LABELS: Labels = {
38
+ recovered: { fr: "Guéri", en: "Recovered" },
39
+ recovering: { fr: "En voie de guérison", en: "Recovering" },
40
+ sequelae: { fr: "Guéri avec séquelles", en: "Recovered with after-effects" },
41
+ not_recovered: { fr: "Non guéri", en: "Not recovered" },
42
+ fatal: { fr: "Décès", en: "Died" },
43
+ unknown: { fr: "Non précisé", en: "Not reported" },
44
+ };
45
+
46
+ export const CAUSALITY_LABELS: Labels = {
47
+ certain: { fr: "Lien certain", en: "Certain link" },
48
+ probable: { fr: "Lien probable", en: "Probable link" },
49
+ possible: { fr: "Lien possible", en: "Possible link" },
50
+ unlikely: { fr: "Lien peu probable", en: "Unlikely link" },
51
+ temporal_only: { fr: "Chronologie seule, sans conclusion", en: "Timing only, no causal claim" },
52
+ not_assessed: { fr: "Non discuté", en: "Not discussed" },
53
+ };
54
+
55
+ export const RECHALLENGE_LABELS: Labels = {
56
+ re_vaccinated_no_event: { fr: "Dose suivante reçue, sans récidive", en: "Next dose given, no problem" },
57
+ re_vaccinated_recurrence: { fr: "Dose suivante reçue, récidive", en: "Next dose given, it happened again" },
58
+ not_re_vaccinated: { fr: "Pas de dose suivante", en: "No further dose" },
59
+ advised_against: { fr: "Doses suivantes déconseillées", en: "Further doses advised against" },
60
+ unknown: { fr: "Non précisé", en: "Not reported" },
61
+ };
62
+
63
+ export const EVENT_CATEGORY_LABELS: Labels = {
64
+ adverse_event: { fr: "Événement après vaccination", en: "Event after vaccination" },
65
+ breakthrough_infection: { fr: "Infection malgré la vaccination", en: "Infection despite vaccination" },
66
+ vaccine_failure: { fr: "Échec de protection", en: "Vaccine did not protect" },
67
+ vaccination_in_special_population: { fr: "Vaccination en situation particulière", en: "Vaccination in a special situation" },
68
+ not_vaccine_related: { fr: "Sans lien avec un vaccin", en: "Not about a vaccine" },
69
+ };
70
+
71
+ /** Dans l'ordre du temps : la facette les range ainsi, pas par effectif. */
72
+ export const ONSET_BAND_LABELS: Labels = {
73
+ same_day: { fr: "Le jour même", en: "Same day" },
74
+ within_week: { fr: "Dans la semaine", en: "Within a week" },
75
+ within_month: { fr: "1 à 4 semaines", en: "1 to 4 weeks" },
76
+ within_3_months: { fr: "1 à 3 mois", en: "1 to 3 months" },
77
+ later: { fr: "Plus de 3 mois", en: "More than 3 months" },
78
+ unknown: { fr: "Non précisé", en: "Not stated" },
79
+ };
80
+ export const ONSET_BAND_ORDER = Object.keys(ONSET_BAND_LABELS);
81
+
82
+ export const PLATFORM_LABELS: Labels = {
83
+ mrna: { fr: "ARN messager", en: "mRNA" },
84
+ "viral vector": { fr: "Vecteur viral", en: "Viral vector" },
85
+ inactivated: { fr: "Inactivé", en: "Inactivated" },
86
+ "live attenuated": { fr: "Vivant atténué", en: "Live attenuated" },
87
+ subunit: { fr: "Sous-unitaire", en: "Subunit" },
88
+ toxoid: { fr: "Anatoxine", en: "Toxoid" },
89
+ conjugate: { fr: "Conjugué", en: "Conjugate" },
90
+ polysaccharide: { fr: "Polysaccharidique", en: "Polysaccharide" },
91
+ };
92
+
93
+ export const PATHOGEN_LABELS: Labels = {
94
+ sars_cov_2: { fr: "Covid-19 (SARS-CoV-2)", en: "Covid-19 (SARS-CoV-2)" },
95
+ varicella_zoster: { fr: "Varicelle, zona", en: "Chickenpox, shingles" },
96
+ hpv: { fr: "Papillomavirus (HPV)", en: "Papillomavirus (HPV)" },
97
+ tuberculosis: { fr: "Tuberculose", en: "Tuberculosis" },
98
+ measles: { fr: "Rougeole", en: "Measles" },
99
+ yellow_fever: { fr: "Fièvre jaune", en: "Yellow fever" },
100
+ rabies: { fr: "Rage", en: "Rabies" },
101
+ influenza: { fr: "Grippe", en: "Influenza" },
102
+ pneumococcus: { fr: "Pneumocoque", en: "Pneumococcus" },
103
+ meningococcus: { fr: "Méningocoque", en: "Meningococcus" },
104
+ hepatitis_b: { fr: "Hépatite B", en: "Hepatitis B" },
105
+ hepatitis_a: { fr: "Hépatite A", en: "Hepatitis A" },
106
+ tetanus: { fr: "Tétanos, diphtérie, coqueluche", en: "Tetanus, diphtheria, pertussis" },
107
+ rotavirus: { fr: "Rotavirus", en: "Rotavirus" },
108
+ rsv: { fr: "Virus respiratoire syncytial (VRS)", en: "Respiratory syncytial virus (RSV)" },
109
+ dengue: { fr: "Dengue", en: "Dengue" },
110
+ japanese_encephalitis: { fr: "Encéphalite japonaise", en: "Japanese encephalitis" },
111
+ typhoid: { fr: "Typhoïde", en: "Typhoid" },
112
+ polio: { fr: "Poliomyélite", en: "Polio" },
113
+ mpox: { fr: "Mpox, variole", en: "Mpox, smallpox" },
114
+ ebola: { fr: "Ebola", en: "Ebola" },
115
+ tick_borne_encephalitis: { fr: "Encéphalite à tiques", en: "Tick-borne encephalitis" },
116
+ rubella: { fr: "Rubéole", en: "Rubella" },
117
+ mumps: { fr: "Oreillons", en: "Mumps" },
118
+ cholera: { fr: "Choléra", en: "Cholera" },
119
+ };
120
+
121
+ export const PRODUCT_LABELS: Labels = {
122
+ comirnaty: { fr: "Comirnaty (Pfizer-BioNTech)", en: "Comirnaty (Pfizer-BioNTech)" },
123
+ spikevax: { fr: "Spikevax (Moderna)", en: "Spikevax (Moderna)" },
124
+ vaxzevria: { fr: "Vaxzevria (AstraZeneca)", en: "Vaxzevria (AstraZeneca)" },
125
+ jcovden: { fr: "Jcovden (Janssen)", en: "Jcovden (Janssen)" },
126
+ nuvaxovid: { fr: "Nuvaxovid (Novavax)", en: "Nuvaxovid (Novavax)" },
127
+ coronavac: { fr: "CoronaVac (Sinovac)", en: "CoronaVac (Sinovac)" },
128
+ bbibp_corv: { fr: "BBIBP-CorV (Sinopharm)", en: "BBIBP-CorV (Sinopharm)" },
129
+ covaxin: { fr: "Covaxin", en: "Covaxin" },
130
+ convidecia: { fr: "Convidecia (CanSino)", en: "Convidecia (CanSino)" },
131
+ sputnik_v: { fr: "Spoutnik V", en: "Sputnik V" },
132
+ other_covid: { fr: "Autre vaccin Covid-19", en: "Other Covid-19 vaccine" },
133
+ unspecified_covid: { fr: "Vaccin Covid-19 non précisé", en: "Unspecified Covid-19 vaccine" },
134
+ shingrix: { fr: "Shingrix", en: "Shingrix" },
135
+ zostavax: { fr: "Zostavax", en: "Zostavax" },
136
+ varicella: { fr: "Varicelle", en: "Varicella" },
137
+ hpv: { fr: "HPV", en: "HPV" },
138
+ bcg: { fr: "BCG", en: "BCG" },
139
+ mmr: { fr: "ROR", en: "MMR" },
140
+ measles: { fr: "Rougeole", en: "Measles" },
141
+ yellow_fever: { fr: "Fièvre jaune", en: "Yellow fever" },
142
+ rabies: { fr: "Rage", en: "Rabies" },
143
+ influenza: { fr: "Grippe", en: "Influenza" },
144
+ pneumococcal: { fr: "Pneumocoque", en: "Pneumococcal" },
145
+ meningococcal: { fr: "Méningocoque", en: "Meningococcal" },
146
+ hepatitis_b: { fr: "Hépatite B", en: "Hepatitis B" },
147
+ hepatitis_a: { fr: "Hépatite A", en: "Hepatitis A" },
148
+ dtp: { fr: "Diphtérie, tétanos, coqueluche", en: "Diphtheria, tetanus, pertussis" },
149
+ rotavirus: { fr: "Rotavirus", en: "Rotavirus" },
150
+ rsv: { fr: "VRS", en: "RSV" },
151
+ dengue: { fr: "Dengue", en: "Dengue" },
152
+ japanese_encephalitis: { fr: "Encéphalite japonaise", en: "Japanese encephalitis" },
153
+ typhoid: { fr: "Typhoïde", en: "Typhoid" },
154
+ polio: { fr: "Poliomyélite", en: "Polio" },
155
+ mpox_smallpox: { fr: "Mpox, variole", en: "Mpox, smallpox" },
156
+ ebola: { fr: "Ebola", en: "Ebola" },
157
+ tbe: { fr: "Encéphalite à tiques", en: "Tick-borne encephalitis" },
158
+ rubella: { fr: "Rubéole", en: "Rubella" },
159
+ mumps: { fr: "Oreillons", en: "Mumps" },
160
+ cholera: { fr: "Choléra", en: "Cholera" },
161
+ };
162
+
163
+ /** Ce qui retient une fiche d'etre comptee sans relecture, en clair. */
164
+ export const VACCINE_FLAG_LABELS: Labels = {
165
+ exposure_missing: { fr: "L'article ne nomme aucun vaccin pour cette personne", en: "The paper names no vaccine for this person" },
166
+ exposure_unsupported: { fr: "Le vaccin indiqué ne figure pas dans le texte de l'article", en: "The vaccine named was not found in the paper's own text" },
167
+ event_missing: { fr: "Aucun événement clair n'a été trouvé", en: "No clear event was found" },
168
+ onset_before_vaccination: { fr: "Les troubles ont commencé avant la dose", en: "The problem started before the dose" },
169
+ not_vaccine_related: { fr: "Le cas ne concerne pas un vaccin", en: "The report is not about a vaccine" },
170
+ block_failed: { fr: "Les informations sur le vaccin n'ont pas pu être lues", en: "The vaccine details could not be read" },
171
+ delay_not_from_dose: { fr: "Le délai est compté depuis un traitement ou un événement, pas depuis la dose", en: "The delay is counted from a treatment or event, not from the dose" },
172
+ delay_implausible: { fr: "Le délai dépasse dix ans : probablement un âge plutôt qu'un délai", en: "The delay is over ten years, more likely an age than a delay" },
173
+ reported_elsewhere: { fr: "Cas déjà publié ailleurs : la personne peut être comptée deux fois", en: "Case published elsewhere, so the person may be counted twice" },
174
+ };
175
+
176
+ export const CHECK_LABELS: Labels = {
177
+ all: { fr: "Toutes les fiches", en: "All records" },
178
+ clean: { fr: "Sans contrôle ouvert", en: "No open check" },
179
+ flagged: { fr: "À vérifier", en: "Needing a check" },
180
+ };
@@ -16,6 +16,8 @@
16
16
  import type { RmProjectKind, RmProjectLang } from "@tekkare/romulus/utils/projects";
17
17
  import scienceIndexConfig from "../oip-science/indexConfig.json";
18
18
  import { DRUG_PROJECT_KINDS } from "../oip-drugs/kinds";
19
+ import { OIP_PATIENT_INDEXES } from "../oip-patients/indexes";
20
+ import { PATIENT_KIND, VACCINE_KIND } from "../oip-patients/kinds";
19
21
 
20
22
  /** Les apps qui ont des fiches a montrer, et ou les ouvrir. */
21
23
  export const OIP_RESOURCE_APPS = {
@@ -27,6 +29,10 @@ export const OIP_RESOURCE_APPS = {
27
29
  name: "France Drugs",
28
30
  url: "https://france-drugs.app.tekkare.com",
29
31
  },
32
+ ww_oip_patients: {
33
+ name: "OIP Case Atlas",
34
+ url: "https://global-case-atlas.app.tekkare.com",
35
+ },
30
36
  } as const;
31
37
 
32
38
  export type OipResourceApp = keyof typeof OIP_RESOURCE_APPS;
@@ -121,6 +127,28 @@ function drugsSpec(kind: RmProjectKind, view: string): OipResourceSpec {
121
127
  };
122
128
  }
123
129
 
130
+ /**
131
+ * Une fiche d'OIP Case Atlas (cas de maladie rare, cas apres vaccination),
132
+ * lue sur le cluster `science` et montree d'un seul tenant, ses onglets
133
+ * compris. Elle s'adresse par son `record_id`, que le document donne.
134
+ */
135
+ function patientsSpec(kind: RmProjectKind, view: string): OipResourceSpec {
136
+ return {
137
+ type: kind.type,
138
+ app: "ww_oip_patients",
139
+ cluster: "science",
140
+ kind,
141
+ routeId: (id) => id,
142
+ base: () => "",
143
+ tabs: [],
144
+ view,
145
+ detailPath: (id, tab, doc) => {
146
+ const path = kind.path!(doc ?? {}, id);
147
+ return tab && tab !== "summary" ? `${path}?tab=${encodeURIComponent(tab)}` : path;
148
+ },
149
+ };
150
+ }
151
+
124
152
  /** La vue de chaque famille de fr_drugs_app (cf. useOipResourceViews). */
125
153
  const DRUG_VIEWS: Record<string, string> = {
126
154
  FrHasBrand: "drugs-brand",
@@ -338,6 +366,8 @@ export const OIP_RESOURCE_SPECS: OipResourceSpec[] = [
338
366
  tabs: [T.trials, T.publications],
339
367
  }),
340
368
  ...DRUG_PROJECT_KINDS.map((kind) => drugsSpec(kind, DRUG_VIEWS[kind.type]!)),
369
+ patientsSpec(PATIENT_KIND, "patients-patient"),
370
+ patientsSpec(VACCINE_KIND, "patients-vaccine"),
341
371
  ];
342
372
 
343
373
  export function oipResourceSpec(type: string): OipResourceSpec | undefined {
@@ -370,7 +400,9 @@ export const OIP_SCIENCE_INDEXES: string[] = Object.values(scienceIndexConfig as
370
400
  /** Les index que le proxy du layer accepte de lire, par cluster. */
371
401
  export function oipResourceIndexes(cluster: OipEsCluster): string[] {
372
402
  const declared = OIP_RESOURCE_SPECS.filter((spec) => spec.cluster === cluster).map((spec) => spec.kind.index);
373
- return cluster === "science" ? [...new Set([...declared, ...OIP_SCIENCE_INDEXES])] : declared;
403
+ return cluster === "science"
404
+ ? [...new Set([...declared, ...OIP_SCIENCE_INDEXES, ...Object.values(OIP_PATIENT_INDEXES)])]
405
+ : declared;
374
406
  }
375
407
 
376
408
  /**
@@ -22,12 +22,14 @@ export default defineNuxtConfig({
22
22
  compatibilityVersion: 4,
23
23
  },
24
24
  // Regles du cadre communes aux apps (bandeau de page, barre de filtres), et
25
- // classes des fiches de France Augmented Science, limitees a `.oip-science`
26
- // (cf. app/oip-science/science.css).
25
+ // classes des fiches de France Augmented Science, de fr_drugs_app et d'OIP
26
+ // Case Atlas, limitees a leur conteneur (`.oip-science`, `.oip-drugs`,
27
+ // `.oip-patients`).
27
28
  css: [
28
29
  fileURLToPath(new URL("./app/assets/oip-frame.css", import.meta.url)),
29
30
  fileURLToPath(new URL("./app/oip-science/science.css", import.meta.url)),
30
31
  fileURLToPath(new URL("./app/oip-drugs/drugs.css", import.meta.url)),
32
+ fileURLToPath(new URL("./app/oip-patients/patients.css", import.meta.url)),
31
33
  ],
32
34
  // Reglages serveur propres au layer. Les cles Directus (directus*Url,
33
35
  // directus*Token) restent declarees par l'app, qui les avait deja.
@@ -57,12 +59,14 @@ export default defineNuxtConfig({
57
59
  /**
58
60
  * Clusters Elasticsearch des vues de ressources (/api/oip/es) : domaine et
59
61
  * cle ApiKey. `science` porte les index de France Augmented Science
60
- * (PubMed, essais), `hcp` l'annuaire, `drugs` ceux de fr_drugs_app. Une
62
+ * (PubMed, essais) et ceux d'OIP Case Atlas (patients), `hcp` l'annuaire, `drugs` ceux de fr_drugs_app. Une
61
63
  * app les remplit depuis ses propres variables, ou par
62
64
  * NUXT_OIP_ES_SCIENCE_DOMAIN / NUXT_OIP_ES_SCIENCE_AUTH, etc.
63
65
  */
64
66
  oipEs: {
65
- science: { domain: "", auth: "" },
67
+ // Cas apres vaccination d'OIP Case Atlas : une autre cle sur le meme
68
+ // domaine. Vide : la cle principale.
69
+ science: { domain: "", auth: "", authVaccines: "" },
66
70
  hcp: { domain: "", auth: "" },
67
71
  // Associations et registres d'autorisation etrangers : une autre cle sur
68
72
  // le meme domaine, comme dans fr_drugs_app. Vides : la cle principale.
@@ -3,6 +3,7 @@ import { requireOipSession } from "../../utils/oipAuth";
3
3
  import { findOipEsIndex, resolveOipEsIndex } from "../../utils/oipEsIndices";
4
4
  import { oipResourceIndexes, type OipEsCluster } from "../../../app/utils/oipResources";
5
5
  import { ES_INDEX } from "../../../app/oip-drugs/esIndices";
6
+ import { OIP_PATIENT_INDEXES } from "../../../app/oip-patients/indexes";
6
7
 
7
8
  /**
8
9
  * POST /api/oip/es?cluster=&index=, servi par le layer OIP.
@@ -18,9 +19,13 @@ import { ES_INDEX } from "../../../app/oip-drugs/esIndices";
18
19
  * dans fr_drugs_app. Ses associations et ses registres d'autorisation
19
20
  * etrangers demandent une autre cle sur le meme domaine (`authAssociations`,
20
21
  * `authApprovals`) ; a defaut, la cle principale.
22
+ *
23
+ * Cluster `science` : les cas apres vaccination d'OIP Case Atlas ne se lisent
24
+ * pas avec la cle PubMed, mais avec une autre sur le meme domaine
25
+ * (`authVaccines`) ; a defaut, la cle principale.
21
26
  */
22
27
 
23
- type DrugsKeyGroup = "auth" | "authAssociations" | "authApprovals";
28
+ type KeyGroup = "auth" | "authAssociations" | "authApprovals" | "authVaccines";
24
29
 
25
30
  const DRUG_IDS = new Set<string>(Object.values(ES_INDEX) as string[]);
26
31
  const ASSOCIATION_IDS = new Set<string>(
@@ -45,7 +50,7 @@ async function drugsKeyOf(ref: string): Promise<string> {
45
50
  return id && DRUG_IDS.has(id) ? id : "";
46
51
  }
47
52
 
48
- function drugsKeyGroup(id: string): DrugsKeyGroup {
53
+ function drugsKeyGroup(id: string): KeyGroup {
49
54
  if (ASSOCIATION_IDS.has(id)) return "authAssociations";
50
55
  if (APPROVAL_IDS.has(id)) return "authApprovals";
51
56
  return "auth";
@@ -71,7 +76,7 @@ export default defineEventHandler(async (event) => {
71
76
  if (!index || !/^[A-Za-z0-9_.,-]+$/.test(index)) throw createError({ status: 400, message: "Invalid index" });
72
77
 
73
78
  const refs = index.split(",");
74
- let authKey: DrugsKeyGroup = "auth";
79
+ let authKey: KeyGroup = "auth";
75
80
  if (cluster === "drugs") {
76
81
  // Une requete multi-index part avec une seule cle : ses index en relevent tous.
77
82
  const ids = await Promise.all(refs.map(drugsKeyOf));
@@ -82,6 +87,11 @@ export default defineEventHandler(async (event) => {
82
87
  } else {
83
88
  const allowed = oipResourceIndexes(cluster as OipEsCluster);
84
89
  if (refs.some((ref) => !allowed.includes(ref))) throw createError({ status: 403, message: "Index not allowed" });
90
+ if (cluster === "science") {
91
+ const groups = new Set(refs.map((ref) => (ref === OIP_PATIENT_INDEXES.VACCINE_PATIENTS ? "authVaccines" : "auth")));
92
+ if (groups.size > 1) throw createError({ status: 400, message: "Indexes require different credentials" });
93
+ authKey = [...groups][0]!;
94
+ }
85
95
  }
86
96
 
87
97
  const auth = target[authKey] || target.auth;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.253",
3
+ "version": "0.1.255",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "./layer": "./layer/nuxt.config.ts",
21
21
  "./layer-utils/*": "./layer/app/utils/*.ts",
22
- "./layer-composables/*": "./layer/app/composables/*.ts"
22
+ "./layer-composables/*": "./layer/app/composables/*.ts",
23
+ "./layer-patients/*": "./layer/app/oip-patients/*.ts"
23
24
  },
24
25
  "main": "./dist/module.cjs",
25
26
  "module": "./dist/module.mjs",
@@ -48,6 +49,8 @@
48
49
  "@tiptap/starter-kit": "^3.31.4",
49
50
  "@tiptap/suggestion": "^3.31.4",
50
51
  "@tiptap/vue-3": "^3.31.4",
52
+ "cytoscape": "^3.33.1",
53
+ "cytoscape-fcose": "^2.2.0",
51
54
  "echarts": "^5.6.0",
52
55
  "html2canvas-pro": "^1.6.7",
53
56
  "jspdf": "^4.2.0",