@tekkare/romulus 0.1.254 → 0.1.256
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.
- package/dist/module.json +1 -1
- package/dist/runtime/assets/styles/shell.css +1 -1
- package/dist/runtime/components/SidebarLeft.vue +110 -2
- package/layer/app/components/OipShell.vue +10 -3
- package/layer/app/components/oip-patients/ConnectionsGraph.vue +354 -0
- package/layer/app/components/oip-patients/PatientFiche.vue +732 -0
- package/layer/app/components/oip-patients/Publications.vue +298 -0
- package/layer/app/components/oip-patients/PubmedDrawer.vue +379 -0
- package/layer/app/components/oip-patients/VaccineFiche.vue +766 -0
- package/layer/app/composables/useOipResourceViews.ts +4 -0
- package/layer/app/oip-patients/cytoscape-fcose.d.ts +2 -0
- package/layer/app/oip-patients/indexes.ts +15 -0
- package/layer/app/oip-patients/kinds.ts +47 -0
- package/layer/app/oip-patients/labels.ts +142 -0
- package/layer/app/oip-patients/patients.css +54 -0
- package/layer/app/oip-patients/types.ts +149 -0
- package/layer/app/oip-patients/useCaseRecord.ts +78 -0
- package/layer/app/oip-patients/vaccineLabels.ts +180 -0
- package/layer/app/oip-science/composables/useEuct.ts +1 -1
- package/layer/app/oip-science/composables/useOipElastic.ts +5 -2
- package/layer/app/oip-science/composables/usePubmed.ts +2 -2
- package/layer/app/oip-science/indexConfig.json +32 -26
- package/layer/app/types/oip.ts +2 -0
- package/layer/app/utils/oipResources.ts +34 -2
- package/layer/nuxt.config.ts +8 -4
- package/layer/server/api/oip/es.post.ts +21 -4
- package/package.json +5 -2
|
@@ -75,6 +75,8 @@ const LOADERS: Record<string, Record<string, () => Promise<any>>> = {
|
|
|
75
75
|
* - `drugs-<fiche>-fiche` : la fiche elle-meme, telle que la page de
|
|
76
76
|
* fr_drugs_app la monte (props de sa route : `brand` et `tab`, `code`,
|
|
77
77
|
* `id`, `label`).
|
|
78
|
+
* - `patients-<famille>` : la fiche d'OIP Case Atlas, que ses pages montent
|
|
79
|
+
* aussi (props `id`, `tab`, `backTo`).
|
|
78
80
|
*/
|
|
79
81
|
const VIEW_LOADERS: Record<string, () => Promise<any>> = {
|
|
80
82
|
"science-phrc": () => import("../components/oip-science/resources/Phrc.vue"),
|
|
@@ -97,6 +99,8 @@ const VIEW_LOADERS: Record<string, () => Promise<any>> = {
|
|
|
97
99
|
"drugs-pnds-fiche": () => import("../components/oip-drugs/fiches/PndsFiche.vue"),
|
|
98
100
|
"drugs-jo-fiche": () => import("../components/oip-drugs/fiches/JoFiche.vue"),
|
|
99
101
|
"drugs-association-fiche": () => import("../components/oip-drugs/fiches/AssociationFiche.vue"),
|
|
102
|
+
"patients-patient": () => import("../components/oip-patients/PatientFiche.vue"),
|
|
103
|
+
"patients-vaccine": () => import("../components/oip-patients/VaccineFiche.vue"),
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
const cache = new Map<string, Component>();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les index d'OIP Case Atlas, tous sur le cluster `science` (celui de PubMed,
|
|
3
|
+
* le cluster de Discovery) : ceux que les fiches patients du layer lisent.
|
|
4
|
+
*/
|
|
5
|
+
export const OIP_PATIENT_INDEXES = {
|
|
6
|
+
// Patients decrits dans la litterature (cas cliniques et groupes de
|
|
7
|
+
// patients), et les groupes qui decrivent la meme population d'un article a
|
|
8
|
+
// l'autre.
|
|
9
|
+
PATIENT_PROFILES: "oip_discovery_patient-profiles",
|
|
10
|
+
COHORT_CLUSTERS: "oip_discovery_cohort-clusters",
|
|
11
|
+
// Patients decrits apres un vaccin : meme forme de fiche, plus le bloc vaccin.
|
|
12
|
+
VACCINE_PATIENTS: "oip_discovery_vaccine-patients",
|
|
13
|
+
// Articles PubMed : l'onglet Publications des fiches.
|
|
14
|
+
PUBMED: "oip-discovery-pubmed",
|
|
15
|
+
} as const;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les familles qu'OIP Case Atlas range dans un projet, decrites pour tout le
|
|
3
|
+
* portail : index, titre, sous-titre et chemin de la fiche dans l'app. L'app
|
|
4
|
+
* les reprend pour sa page de projet, avec ses propres champs a relire (ceux
|
|
5
|
+
* de ses listes, que son export attend).
|
|
6
|
+
*
|
|
7
|
+
* `resourceId` est l'`_id` du document ; la page de la fiche, elle, s'adresse
|
|
8
|
+
* par `record_id` (« 41698193:p1 »), qui porte deux-points.
|
|
9
|
+
*
|
|
10
|
+
* Importe aussi par le serveur : pas d'import propre a Vue ou a Nuxt ici.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { RmProjectKind } from "@tekkare/romulus/utils/projects";
|
|
14
|
+
import { OIP_PATIENT_INDEXES } from "./indexes";
|
|
15
|
+
import { patientDiseaseName, patientLabel } from "./labels";
|
|
16
|
+
|
|
17
|
+
/** La fiche d'un cas, par son `record_id`, sinon son `_id`. */
|
|
18
|
+
export const patientPath = (doc: Record<string, any> | null | undefined, id: string) =>
|
|
19
|
+
`/patients/${encodeURIComponent(doc?.record_id || id)}`;
|
|
20
|
+
export const vaccinePath = (doc: Record<string, any> | null | undefined, id: string) =>
|
|
21
|
+
`/vaccines/${encodeURIComponent(doc?.record_id || id)}`;
|
|
22
|
+
|
|
23
|
+
export const PATIENT_KIND: RmProjectKind = {
|
|
24
|
+
type: "PatientProfile",
|
|
25
|
+
index: OIP_PATIENT_INDEXES.PATIENT_PROFILES,
|
|
26
|
+
label: { fr: "Cas de maladies rares", en: "Rare disease cases" },
|
|
27
|
+
icon: "UsersThree",
|
|
28
|
+
// Les champs du titre, et ceux des autres sujets du meme article (cf. PatientFiche).
|
|
29
|
+
source: ["record_id", "record_type", "condition", "cohort_condition", "label", "n_patients", "pmid", "year"],
|
|
30
|
+
title: (s, lang) => patientDiseaseName(s) || (lang === "fr" ? "Pathologie non précisée" : "Condition not stated"),
|
|
31
|
+
subtitle: (s) =>
|
|
32
|
+
[s.record_type === "cohort" ? "" : patientLabel(s.label), s.pmid ? `PMID ${s.pmid}` : "", s.year]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join(" · "),
|
|
35
|
+
path: patientPath,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const VACCINE_KIND: RmProjectKind = {
|
|
39
|
+
type: "VaccinePatient",
|
|
40
|
+
index: OIP_PATIENT_INDEXES.VACCINE_PATIENTS,
|
|
41
|
+
label: { fr: "Cas après vaccination", en: "Vaccine cases" },
|
|
42
|
+
icon: "Syringe",
|
|
43
|
+
source: ["record_id", "event_term", "label", "vaccine_product_label", "pmid", "year"],
|
|
44
|
+
title: (s, lang) => s.event_term || (lang === "fr" ? "Événement non nommé" : "Event not named"),
|
|
45
|
+
subtitle: (s) => [s.vaccine_product_label, s.pmid ? `PMID ${s.pmid}` : "", s.year].filter(Boolean).join(" · "),
|
|
46
|
+
path: vaccinePath,
|
|
47
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Libelles des vocabulaires de l'index des patients.
|
|
3
|
+
*
|
|
4
|
+
* Un seul module : la liste, la carte et les filtres les lisent tous, et un
|
|
5
|
+
* libelle qui se lit autrement d'un ecran a l'autre ne se remarque pas.
|
|
6
|
+
* L'index ecrit des codes (`single_centre`, `HP:0003593`) ou de l'anglais
|
|
7
|
+
* (`medical_domain`) : la valeur reste celle de l'index, seul l'affichage est
|
|
8
|
+
* traduit.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type PatientLang = "fr" | "en";
|
|
12
|
+
type Labels = Record<string, { fr: string; en: string }>;
|
|
13
|
+
|
|
14
|
+
/** Le libelle d'une valeur, ou la valeur brute quand le vocabulaire ne la connait pas. */
|
|
15
|
+
export function patientLabelOf(labels: Labels, value: string | null | undefined, lang: PatientLang): string {
|
|
16
|
+
if (!value) return "";
|
|
17
|
+
return labels[value]?.[lang] ?? value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Les deux unites de l'index. Ecrites en toutes lettres : « patient » et
|
|
22
|
+
* « cohort » ressemblent a un detail dans une liste de facettes, alors que
|
|
23
|
+
* c'est la difference entre une personne et une population.
|
|
24
|
+
*/
|
|
25
|
+
export const RECORD_TYPE_LABELS: Labels = {
|
|
26
|
+
patient: { fr: "Patient individuel", en: "Individual patient" },
|
|
27
|
+
cohort: { fr: "Groupe de patients", en: "Group of patients" },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Les termes « Onset » de HPO, qui classent chaque age resolu. */
|
|
31
|
+
export const ONSET_CLASS_LABELS: Labels = {
|
|
32
|
+
"HP:0030674": { fr: "Anténatal", en: "Antenatal onset" },
|
|
33
|
+
"HP:0003577": { fr: "Congénital", en: "Congenital onset" },
|
|
34
|
+
"HP:0003623": { fr: "Néonatal", en: "Neonatal onset" },
|
|
35
|
+
"HP:0003593": { fr: "Nourrisson", en: "Infantile onset" },
|
|
36
|
+
"HP:0011463": { fr: "Enfance", en: "Childhood onset" },
|
|
37
|
+
"HP:0003621": { fr: "Adolescence", en: "Juvenile onset" },
|
|
38
|
+
"HP:0011462": { fr: "Jeune adulte", en: "Young adult onset" },
|
|
39
|
+
"HP:0003581": { fr: "Adulte", en: "Adult onset" },
|
|
40
|
+
"HP:0003596": { fr: "Âge moyen", en: "Middle age onset" },
|
|
41
|
+
"HP:0003584": { fr: "Tardif", en: "Late onset" },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SEX_LABELS: Labels = {
|
|
45
|
+
female: { fr: "Femme", en: "Female" },
|
|
46
|
+
male: { fr: "Homme", en: "Male" },
|
|
47
|
+
unknown: { fr: "Non précisé", en: "Not stated" },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const OUTCOME_LABELS: Labels = {
|
|
51
|
+
alive: { fr: "En vie", en: "Alive" },
|
|
52
|
+
died: { fr: "Décédé", en: "Died" },
|
|
53
|
+
lost_to_followup: { fr: "Perdu de vue", en: "Not followed up" },
|
|
54
|
+
unknown: { fr: "Issue non précisée", en: "Outcome not stated" },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Le role que l'article donne a un gene, dans l'ordre ou la fiche les range. */
|
|
58
|
+
export const GENE_ROLE_LABELS: Labels = {
|
|
59
|
+
CAUSATIVE: { fr: "Gène responsable de la maladie", en: "The gene behind the illness" },
|
|
60
|
+
CONTRIBUTORY: { fr: "Gène qui y contribue", en: "Added to the illness" },
|
|
61
|
+
CANDIDATE: { fr: "Cause possible, non confirmée", en: "A possible cause, not confirmed" },
|
|
62
|
+
REJECTED: { fr: "Testé et écarté", en: "Tested and ruled out" },
|
|
63
|
+
UNKNOWN_STATUS: { fr: "Rôle non précisé par l'article", en: "Part not stated by the paper" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const CONDITION_STATUS_LABELS: Labels = {
|
|
67
|
+
DIAGNOSED: { fr: "Diagnostiqué", en: "Diagnosed" },
|
|
68
|
+
SUSPECTED: { fr: "Suspecté", en: "Suspected" },
|
|
69
|
+
COMORBIDITY: { fr: "Comorbidité", en: "Comorbidity" },
|
|
70
|
+
EXCLUDED: { fr: "Exclu", en: "Excluded" },
|
|
71
|
+
UNKNOWN: { fr: "Statut non précisé", en: "Status not stated" },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const COHORT_SETTING_LABELS: Labels = {
|
|
75
|
+
single_centre: { fr: "Un centre", en: "One centre" },
|
|
76
|
+
multicentre: { fr: "Plusieurs centres", en: "Several centres" },
|
|
77
|
+
national_registry: { fr: "Registre national", en: "National registry" },
|
|
78
|
+
screening_programme: { fr: "Programme de dépistage", en: "Screening programme" },
|
|
79
|
+
biobank: { fr: "Biobanque", en: "Biobank" },
|
|
80
|
+
trial: { fr: "Essai", en: "Trial" },
|
|
81
|
+
prior: { fr: "Déjà publié", en: "Previously reported" },
|
|
82
|
+
unclear: { fr: "Cadre non précisé", en: "Setting not stated" },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const COHORT_SCOPE_LABELS: Labels = {
|
|
86
|
+
disease: { fr: "Une maladie", en: "One disease" },
|
|
87
|
+
category: { fr: "Une famille de pathologies", en: "A family of conditions" },
|
|
88
|
+
several: { fr: "Plusieurs maladies", en: "Several diseases" },
|
|
89
|
+
unknown: { fr: "Périmètre non précisé", en: "Scope not stated" },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Les domaines medicaux, que l'index ecrit en anglais. */
|
|
93
|
+
export const MEDICAL_DOMAIN_LABELS: Labels = {
|
|
94
|
+
"Hormones and metabolism": { fr: "Hormones et métabolisme", en: "Hormones and metabolism" },
|
|
95
|
+
"Conditions present from birth": { fr: "Pathologies présentes dès la naissance", en: "Conditions present from birth" },
|
|
96
|
+
"Brain and nerves": { fr: "Cerveau et nerfs", en: "Brain and nerves" },
|
|
97
|
+
"Blood and immune system": { fr: "Sang et système immunitaire", en: "Blood and immune system" },
|
|
98
|
+
"Cancers and tumours": { fr: "Cancers et tumeurs", en: "Cancers and tumours" },
|
|
99
|
+
"Bones, joints and muscles": { fr: "Os, articulations et muscles", en: "Bones, joints and muscles" },
|
|
100
|
+
"Heart and circulation": { fr: "Cœur et circulation", en: "Heart and circulation" },
|
|
101
|
+
"Eyes and vision": { fr: "Yeux et vision", en: "Eyes and vision" },
|
|
102
|
+
"Mental health and behaviour": { fr: "Santé mentale et comportement", en: "Mental health and behaviour" },
|
|
103
|
+
"Kidneys and urinary system": { fr: "Reins et voies urinaires", en: "Kidneys and urinary system" },
|
|
104
|
+
Skin: { fr: "Peau", en: "Skin" },
|
|
105
|
+
Infections: { fr: "Infections", en: "Infections" },
|
|
106
|
+
"Digestive system": { fr: "Appareil digestif", en: "Digestive system" },
|
|
107
|
+
"Lungs and breathing": { fr: "Poumons et respiration", en: "Lungs and breathing" },
|
|
108
|
+
"Conditions around birth": { fr: "Pathologies périnatales", en: "Conditions around birth" },
|
|
109
|
+
"Injury and poisoning": { fr: "Traumatismes et intoxications", en: "Injury and poisoning" },
|
|
110
|
+
"Special purposes": { fr: "Codes spéciaux", en: "Special purposes" },
|
|
111
|
+
"Pregnancy and childbirth": { fr: "Grossesse et accouchement", en: "Pregnancy and childbirth" },
|
|
112
|
+
"Ears and hearing": { fr: "Oreilles et audition", en: "Ears and hearing" },
|
|
113
|
+
"Other signs and findings": { fr: "Autres signes et observations", en: "Other signs and findings" },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* La maladie sous laquelle le document a ete trouve, en titre. Un groupe porte
|
|
118
|
+
* sa propre pathologie ; l'index garde la casse de l'article, souvent en
|
|
119
|
+
* minuscules, d'ou la majuscule.
|
|
120
|
+
*/
|
|
121
|
+
export function patientDiseaseName(profile?: {
|
|
122
|
+
record_type?: string;
|
|
123
|
+
condition?: string | null;
|
|
124
|
+
cohort_condition?: string | null;
|
|
125
|
+
}): string {
|
|
126
|
+
const name =
|
|
127
|
+
profile?.record_type === "cohort"
|
|
128
|
+
? profile?.cohort_condition || profile?.condition
|
|
129
|
+
: profile?.condition;
|
|
130
|
+
return name ? name.charAt(0).toUpperCase() + name.slice(1) : "";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Comment l'article designe le patient, sous une forme qui se lit hors de
|
|
135
|
+
* l'article : un identifiant nu (« II-3 », « P1 », « b ») recoit « Patient »
|
|
136
|
+
* devant lui.
|
|
137
|
+
*/
|
|
138
|
+
export function patientLabel(label: string | null | undefined): string {
|
|
139
|
+
const name = (label || "").trim();
|
|
140
|
+
if (!name) return "Patient";
|
|
141
|
+
return /patient|case|cas\b|proband|subject|proposit/i.test(name) ? name : `Patient ${name}`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Les classes globales d'OIP Case Atlas (main.css : oip_row, oip_list, gap-*)
|
|
3
|
+
que portent les fiches patients du layer (components/oip-patients),
|
|
4
|
+
limitees a leur conteneur `.oip-patients` : une app qui les monte garde ses
|
|
5
|
+
propres styles. Specificite inchangee (:where). A reduire a mesure que les
|
|
6
|
+
fiches passent sur les composants Romulus.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
:where(.oip-patients) .oip_row {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: row;
|
|
12
|
+
}
|
|
13
|
+
:where(.oip-patients) .oip_list {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
}
|
|
17
|
+
:where(.oip-patients) .oip_row.wrap,
|
|
18
|
+
:where(.oip-patients) .oip_list.wrap {
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
}
|
|
21
|
+
:where(.oip-patients) .oip_row.space-between,
|
|
22
|
+
:where(.oip-patients) .oip_list.space-between {
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
}
|
|
25
|
+
:where(.oip-patients) .oip_row.v-start,
|
|
26
|
+
:where(.oip-patients) .oip_list.v-start {
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
}
|
|
29
|
+
:where(.oip-patients) .oip_row.v-center,
|
|
30
|
+
:where(.oip-patients) .oip_list.v-center {
|
|
31
|
+
align-items: center;
|
|
32
|
+
}
|
|
33
|
+
:where(.oip-patients) .oip_row.v-end,
|
|
34
|
+
:where(.oip-patients) .oip_list.v-end {
|
|
35
|
+
align-items: flex-end;
|
|
36
|
+
}
|
|
37
|
+
:where(.oip-patients) .gap-4 {
|
|
38
|
+
gap: 4px;
|
|
39
|
+
}
|
|
40
|
+
:where(.oip-patients) .gap-8 {
|
|
41
|
+
gap: 8px;
|
|
42
|
+
}
|
|
43
|
+
:where(.oip-patients) .gap-12 {
|
|
44
|
+
gap: 12px;
|
|
45
|
+
}
|
|
46
|
+
:where(.oip-patients) .gap-16 {
|
|
47
|
+
gap: 16px;
|
|
48
|
+
}
|
|
49
|
+
:where(.oip-patients) .gap-24 {
|
|
50
|
+
gap: 24px;
|
|
51
|
+
}
|
|
52
|
+
:where(.oip-patients) .full_width {
|
|
53
|
+
width: 100%;
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les deux index des patients de la litterature.
|
|
3
|
+
*
|
|
4
|
+
* `patient-profiles` tient un document par sujet decrit dans une publication :
|
|
5
|
+
* un patient decrit individuellement (`record_type: "patient"`) ou un groupe
|
|
6
|
+
* qu'un article ne decrit qu'en agrege (`record_type: "cohort"`). Les deux
|
|
7
|
+
* unites vivent dans le meme index et ne s'additionnent jamais : une ligne de
|
|
8
|
+
* groupe peut valoir des centaines de personnes.
|
|
9
|
+
*
|
|
10
|
+
* `cohort-clusters` rapproche les groupes qui decrivent la meme population
|
|
11
|
+
* d'un article a l'autre. Le lien est une deduction (auteurs communs, details
|
|
12
|
+
* qui se recoupent), jamais une identite confirmee : tout ce qui en sort se
|
|
13
|
+
* formule comme une probabilite.
|
|
14
|
+
*
|
|
15
|
+
* Seuls les champs que l'app lit sont types.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type PatientRecordType = "patient" | "cohort";
|
|
19
|
+
|
|
20
|
+
export type AgePrecision =
|
|
21
|
+
| "absent"
|
|
22
|
+
| "exact"
|
|
23
|
+
| "range"
|
|
24
|
+
| "aggregate"
|
|
25
|
+
| "class_derived"
|
|
26
|
+
| "assumed_unit"
|
|
27
|
+
| "gestational";
|
|
28
|
+
|
|
29
|
+
/** Un age tel que l'article l'a formule, avec sa classe HPO quand elle est resolue. */
|
|
30
|
+
export interface AgeStatement {
|
|
31
|
+
text?: string;
|
|
32
|
+
class?: string;
|
|
33
|
+
class_label?: string;
|
|
34
|
+
precision?: AgePrecision;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Un signe tel que l'article le rapporte : `present: false` l'ecarte explicitement. */
|
|
38
|
+
export interface RecordPhenotype {
|
|
39
|
+
hpo_id?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
present?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Une ligne genetique de l'article, avec le role qu'il donne au gene. */
|
|
45
|
+
export interface RecordGenotype {
|
|
46
|
+
gene_hgnc?: string;
|
|
47
|
+
cdna?: string;
|
|
48
|
+
protein?: string;
|
|
49
|
+
zygosity?: string;
|
|
50
|
+
gene_source?: string;
|
|
51
|
+
interpretation_status?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PatientProfile {
|
|
55
|
+
_id?: string;
|
|
56
|
+
record_id: string;
|
|
57
|
+
record_type: PatientRecordType;
|
|
58
|
+
label?: string;
|
|
59
|
+
|
|
60
|
+
// Publication
|
|
61
|
+
pmid?: string;
|
|
62
|
+
title?: string;
|
|
63
|
+
year?: number;
|
|
64
|
+
journal?: string;
|
|
65
|
+
summary?: string | null;
|
|
66
|
+
evidence_basis?: "primary" | "secondary";
|
|
67
|
+
/** Derniere lecture de la fiche par le pipeline (ISO). */
|
|
68
|
+
built_at?: string;
|
|
69
|
+
|
|
70
|
+
// Sujet
|
|
71
|
+
subject?: "human" | "animal";
|
|
72
|
+
species?: string;
|
|
73
|
+
sex?: "female" | "male" | "unknown";
|
|
74
|
+
country?: string;
|
|
75
|
+
outcome?: string;
|
|
76
|
+
|
|
77
|
+
// Pathologie
|
|
78
|
+
condition?: string | null;
|
|
79
|
+
condition_status?: string;
|
|
80
|
+
source_orpha?: number[];
|
|
81
|
+
condition_name_orpha?: string[];
|
|
82
|
+
medical_domain?: string[];
|
|
83
|
+
ruled_out_names?: string[];
|
|
84
|
+
genes?: string[];
|
|
85
|
+
causative_genes?: string[];
|
|
86
|
+
variants?: string[];
|
|
87
|
+
hpo_labels?: string[];
|
|
88
|
+
|
|
89
|
+
// Ages, a plat : classe HPO, bornes en jours, precision de l'enonce
|
|
90
|
+
age_onset_class?: string;
|
|
91
|
+
age_onset_days_min?: number;
|
|
92
|
+
age_onset_days_max?: number;
|
|
93
|
+
age_onset_precision?: AgePrecision;
|
|
94
|
+
age_report_class?: string;
|
|
95
|
+
age_report_days_min?: number;
|
|
96
|
+
age_report_days_max?: number;
|
|
97
|
+
age_report_precision?: AgePrecision;
|
|
98
|
+
|
|
99
|
+
// Groupe de patients
|
|
100
|
+
cohort_id?: string;
|
|
101
|
+
cohort_scope?: string;
|
|
102
|
+
cohort_setting?: string;
|
|
103
|
+
cohort_condition?: string | null;
|
|
104
|
+
cohort_ascertainment?: string | null;
|
|
105
|
+
cohort_country_names?: string[];
|
|
106
|
+
n_patients?: number;
|
|
107
|
+
cohort_n_female?: number;
|
|
108
|
+
cohort_n_male?: number;
|
|
109
|
+
cohort_n_alive?: number;
|
|
110
|
+
cohort_n_died?: number;
|
|
111
|
+
cohort_inclusion?: string | null;
|
|
112
|
+
cohort_follow_up?: string | null;
|
|
113
|
+
ethnicity?: string | null;
|
|
114
|
+
|
|
115
|
+
/** L'enonce de l'article, quand la liste en a besoin (ages, statistiques du groupe). */
|
|
116
|
+
record?: {
|
|
117
|
+
age_onset?: AgeStatement;
|
|
118
|
+
age_diagnosis?: AgeStatement;
|
|
119
|
+
age_report?: AgeStatement;
|
|
120
|
+
age_death?: AgeStatement;
|
|
121
|
+
summary?: string | null;
|
|
122
|
+
consanguineous?: boolean | null;
|
|
123
|
+
family_history?: string | null;
|
|
124
|
+
phenotypes?: RecordPhenotype[];
|
|
125
|
+
genotype?: RecordGenotype[];
|
|
126
|
+
timeline?: { age?: AgeStatement; event?: string }[];
|
|
127
|
+
ruled_out?: { disease?: string; evidence?: string }[];
|
|
128
|
+
cohort?: {
|
|
129
|
+
countries?: string[];
|
|
130
|
+
age_onset_stat?: string | null;
|
|
131
|
+
age_diagnosis_stat?: string | null;
|
|
132
|
+
age_at_study_stat?: string | null;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface CohortCluster {
|
|
138
|
+
/** Groupe canonique du cluster. */
|
|
139
|
+
cohort_id: string;
|
|
140
|
+
/** PMID des publications rapprochees. */
|
|
141
|
+
pmids: string[];
|
|
142
|
+
/**
|
|
143
|
+
* Bornes du nombre de personnes DISTINCTES : `n_min` est le plus grand
|
|
144
|
+
* groupe, `n_max` la somme si aucun ne se recoupait.
|
|
145
|
+
*/
|
|
146
|
+
n_min: number;
|
|
147
|
+
n_max: number;
|
|
148
|
+
evidence: ("declared" | "authors")[];
|
|
149
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Une fiche d'OIP Case Atlas (cas de maladie rare ou cas apres vaccination),
|
|
3
|
+
* pour sa vue : le document entier moins ses vecteurs, les autres sujets du
|
|
4
|
+
* meme article (une serie de cas se lit comme une serie) et, pour un groupe de
|
|
5
|
+
* patients, le cluster qui dit s'il a deja ete decrit ailleurs.
|
|
6
|
+
*
|
|
7
|
+
* La fiche s'adresse par son `record_id` (sa page) ou par son `_id` (ce qu'un
|
|
8
|
+
* projet range) : les deux sont acceptes.
|
|
9
|
+
*/
|
|
10
|
+
import type { Ref } from "vue";
|
|
11
|
+
import { oipEsSearch } from "../composables/useOipResources";
|
|
12
|
+
import { OIP_PATIENT_INDEXES } from "./indexes";
|
|
13
|
+
|
|
14
|
+
export function useCaseRecord(
|
|
15
|
+
index: typeof OIP_PATIENT_INDEXES.PATIENT_PROFILES | typeof OIP_PATIENT_INDEXES.VACCINE_PATIENTS,
|
|
16
|
+
id: Ref<string>,
|
|
17
|
+
/** Champs des autres sujets du meme article. */
|
|
18
|
+
siblingSource: string[],
|
|
19
|
+
) {
|
|
20
|
+
const search = (target: string, body: Record<string, unknown>) => oipEsSearch<any>("science", target, body);
|
|
21
|
+
|
|
22
|
+
const profile = ref<Record<string, any> | null>(null);
|
|
23
|
+
const samePaper = ref<Record<string, any>[]>([]);
|
|
24
|
+
const cluster = ref<Record<string, any> | null>(null);
|
|
25
|
+
const loading = ref(true);
|
|
26
|
+
const error = ref(false);
|
|
27
|
+
let request = 0;
|
|
28
|
+
|
|
29
|
+
async function load(value: string) {
|
|
30
|
+
const current = ++request;
|
|
31
|
+
loading.value = true;
|
|
32
|
+
error.value = false;
|
|
33
|
+
profile.value = null;
|
|
34
|
+
samePaper.value = [];
|
|
35
|
+
cluster.value = null;
|
|
36
|
+
try {
|
|
37
|
+
const res = await search(index, {
|
|
38
|
+
size: 1,
|
|
39
|
+
_source: { excludes: ["summary_vector", "record.summary_vector"] },
|
|
40
|
+
query: { bool: { should: [{ term: { record_id: value } }, { ids: { values: [value] } }], minimum_should_match: 1 } },
|
|
41
|
+
});
|
|
42
|
+
if (current !== request) return;
|
|
43
|
+
const hit = res?.hits?.hits?.[0];
|
|
44
|
+
profile.value = hit ? { ...hit._source, _id: hit._id } : null;
|
|
45
|
+
const p = profile.value;
|
|
46
|
+
if (!p) return;
|
|
47
|
+
|
|
48
|
+
// Les lectures secondaires ne coutent qu'une mention : la fiche
|
|
49
|
+
// s'affiche meme si elles echouent.
|
|
50
|
+
await Promise.all([
|
|
51
|
+
p.pmid
|
|
52
|
+
? search(index, {
|
|
53
|
+
size: 50,
|
|
54
|
+
_source: siblingSource,
|
|
55
|
+
query: { bool: { must: [{ term: { pmid: p.pmid } }], must_not: [{ term: { record_id: p.record_id } }] } },
|
|
56
|
+
})
|
|
57
|
+
.then((r) => current === request && (samePaper.value = (r?.hits?.hits ?? []).map((h: any) => h._source)))
|
|
58
|
+
.catch((err) => console.error("Same-paper records failed:", err))
|
|
59
|
+
: null,
|
|
60
|
+
p.cohort_id && index === OIP_PATIENT_INDEXES.PATIENT_PROFILES
|
|
61
|
+
? search(OIP_PATIENT_INDEXES.COHORT_CLUSTERS, { size: 1, query: { term: { cohort_id: p.cohort_id } } })
|
|
62
|
+
.then((r) => current === request && (cluster.value = r?.hits?.hits?.[0]?._source ?? null))
|
|
63
|
+
.catch((err) => console.error("Cohort cluster failed:", err))
|
|
64
|
+
: null,
|
|
65
|
+
]);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (current !== request) return;
|
|
68
|
+
console.error("Case record failed:", err);
|
|
69
|
+
error.value = true;
|
|
70
|
+
} finally {
|
|
71
|
+
if (current === request) loading.value = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
watch(id, (value) => value && load(value), { immediate: true });
|
|
76
|
+
|
|
77
|
+
return { profile, samePaper, cluster, loading, error, reload: () => load(id.value) };
|
|
78
|
+
}
|