@tekkare/romulus 0.1.251 → 0.1.253
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/utils/projects.d.ts +8 -0
- package/dist/runtime/utils/projects.js +8 -5
- package/layer/app/app.config.ts +6 -0
- package/layer/app/components/OipPubmedSummary.vue +223 -0
- package/layer/app/components/OipResourceDrawer.vue +331 -0
- package/layer/app/components/OipResourceDrawerLink.vue +40 -0
- package/layer/app/components/OipResourceField.vue +41 -0
- package/layer/app/components/OipResourceGenericCard.vue +54 -0
- package/layer/app/components/OipResourceSource.vue +50 -0
- package/layer/app/components/OipResourceTags.vue +65 -0
- package/layer/app/components/OipResourceView.vue +107 -0
- package/layer/app/components/oip-drugs/fiches/AssociationFiche.vue +775 -0
- package/layer/app/components/oip-drugs/fiches/BrandFiche.vue +1111 -0
- package/layer/app/components/oip-drugs/fiches/IndicationFiche.vue +182 -0
- package/layer/app/components/oip-drugs/fiches/JoFiche.vue +101 -0
- package/layer/app/components/oip-drugs/fiches/PndsFiche.vue +279 -0
- package/layer/app/components/oip-drugs/heart/JoCard.vue +314 -0
- package/layer/app/components/oip-drugs/heart/TextHighlight.vue +326 -0
- package/layer/app/components/oip-drugs/heart/ansm/BrandCard.vue +187 -0
- package/layer/app/components/oip-drugs/heart/ansm/CisDetail.vue +363 -0
- package/layer/app/components/oip-drugs/heart/ansm/CisTable.vue +167 -0
- package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyCard.vue +373 -0
- package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyFiche.vue +372 -0
- package/layer/app/components/oip-drugs/heart/associations/TranscriptionCard.vue +55 -0
- package/layer/app/components/oip-drugs/heart/elastic/SourceContainerElastic.vue +103 -0
- package/layer/app/components/oip-drugs/heart/france/BrandGroupCard.vue +450 -0
- package/layer/app/components/oip-drugs/heart/mitm/Card.vue +199 -0
- package/layer/app/components/oip-drugs/heart/mitm/Source.vue +23 -0
- package/layer/app/components/oip-drugs/heart/mitm/details/Overview.vue +381 -0
- package/layer/app/components/oip-drugs/heart/put/Card.vue +212 -0
- package/layer/app/components/oip-drugs/heart/results/AccesCard.vue +696 -0
- package/layer/app/components/oip-drugs/heart/results/AlertCard.vue +342 -0
- package/layer/app/components/oip-drugs/heart/results/AsmrCursor.vue +229 -0
- package/layer/app/components/oip-drugs/heart/results/EvalCard.vue +370 -0
- package/layer/app/components/oip-drugs/heart/results/IndicCard.vue +743 -0
- package/layer/app/components/oip-drugs/heart/results/MatchExplain.vue +243 -0
- package/layer/app/components/oip-drugs/heart/results/PrefRows.vue +105 -0
- package/layer/app/components/oip-drugs/heart/results/details/HasHistory.vue +487 -0
- package/layer/app/components/oip-drugs/heart/results/details/InternationalCountryCard.vue +461 -0
- package/layer/app/components/oip-drugs/heart/results/details/InternationalOverview.vue +200 -0
- package/layer/app/components/oip-drugs/heart/results/details/InternationalPriceDrawer.vue +647 -0
- package/layer/app/components/oip-drugs/heart/results/details/Monograph.vue +128 -0
- package/layer/app/components/oip-drugs/heart/results/details/OpinionDrawer.vue +535 -0
- package/layer/app/components/oip-drugs/heart/results/details/PresentationDrawer.vue +738 -0
- package/layer/app/components/oip-drugs/heart/results/details/Price.vue +227 -0
- package/layer/app/components/oip-drugs/heart/results/details/PriceOverview.vue +1261 -0
- package/layer/app/components/oip-drugs/heart/results/details/RecordDrawer.vue +494 -0
- package/layer/app/components/oip-drugs/heart/results/details/RecordList.vue +426 -0
- package/layer/app/components/oip-drugs/heart/results/les/Card.vue +340 -0
- package/layer/app/components/oip-drugs/heart/results/les/Drawer.vue +67 -0
- package/layer/app/components/oip-drugs/heart/results/les/IndicationDetail.vue +149 -0
- package/layer/app/components/oip-drugs/heart/results/les/Source.vue +20 -0
- package/layer/app/components/oip-drugs/heart/results/les/SourceTag.vue +39 -0
- package/layer/app/components/oip-drugs/heart/searchPage/DynamicSource.vue +29 -0
- package/layer/app/components/oip-drugs/medicam/market/Chart.vue +121 -0
- package/layer/app/components/oip-drugs/outpatient/ProductEvolution.vue +236 -0
- package/layer/app/components/oip-drugs/resources/Ansm.vue +75 -0
- package/layer/app/components/oip-drugs/resources/Association.vue +30 -0
- package/layer/app/components/oip-drugs/resources/Brand.vue +65 -0
- package/layer/app/components/oip-drugs/resources/Contribution.vue +216 -0
- package/layer/app/components/oip-drugs/resources/Hospital.vue +151 -0
- package/layer/app/components/oip-drugs/resources/Indication.vue +54 -0
- package/layer/app/components/oip-drugs/resources/Jo.vue +57 -0
- package/layer/app/components/oip-drugs/resources/Medicam.vue +140 -0
- package/layer/app/components/oip-drugs/resources/Mitm.vue +84 -0
- package/layer/app/components/oip-drugs/resources/NationalAssembly.vue +93 -0
- package/layer/app/components/oip-drugs/resources/Outpatient.vue +97 -0
- package/layer/app/components/oip-drugs/resources/Pnds.vue +24 -0
- package/layer/app/components/oip-drugs/resources/Price.vue +106 -0
- package/layer/app/components/oip-drugs/resources/Put.vue +143 -0
- package/layer/app/components/oip-science/appel-a-projet/Cards.vue +162 -0
- package/layer/app/components/oip-science/clinical-trials/Cards.vue +382 -0
- package/layer/app/components/oip-science/clinical-trials/CardsGlobal.vue +273 -0
- package/layer/app/components/oip-science/clinical-trials/LinkedList.vue +366 -0
- package/layer/app/components/oip-science/clinical-trials/PairBadge.vue +25 -0
- package/layer/app/components/oip-science/clinical-trials/PairBody.vue +182 -0
- package/layer/app/components/oip-science/clinical-trials/SitesDrawer.vue +95 -0
- package/layer/app/components/oip-science/common/Form.vue +73 -0
- package/layer/app/components/oip-science/common/MatchAvgTag.vue +283 -0
- package/layer/app/components/oip-science/common/NoData.vue +16 -0
- package/layer/app/components/oip-science/common/ScoreBadge.vue +35 -0
- package/layer/app/components/oip-science/common/Source.vue +31 -0
- package/layer/app/components/oip-science/common/TableTags.vue +279 -0
- package/layer/app/components/oip-science/common/Timeline.vue +104 -0
- package/layer/app/components/oip-science/common/ToolTips.vue +89 -0
- package/layer/app/components/oip-science/cross/publicationPubmed.vue +484 -0
- package/layer/app/components/oip-science/crossData/CrossDataLocations.vue +71 -0
- package/layer/app/components/oip-science/ctgov/Investigators.vue +148 -0
- package/layer/app/components/oip-science/ctgov/Locations.vue +102 -0
- package/layer/app/components/oip-science/ctgov/Publications.vue +52 -0
- package/layer/app/components/oip-science/ctgov/Sponsors.vue +66 -0
- package/layer/app/components/oip-science/ctgov/StudyDescription.vue +1136 -0
- package/layer/app/components/oip-science/ctgov-centers/ClinicalTrials.vue +40 -0
- package/layer/app/components/oip-science/ctgov-centers/Publications.vue +50 -0
- package/layer/app/components/oip-science/ctgov-investigators/ClinicalTrials.vue +43 -0
- package/layer/app/components/oip-science/ctgov-investigators/Informations.vue +171 -0
- package/layer/app/components/oip-science/ctgov-investigators/Publications.vue +44 -0
- package/layer/app/components/oip-science/elastic/DateRangeFilter.vue +209 -0
- package/layer/app/components/oip-science/elastic/Filter.vue +719 -0
- package/layer/app/components/oip-science/elastic/MatchExplain.vue +152 -0
- package/layer/app/components/oip-science/establishments/Cards.vue +183 -0
- package/layer/app/components/oip-science/establishments/ClinicalTrials.vue +62 -0
- package/layer/app/components/oip-science/establishments/Hcps.vue +227 -0
- package/layer/app/components/oip-science/establishments/Informations.vue +263 -0
- package/layer/app/components/oip-science/establishments/LinkedList.vue +153 -0
- package/layer/app/components/oip-science/establishments/MatchedLabels.vue +194 -0
- package/layer/app/components/oip-science/establishments/PublicFinance.vue +62 -0
- package/layer/app/components/oip-science/euct/Investigators.vue +125 -0
- package/layer/app/components/oip-science/euct/Locations.vue +102 -0
- package/layer/app/components/oip-science/euct/Products.vue +101 -0
- package/layer/app/components/oip-science/euct/Publications.vue +89 -0
- package/layer/app/components/oip-science/euct/Sponsors.vue +86 -0
- package/layer/app/components/oip-science/euct/StudyDescription.vue +1075 -0
- package/layer/app/components/oip-science/euct-centers/ClinicalTrials.vue +40 -0
- package/layer/app/components/oip-science/euct-centers/Publications.vue +55 -0
- package/layer/app/components/oip-science/euct-investigators/ClinicalTrials.vue +41 -0
- package/layer/app/components/oip-science/euct-investigators/Informations.vue +171 -0
- package/layer/app/components/oip-science/euct-investigators/Publications.vue +51 -0
- package/layer/app/components/oip-science/hcps/AppelsAProjet.vue +62 -0
- package/layer/app/components/oip-science/hcps/Cards.vue +237 -0
- package/layer/app/components/oip-science/hcps/ClinicalTrials.vue +78 -0
- package/layer/app/components/oip-science/hcps/Informations.vue +296 -0
- package/layer/app/components/oip-science/hcps/LinkedList.vue +153 -0
- package/layer/app/components/oip-science/hcps/MatchedLabels.vue +207 -0
- package/layer/app/components/oip-science/hcps/Pubmed.vue +128 -0
- package/layer/app/components/oip-science/local/SliderTabs.vue +99 -0
- package/layer/app/components/oip-science/local/SliderTabsFull.vue +135 -0
- package/layer/app/components/oip-science/pubmed/Authors.vue +128 -0
- package/layer/app/components/oip-science/pubmed/Cards.vue +417 -0
- package/layer/app/components/oip-science/pubmed/ClinicalTrials.vue +71 -0
- package/layer/app/components/oip-science/resources/Phrc.vue +39 -0
- package/layer/app/composables/useOipChapterScroll.ts +43 -0
- package/layer/app/composables/useOipResourceHooks.ts +27 -0
- package/layer/app/composables/useOipResourceNav.ts +66 -0
- package/layer/app/composables/useOipResourceViews.ts +117 -0
- package/layer/app/composables/useOipResources.ts +83 -0
- package/layer/app/oip-drugs/PORTING.md +120 -0
- package/layer/app/oip-drugs/composables/useAssociationDetail.ts +220 -0
- package/layer/app/oip-drugs/composables/useAssociationExport.ts +63 -0
- package/layer/app/oip-drugs/composables/useAssociationLookup.ts +105 -0
- package/layer/app/oip-drugs/composables/useCardPrefOptions.ts +201 -0
- package/layer/app/oip-drugs/composables/useCurrency.ts +121 -0
- package/layer/app/oip-drugs/composables/useDirectusSources.ts +224 -0
- package/layer/app/oip-drugs/composables/useHeartBrandMatches.ts +112 -0
- package/layer/app/oip-drugs/composables/useHeartDetailSearches.ts +479 -0
- package/layer/app/oip-drugs/composables/useHeartIntlApprovals.ts +661 -0
- package/layer/app/oip-drugs/composables/useHeartLesIndication.ts +168 -0
- package/layer/app/oip-drugs/composables/useHeartMatchExplain.ts +231 -0
- package/layer/app/oip-drugs/composables/useHeartReports.ts +239 -0
- package/layer/app/oip-drugs/composables/useHeartSearchFields.ts +233 -0
- package/layer/app/oip-drugs/composables/useMergeByKey.ts +23 -0
- package/layer/app/oip-drugs/composables/useOipDrugsElastic.ts +621 -0
- package/layer/app/oip-drugs/composables/usePageEmpty.ts +58 -0
- package/layer/app/oip-drugs/composables/v-useSalesSource.ts +64 -0
- package/layer/app/oip-drugs/drugs.css +500 -0
- package/layer/app/oip-drugs/esIndices.ts +322 -0
- package/layer/app/oip-drugs/kinds.ts +264 -0
- package/layer/app/oip-drugs/locales/en.json +1571 -0
- package/layer/app/oip-drugs/locales/fr.json +1571 -0
- package/layer/app/oip-drugs/monographs/blincyto-has.vue +289 -0
- package/layer/app/oip-drugs/monographs/darzalex-has.vue +276 -0
- package/layer/app/oip-drugs/monographs/dupixent-has.vue +216 -0
- package/layer/app/oip-drugs/monographs/enhertu-has.vue +280 -0
- package/layer/app/oip-drugs/monographs/has-sein-2023.vue +1649 -0
- package/layer/app/oip-drugs/monographs/ibrance-has.vue +277 -0
- package/layer/app/oip-drugs/monographs/imfinzi-has.vue +293 -0
- package/layer/app/oip-drugs/monographs/keytruda-has.vue +334 -0
- package/layer/app/oip-drugs/monographs/kisqali-has.vue +201 -0
- package/layer/app/oip-drugs/monographs/opdivo-has.vue +231 -0
- package/layer/app/oip-drugs/monographs/ozempic-has.vue +186 -0
- package/layer/app/oip-drugs/monographs/tecentriq-has.vue +312 -0
- package/layer/app/oip-drugs/monographs/yescarta-has.vue +272 -0
- package/layer/app/oip-drugs/types/elastic.ts +113 -0
- package/layer/app/oip-drugs/types/es/agencies.ts +152 -0
- package/layer/app/oip-drugs/types/es/directory.ts +54 -0
- package/layer/app/oip-drugs/types/es/docByIndex.ts +151 -0
- package/layer/app/oip-drugs/types/es/openmedic.ts +67 -0
- package/layer/app/oip-drugs/types/es/outpatient.ts +71 -0
- package/layer/app/oip-drugs/types/es/portal.ts +130 -0
- package/layer/app/oip-drugs/types/es/retail.ts +107 -0
- package/layer/app/oip-drugs/types/es/statistics.ts +29 -0
- package/layer/app/oip-drugs/types/heart/CurrencyRate.ts +16 -0
- package/layer/app/oip-drugs/types/heart/ansm.ts +106 -0
- package/layer/app/oip-drugs/types/heart/tag.ts +29 -0
- package/layer/app/oip-drugs/utils/associationFields.ts +118 -0
- package/layer/app/oip-drugs/utils/contributions.ts +36 -0
- package/layer/app/oip-drugs/utils/countryMapping.ts +387 -0
- package/layer/app/oip-drugs/utils/esHighlight.ts +21 -0
- package/layer/app/oip-drugs/utils/exportSheets.ts +57 -0
- package/layer/app/oip-drugs/utils/heartIntlPrice.ts +228 -0
- package/layer/app/oip-drugs/utils/heartStyleAndFormat.ts +262 -0
- package/layer/app/oip-drugs/utils/heartStyleAnsm.ts +71 -0
- package/layer/app/oip-drugs/utils/latestDate.ts +31 -0
- package/layer/app/oip-drugs/utils/p-heartCardTags.ts +53 -0
- package/layer/app/oip-drugs/utils/priceExport.ts +269 -0
- package/layer/app/oip-drugs/utils/q-hasRatings.ts +43 -0
- package/layer/app/oip-drugs/utils/s-openCard.ts +20 -0
- package/layer/app/oip-drugs/utils/sourceUuids.ts +155 -0
- package/layer/app/oip-drugs/utils/styleAndFormat.ts +40 -0
- package/layer/app/oip-drugs/utils/v-salesSeries.ts +156 -0
- package/layer/app/oip-drugs/utils/v-scopeTags.ts +23 -0
- package/layer/app/oip-science/PORTING.md +93 -0
- package/layer/app/oip-science/assets/smartSelection.json +428 -0
- package/layer/app/oip-science/composables/useAuthorMatching.ts +342 -0
- package/layer/app/oip-science/composables/useCTCenter.ts +107 -0
- package/layer/app/oip-science/composables/useCTInvestigators.ts +107 -0
- package/layer/app/oip-science/composables/useEstablishmentCrossData.ts +139 -0
- package/layer/app/oip-science/composables/useEstablishmentMatching.ts +176 -0
- package/layer/app/oip-science/composables/useEuct.ts +418 -0
- package/layer/app/oip-science/composables/useEuctCenter.ts +107 -0
- package/layer/app/oip-science/composables/useEuctInvestigators.ts +107 -0
- package/layer/app/oip-science/composables/useGlobalClinicalTrials.ts +624 -0
- package/layer/app/oip-science/composables/useLinkedTrials.ts +83 -0
- package/layer/app/oip-science/composables/useOipElastic.ts +658 -0
- package/layer/app/oip-science/composables/useOipSearchExplain.ts +166 -0
- package/layer/app/oip-science/composables/usePubmed.ts +487 -0
- package/layer/app/oip-science/composables/useSearchFields.ts +153 -0
- package/layer/app/oip-science/indexConfig.json +794 -0
- package/layer/app/oip-science/locales/en.json +483 -0
- package/layer/app/oip-science/locales/fr.json +483 -0
- package/layer/app/oip-science/science.css +1182 -0
- package/layer/app/oip-science/utils/a-openCard.ts +23 -0
- package/layer/app/oip-science/utils/b-openCard.ts +35 -0
- package/layer/app/oip-science/utils/c-filterContext.ts +81 -0
- package/layer/app/oip-science/utils/f-types.ts +241 -0
- package/layer/app/oip-science/utils/frDiseaseTerms.ts +269 -0
- package/layer/app/oip-science/utils/matchMethod.ts +53 -0
- package/layer/app/oip-science/utils/sliderTabs.ts +13 -0
- package/layer/app/oip-science/utils/sources.ts +15 -0
- package/layer/app/oip-science/utils/styleAndFormat.ts +608 -0
- package/layer/app/oip-science/utils/toPresets.ts +7 -0
- package/layer/app/oip-science/utils/valueTable.ts +126 -0
- package/layer/app/plugins/oip-science-i18n.ts +17 -0
- package/layer/app/utils/oipFicheRoutes.ts +118 -0
- package/layer/app/utils/oipResources.ts +440 -0
- package/layer/nuxt.config.ts +22 -2
- package/layer/public/reports/blincyto-has.html +292 -0
- package/layer/public/reports/blincyto-has.pdf +0 -0
- package/layer/public/reports/darzalex-has.html +279 -0
- package/layer/public/reports/darzalex-has.pdf +0 -0
- package/layer/public/reports/dupixent-has.html +217 -0
- package/layer/public/reports/dupixent-has.pdf +0 -0
- package/layer/public/reports/enhertu-has.html +283 -0
- package/layer/public/reports/enhertu-has.pdf +0 -0
- package/layer/public/reports/has-sein-2023.html +1639 -0
- package/layer/public/reports/ibrance-has.html +280 -0
- package/layer/public/reports/ibrance-has.pdf +0 -0
- package/layer/public/reports/imfinzi-has.html +297 -0
- package/layer/public/reports/imfinzi-has.pdf +0 -0
- package/layer/public/reports/keytruda-has.html +337 -0
- package/layer/public/reports/keytruda-has.pdf +0 -0
- package/layer/public/reports/kisqali-has.html +202 -0
- package/layer/public/reports/kisqali-has.pdf +0 -0
- package/layer/public/reports/opdivo-has.html +232 -0
- package/layer/public/reports/opdivo-has.pdf +0 -0
- package/layer/public/reports/ozempic-has.html +188 -0
- package/layer/public/reports/ozempic-has.pdf +0 -0
- package/layer/public/reports/tecentriq-has.html +315 -0
- package/layer/public/reports/tecentriq-has.pdf +0 -0
- package/layer/public/reports/yescarta-has.html +275 -0
- package/layer/public/reports/yescarta-has.pdf +0 -0
- package/layer/server/api/oip/es.post.ts +99 -0
- package/layer/server/api/oip/pdf.get.ts +70 -0
- package/layer/server/api/oip/reports.get.ts +98 -0
- package/layer/server/api/oip/resource-types.get.ts +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ES_INDEX } from "../esIndices";
|
|
2
|
+
import useOipDrugsElastic from "./useOipDrugsElastic";
|
|
3
|
+
/**
|
|
4
|
+
* Liste en sus (LES) : lecture d'une indication.
|
|
5
|
+
*
|
|
6
|
+
* L'index LES tient une ligne par couple indication x produit. Une indication
|
|
7
|
+
* (code CPEMB13, I000711...) se lit donc en rassemblant ses lignes : la carte
|
|
8
|
+
* d'identite (libelle, produits, marques) et l'historique d'inscription de
|
|
9
|
+
* chaque produit. Ces lectures servent a la page `indication/[id]` et au tiroir
|
|
10
|
+
* LES de la fiche marque, pour que les deux montrent exactement la meme chose.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Index de la liste en sus, sur le cluster « pubmed ». */
|
|
14
|
+
export const HEART_LES_INDEX = ES_INDEX.INDICATION_SEARCH_HAS;
|
|
15
|
+
|
|
16
|
+
/** Colonnes d'historique, montrees seulement si au moins un produit les renseigne. */
|
|
17
|
+
const HISTORY_FIELDS = [
|
|
18
|
+
// `flag` (OUI / NON) et `tag` sont rendus en pastilles par indicationDetail.vue.
|
|
19
|
+
{ field: "statut", type: "flag", name: "Status", frName: "Statut" },
|
|
20
|
+
{ field: "drug_type", type: "tag", name: "Drug Type", frName: "Type" },
|
|
21
|
+
{ field: "start_date", type: "date", name: "Start Date", frName: "Date Début" },
|
|
22
|
+
{ field: "amm_date", type: "date", name: "AMM Date", frName: "Date AMM" },
|
|
23
|
+
{ field: "end_date", type: "date", name: "End Date", frName: "Date Fin" },
|
|
24
|
+
{ field: "end_motive", type: "", name: "End Motive", frName: "Motif Fin" },
|
|
25
|
+
{
|
|
26
|
+
field: "end_date_treatment",
|
|
27
|
+
type: "date",
|
|
28
|
+
name: "Treatment End Date",
|
|
29
|
+
frName: "Date Fin Traitement",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
field: "end_date_contract",
|
|
33
|
+
type: "date",
|
|
34
|
+
name: "Contract End Date",
|
|
35
|
+
frName: "Date Fin Contrat",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export type LesHistoryField = (typeof HISTORY_FIELDS)[number];
|
|
40
|
+
|
|
41
|
+
/** Toutes les lignes d'une indication. */
|
|
42
|
+
export function useHeartLesIndicationLoader() {
|
|
43
|
+
const { queryCustomElastic } = useOipDrugsElastic();
|
|
44
|
+
const results = ref<any[] | null>(null);
|
|
45
|
+
const isLoading = ref(false);
|
|
46
|
+
|
|
47
|
+
async function load(code: string) {
|
|
48
|
+
isLoading.value = true;
|
|
49
|
+
const res = await queryCustomElastic({
|
|
50
|
+
index: HEART_LES_INDEX,
|
|
51
|
+
// `indication` est analyse : un `match` ramenait les indications
|
|
52
|
+
// voisines. La page liste agrege sur `indication.keyword`, meme champ ici.
|
|
53
|
+
customQuery: { query: { term: { "indication.keyword": code } }, size: 100 },
|
|
54
|
+
cluster: "pubmed",
|
|
55
|
+
}).catch(() => null);
|
|
56
|
+
results.value = res?.results ?? null;
|
|
57
|
+
isLoading.value = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { results, isLoading, load };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Ce que la carte LES attend : l'indication et ses produits. */
|
|
64
|
+
export function heartLesCardData(code: string, results: any[] | null) {
|
|
65
|
+
if (!results?.length) return null;
|
|
66
|
+
return {
|
|
67
|
+
indication: code,
|
|
68
|
+
indication_label_fr: results[0].indication_label_fr ?? "",
|
|
69
|
+
all_brand_by_ind:
|
|
70
|
+
results[0].all_brand_by_ind ??
|
|
71
|
+
results.map((r: any) => `[${r.brand} (${r.owner_regroup})]`).join(""),
|
|
72
|
+
products: results.map((r: any) => ({
|
|
73
|
+
atc5_fr: r.atc5_fr,
|
|
74
|
+
brand: r.brand,
|
|
75
|
+
owner_regroup: r.owner_regroup,
|
|
76
|
+
source: r.source,
|
|
77
|
+
start_date: r.start_date,
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function heartLesAvailableFields(results: any[] | null): LesHistoryField[] {
|
|
83
|
+
if (!results) return [];
|
|
84
|
+
return HISTORY_FIELDS.filter((f) => results.find((a) => a[f.field] !== null));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* En-tete de la colonne statut. Le titre de l'index (« Accès et prise en charge
|
|
89
|
+
* au titre de l'accès précoce ») tient sur six lignes dans une colonne : on le
|
|
90
|
+
* ramene a l'essentiel, la valeur OUI / NON dit le reste.
|
|
91
|
+
*/
|
|
92
|
+
function shortStatusTitle(title: string | null | undefined, fr: boolean) {
|
|
93
|
+
const value = title ?? "";
|
|
94
|
+
if (/acc[eè]s pr[eé]coce/i.test(value)) return fr ? "Accès précoce" : "Early access";
|
|
95
|
+
if (/liste en sus/i.test(value)) return fr ? "Liste en sus" : "Liste en sus";
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Table d'historique : un produit par ligne, du plus recent au plus ancien.
|
|
101
|
+
*
|
|
102
|
+
* Peu de colonnes, pour tenir sans defilement horizontal : le fabricant, l'UCD
|
|
103
|
+
* et le type montent en pastilles dans la cellule du produit, et les dates de
|
|
104
|
+
* debut et de fin forment une seule periode.
|
|
105
|
+
*/
|
|
106
|
+
export function heartLesHistoryTable(
|
|
107
|
+
results: any[] | null,
|
|
108
|
+
locale: string,
|
|
109
|
+
companyHeader: string,
|
|
110
|
+
) {
|
|
111
|
+
const fields = heartLesAvailableFields(results);
|
|
112
|
+
const has = (field: string) => fields.some((f) => f.field === field);
|
|
113
|
+
const fr = locale === "fr";
|
|
114
|
+
const statusName = has("statut")
|
|
115
|
+
? shortStatusTitle(results?.find((a) => a.statut_title !== null)?.statut_title, fr)
|
|
116
|
+
: "";
|
|
117
|
+
|
|
118
|
+
const header: string[] = [fr ? "Produit" : "Product", "Source"];
|
|
119
|
+
const type: string[] = ["product", "source"];
|
|
120
|
+
const size: number[] = [4, 2];
|
|
121
|
+
const cell: ((row: any) => any)[] = [
|
|
122
|
+
(row) => ({
|
|
123
|
+
label: row.french_label,
|
|
124
|
+
ucd: row.ucd13_code ? row.ucd13_code : row.ucd7_code,
|
|
125
|
+
company: row.owner_regroup,
|
|
126
|
+
type: has("drug_type") ? row.drug_type : null,
|
|
127
|
+
}),
|
|
128
|
+
(row) => row.source,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
let periodDone = false;
|
|
132
|
+
for (const f of fields) {
|
|
133
|
+
// Deja dans la cellule produit.
|
|
134
|
+
if (f.field === "drug_type") continue;
|
|
135
|
+
// Debut et fin : une seule colonne « Periode ».
|
|
136
|
+
if (f.field === "start_date" || f.field === "end_date") {
|
|
137
|
+
if (periodDone) continue;
|
|
138
|
+
periodDone = true;
|
|
139
|
+
header.push(fr ? "Période" : "Period");
|
|
140
|
+
type.push("period");
|
|
141
|
+
size.push(3);
|
|
142
|
+
cell.push((row) => ({ start: row.start_date ?? null, end: row.end_date ?? null }));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
header.push(f.name === "Status" ? statusName : fr ? f.frName : f.name);
|
|
146
|
+
type.push(f.type);
|
|
147
|
+
size.push(f.field === "end_motive" ? 3 : 2);
|
|
148
|
+
cell.push((row) => row[f.field]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const data = (results ?? []).map((row: any) => ({ values: cell.map((get) => get(row)) }));
|
|
152
|
+
|
|
153
|
+
const time = (value: any) => (value ? new Date(value).getTime() || 0 : 0);
|
|
154
|
+
const periodIndex = type.indexOf("period");
|
|
155
|
+
const dateIndex = type.indexOf("date");
|
|
156
|
+
if (periodIndex >= 0) {
|
|
157
|
+
data.sort((a, b) => time(b.values[periodIndex]?.start) - time(a.values[periodIndex]?.start));
|
|
158
|
+
} else if (dateIndex >= 0) {
|
|
159
|
+
data.sort((a, b) => time(b.values[dateIndex]) - time(a.values[dateIndex]));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
header,
|
|
164
|
+
type,
|
|
165
|
+
style: { align: type.map(() => "left" as const), size },
|
|
166
|
+
data,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { heartSearchFieldGroup, heartSearchGroupByKey, heartSearchGroupLabel } from "./useHeartSearchFields";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pourquoi un resultat sort.
|
|
5
|
+
*
|
|
6
|
+
* L'explication vient d'Elasticsearch, pas d'une reconstitution cote client :
|
|
7
|
+
* chaque clause de recherche embarque des `inner_hits` nommes `search:<famille>`
|
|
8
|
+
* qui renvoient les avis matches et les extraits surlignes, et chaque filtre
|
|
9
|
+
* nested embarque les siens, nommes d'apres le filtre. Le moteur dit donc
|
|
10
|
+
* lui-meme OU il a trouve les termes et COMBIEN d'avis sont concernes — la ou
|
|
11
|
+
* une detection par expression reguliere ne pouvait qu'approcher son analyse
|
|
12
|
+
* (accents, pluriels, synonymes, proximite des mots).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type MatchFieldExplain = {
|
|
16
|
+
/** Cle de famille (`indication`, `essential`...). */
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
/** Onglet de la fiche produit qui porte ce contenu. */
|
|
20
|
+
tab: string | null;
|
|
21
|
+
/** Nombre d'avis de la marque ou la famille matche. */
|
|
22
|
+
count: number;
|
|
23
|
+
/** Extraits surlignes en `<mark>`, tels que renvoyes par le moteur, avec la date de l'avis d'ou ils viennent. */
|
|
24
|
+
fragments: { html: string; date: string | null; ref?: string | null }[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MatchExplain = {
|
|
28
|
+
/** Familles de champs ou les termes ont ete trouves, la plus fournie d'abord. */
|
|
29
|
+
fields: MatchFieldExplain[];
|
|
30
|
+
/** Filtres retenus par ce resultat, avec le nombre d'avis concernes. */
|
|
31
|
+
filters: { name: string; label: string; count: number }[];
|
|
32
|
+
/** Identifiants des avis matches, pour marquer les cartes de la fiche. */
|
|
33
|
+
opinionIds: string[];
|
|
34
|
+
hasCodes: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SEARCH_PREFIX = "search:";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Surlignage d'un champ `keyword` : faute de sous-champ analyse, le moteur
|
|
41
|
+
* traite la valeur comme un seul terme et la renvoie entierement enveloppee.
|
|
42
|
+
* L'annuaire des associations en donne l'exemple — chercher « keytruda »
|
|
43
|
+
* peignait en rose les quinze appellations de l'association d'un bloc. Un tel
|
|
44
|
+
* extrait ne montre rien : la famille garde sa pastille, l'extrait tombe.
|
|
45
|
+
*/
|
|
46
|
+
const WHOLE_VALUE = /^<mark>[^<]*<\/mark>$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Bloc `highlight` a poser a la racine d'une requete sur un index plat.
|
|
50
|
+
* Les balises sont demandees en `<mark>`, comme pour les inner hits : c'est la
|
|
51
|
+
* balise que la feuille du hub peint en rose.
|
|
52
|
+
*/
|
|
53
|
+
export function heartSearchHighlight(fields: string[]): Record<string, any> | undefined {
|
|
54
|
+
if (!fields.length) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
pre_tags: ["<mark>"],
|
|
57
|
+
post_tags: ["</mark>"],
|
|
58
|
+
fields: Object.fromEntries(
|
|
59
|
+
fields.map((field) => [field, { number_of_fragments: 2, fragment_size: 180 }]),
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bloc `highlight` depose par la barre de recherche dans le magasin de
|
|
66
|
+
* requetes. Une page le passe tel quel a `queryElastic`.
|
|
67
|
+
*/
|
|
68
|
+
export function heartHighlightFromQueries(queries: any[]): Record<string, any> | undefined {
|
|
69
|
+
return queries.find((query: any) => query?.highlight)?.highlight;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Filtres dont les inner hits ne disent rien a l'utilisateur. `searchType` est
|
|
74
|
+
* le perimetre de l'onglet courant, pose sur toutes les requetes : sans filtre
|
|
75
|
+
* choisi, chaque carte affichait « Correspondance dans 7 indications ».
|
|
76
|
+
* `patientContribution` est l'inner hit de ce meme perimetre sur l'onglet
|
|
77
|
+
* Contributions (cf. heartSearchType) : chaque carte affichait sinon
|
|
78
|
+
* « Filtres retenus patientContribution 3 ».
|
|
79
|
+
*/
|
|
80
|
+
const SILENT_FILTERS = new Set(["Take", "Sort", "Type", "Search Filter", "searchType", "patientContribution"]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Nom technique du filtre (`filterName` pose sur `HeartElastic*Filter`) vers
|
|
84
|
+
* son libelle affichable. Sans cette table, la pastille montre le nom de
|
|
85
|
+
* variable tel quel (« oncologyIcd10 ») plutot que ce que l'utilisateur a
|
|
86
|
+
* choisi dans le panneau de filtres.
|
|
87
|
+
*/
|
|
88
|
+
const FILTER_LABELS: Record<string, { fr: string; en: string }> = {
|
|
89
|
+
motif: { fr: "Motif", en: "Reason" },
|
|
90
|
+
brand: { fr: "Marque", en: "Brand" },
|
|
91
|
+
molecule: { fr: "Molécule", en: "Molecule" },
|
|
92
|
+
atc: { fr: "ACT4", en: "ACT4" },
|
|
93
|
+
ATC4: { fr: "ACT4", en: "ACT4" },
|
|
94
|
+
company: { fr: "Fabricant", en: "Manufacturer" },
|
|
95
|
+
smr: { fr: "SMR", en: "SMR" },
|
|
96
|
+
asmr: { fr: "ASMR", en: "ASMR" },
|
|
97
|
+
impact: { fr: "Impact CT", en: "CT Impact" },
|
|
98
|
+
biosimilar: { fr: "Biosimilaire", en: "Biosimilar" },
|
|
99
|
+
containsRare: { fr: "Maladies Rares", en: "Rare Diseases" },
|
|
100
|
+
rare: { fr: "Maladies Rares", en: "Rare Diseases" },
|
|
101
|
+
containsOnco: { fr: "Oncologie", en: "Oncology" },
|
|
102
|
+
oncologyTerms: { fr: "Oncology Terms", en: "Oncology Terms" },
|
|
103
|
+
oncologyIcd10: { fr: "Oncology ICD-10", en: "Oncology ICD-10" },
|
|
104
|
+
periodRange: { fr: "Période", en: "Period" },
|
|
105
|
+
association: { fr: "Association", en: "Association" },
|
|
106
|
+
parliamentarySession: { fr: "Session parlementaire", en: "Parliamentary session" },
|
|
107
|
+
opinion: { fr: "Avis", en: "Opinion" },
|
|
108
|
+
status: { fr: "Statut", en: "Status" },
|
|
109
|
+
target: { fr: "Cible", en: "Target" },
|
|
110
|
+
Distributions: { fr: "Distribution", en: "Distribution" },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function filterLabel(name: string, locale: string): string {
|
|
114
|
+
const known = FILTER_LABELS[name];
|
|
115
|
+
return known ? (locale === "en" ? known.en : known.fr) : name;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function fragmentsOf(innerHit: any): { html: string; date: string | null }[] {
|
|
119
|
+
return (innerHit?.hits?.hits ?? []).flatMap((hit: any) => {
|
|
120
|
+
const date = hit?._source?.decision_date ?? null;
|
|
121
|
+
return (Object.values(hit?.highlight ?? {}).flat() as string[]).map((html) => ({ html, date }));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function idsOf(innerHit: any) {
|
|
126
|
+
const ids: string[] = [];
|
|
127
|
+
const codes: string[] = [];
|
|
128
|
+
for (const hit of innerHit?.hits?.hits ?? []) {
|
|
129
|
+
const source = hit?._source ?? {};
|
|
130
|
+
if (source.opinion_id) ids.push(String(source.opinion_id));
|
|
131
|
+
if (source.has_code) codes.push(String(source.has_code));
|
|
132
|
+
}
|
|
133
|
+
return { ids, codes };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Lit les inner hits d'un resultat et en tire l'explication.
|
|
138
|
+
* Retourne `null` quand le resultat ne porte aucune explication — recherche
|
|
139
|
+
* vide, ou requete lancee sans inner hits.
|
|
140
|
+
*/
|
|
141
|
+
export function heartExplainMatch(hit: any, locale: string): MatchExplain | null {
|
|
142
|
+
const inner = hit?.inner_hits;
|
|
143
|
+
const highlight = hit?.highlight;
|
|
144
|
+
if ((!inner || typeof inner !== "object") && (!highlight || typeof highlight !== "object")) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const fields: MatchFieldExplain[] = [];
|
|
149
|
+
const filters: { name: string; label: string; count: number }[] = [];
|
|
150
|
+
const opinionIds = new Set<string>();
|
|
151
|
+
const hasCodes = new Set<string>();
|
|
152
|
+
|
|
153
|
+
for (const [name, innerHit] of Object.entries<any>(inner ?? {})) {
|
|
154
|
+
const count = innerHit?.hits?.total?.value ?? 0;
|
|
155
|
+
if (!count) continue;
|
|
156
|
+
|
|
157
|
+
if (name.startsWith(SEARCH_PREFIX)) {
|
|
158
|
+
const group = heartSearchGroupByKey(name.slice(SEARCH_PREFIX.length));
|
|
159
|
+
fields.push({
|
|
160
|
+
key: group.key,
|
|
161
|
+
label: heartSearchGroupLabel(group, locale),
|
|
162
|
+
tab: group.tab,
|
|
163
|
+
count,
|
|
164
|
+
fragments: fragmentsOf(innerHit),
|
|
165
|
+
});
|
|
166
|
+
const { ids, codes } = idsOf(innerHit);
|
|
167
|
+
ids.forEach((id) => opinionIds.add(id));
|
|
168
|
+
codes.forEach((code) => hasCodes.add(code));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!SILENT_FILTERS.has(name)) filters.push({ name, label: filterLabel(name, locale), count });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Highlight racine : un champ par cle, regroupe en famille comme les inner
|
|
176
|
+
// hits le sont deja. Sans decompte — la ligne matchee est le resultat
|
|
177
|
+
// lui-meme.
|
|
178
|
+
const byGroup = new Map<string, MatchFieldExplain>();
|
|
179
|
+
for (const [field, fragments] of Object.entries<any>(highlight ?? {})) {
|
|
180
|
+
const list = (Array.isArray(fragments) ? fragments : []).filter(
|
|
181
|
+
(html: any) => typeof html === "string" && html,
|
|
182
|
+
);
|
|
183
|
+
if (!list.length) continue;
|
|
184
|
+
const group = heartSearchFieldGroup(field);
|
|
185
|
+
if (!byGroup.has(group.key)) {
|
|
186
|
+
byGroup.set(group.key, {
|
|
187
|
+
key: group.key,
|
|
188
|
+
label: heartSearchGroupLabel(group, locale),
|
|
189
|
+
tab: group.tab,
|
|
190
|
+
count: 0,
|
|
191
|
+
fragments: [],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const entry = byGroup.get(group.key)!;
|
|
195
|
+
// Une meme famille couvre les deux langues et la variante sans accents du
|
|
196
|
+
// champ : le meme passage y revient a l'identique.
|
|
197
|
+
for (const html of list) {
|
|
198
|
+
if (WHOLE_VALUE.test(html)) continue;
|
|
199
|
+
if (!entry.fragments.some((fragment) => fragment.html === html)) {
|
|
200
|
+
entry.fragments.push({ html, date: null });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
fields.push(...byGroup.values());
|
|
205
|
+
|
|
206
|
+
if (!fields.length && !filters.length) return null;
|
|
207
|
+
|
|
208
|
+
fields.sort((a, b) => b.count - a.count || b.fragments.length - a.fragments.length);
|
|
209
|
+
filters.sort((a, b) => a.name.localeCompare(b.name));
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
fields,
|
|
213
|
+
filters,
|
|
214
|
+
opinionIds: [...opinionIds],
|
|
215
|
+
hasCodes: [...hasCodes],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Termes de recherche saisis pour la liste courante, pour les rappeler dans
|
|
221
|
+
* l'explication. fr_drugs_app les relisait dans le filterStore de sa page de
|
|
222
|
+
* recherche ; le layer n'a pas ce store : la fiche les recoit en prop
|
|
223
|
+
* (`highlight`) et les passe ici. Meme peigne que l'original : liste de
|
|
224
|
+
* termes, terme seul ou clause `{ value }`.
|
|
225
|
+
*/
|
|
226
|
+
export function heartCurrentSearchTerms(values: unknown): string[] {
|
|
227
|
+
const list = Array.isArray(values) ? values : values ? [values] : [];
|
|
228
|
+
return list
|
|
229
|
+
.map((value: any) => (value && typeof value === "object" ? value.value : value))
|
|
230
|
+
.filter((value: any) => typeof value === "string" && value.trim());
|
|
231
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// useHeartReports, la liste des rapports cures du rail : ceux livres avec
|
|
2
|
+
// le layer (public/reports) et ceux que Directus publie.
|
|
3
|
+
//
|
|
4
|
+
// Les artefacts Directus passent par /api/oip/reports : l'appel direct depuis le
|
|
5
|
+
// navigateur emportait le token dans le bundle et echouait en CORS, l'instance
|
|
6
|
+
// n'autorisant pas l'origine de l'app.
|
|
7
|
+
//
|
|
8
|
+
// Shares state between the sidebar and the /reports/[id] page via
|
|
9
|
+
// `useState` (Nuxt's app-level singleton). Re-fetches on every consumer
|
|
10
|
+
// mount when the cache is empty so a transient failure (e.g. Directus
|
|
11
|
+
// permission being toggled mid-edit) doesn't leave the sidebar section
|
|
12
|
+
// hidden until a full reload.
|
|
13
|
+
|
|
14
|
+
export type ReportCategory = 'report' | 'monograph';
|
|
15
|
+
|
|
16
|
+
export interface Report {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
html: string;
|
|
20
|
+
category: ReportCategory;
|
|
21
|
+
/**
|
|
22
|
+
* Nom du composant de `oip-drugs/monographs/` qui porte le document. Quand il est
|
|
23
|
+
* la, la page de rapport rend le document dans le DOM de l'app (comme
|
|
24
|
+
* l'onglet Monographie d'une fiche marque) au lieu d'une iframe, et le HTML
|
|
25
|
+
* de `public/reports/` n'est pas charge.
|
|
26
|
+
*/
|
|
27
|
+
component?: string;
|
|
28
|
+
/** PDF equivalent, quand il existe dans `public/reports/`. */
|
|
29
|
+
pdf?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Rapports servis en STATIQUE (HTML livré dans public/reports/) plutôt que
|
|
33
|
+
// via Directus (utile quand l'artefact Directus a été perdu/dépublié). Ils
|
|
34
|
+
// sont prioritaires sur Directus (dédup par id) et chargés même si
|
|
35
|
+
// l'intégration Directus n'est pas configurée.
|
|
36
|
+
interface StaticReport {
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
file: string; // chemin servi depuis public/, pour le repli en iframe
|
|
40
|
+
category: ReportCategory;
|
|
41
|
+
/** Composant de `oip-drugs/monographs/` a rendre a la place de l'iframe. */
|
|
42
|
+
component?: string;
|
|
43
|
+
/** PDF equivalent servi depuis public/reports/. */
|
|
44
|
+
pdf?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const STATIC_REPORTS: StaticReport[] = [
|
|
48
|
+
{
|
|
49
|
+
id: '79381b66-fd6b-41b8-8a0f-1a0acc37d69c',
|
|
50
|
+
title: 'Avis HAS, cancer du sein 2023-2026',
|
|
51
|
+
file: '/reports/has-sein-2023.html',
|
|
52
|
+
component: 'has-sein-2023',
|
|
53
|
+
category: 'report',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'monograph-blincyto',
|
|
57
|
+
title: 'Blincyto',
|
|
58
|
+
file: '/reports/blincyto-has.html',
|
|
59
|
+
component: 'blincyto-has',
|
|
60
|
+
pdf: '/reports/blincyto-has.pdf',
|
|
61
|
+
category: 'monograph',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'monograph-darzalex',
|
|
65
|
+
title: 'Darzalex',
|
|
66
|
+
file: '/reports/darzalex-has.html',
|
|
67
|
+
component: 'darzalex-has',
|
|
68
|
+
pdf: '/reports/darzalex-has.pdf',
|
|
69
|
+
category: 'monograph',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'monograph-dupixent',
|
|
73
|
+
title: 'Dupixent',
|
|
74
|
+
file: '/reports/dupixent-has.html',
|
|
75
|
+
component: 'dupixent-has',
|
|
76
|
+
pdf: '/reports/dupixent-has.pdf',
|
|
77
|
+
category: 'monograph',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'monograph-enhertu',
|
|
81
|
+
title: 'Enhertu',
|
|
82
|
+
file: '/reports/enhertu-has.html',
|
|
83
|
+
component: 'enhertu-has',
|
|
84
|
+
pdf: '/reports/enhertu-has.pdf',
|
|
85
|
+
category: 'monograph',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'monograph-ibrance',
|
|
89
|
+
title: 'Ibrance',
|
|
90
|
+
file: '/reports/ibrance-has.html',
|
|
91
|
+
component: 'ibrance-has',
|
|
92
|
+
pdf: '/reports/ibrance-has.pdf',
|
|
93
|
+
category: 'monograph',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'monograph-imfinzi',
|
|
97
|
+
title: 'Imfinzi',
|
|
98
|
+
file: '/reports/imfinzi-has.html',
|
|
99
|
+
component: 'imfinzi-has',
|
|
100
|
+
pdf: '/reports/imfinzi-has.pdf',
|
|
101
|
+
category: 'monograph',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'monograph-keytruda',
|
|
105
|
+
title: 'Keytruda',
|
|
106
|
+
file: '/reports/keytruda-has.html',
|
|
107
|
+
component: 'keytruda-has',
|
|
108
|
+
pdf: '/reports/keytruda-has.pdf',
|
|
109
|
+
category: 'monograph',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'monograph-kisqali',
|
|
113
|
+
title: 'Kisqali',
|
|
114
|
+
file: '/reports/kisqali-has.html',
|
|
115
|
+
component: 'kisqali-has',
|
|
116
|
+
pdf: '/reports/kisqali-has.pdf',
|
|
117
|
+
category: 'monograph',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'monograph-opdivo',
|
|
121
|
+
title: 'Opdivo',
|
|
122
|
+
file: '/reports/opdivo-has.html',
|
|
123
|
+
component: 'opdivo-has',
|
|
124
|
+
pdf: '/reports/opdivo-has.pdf',
|
|
125
|
+
category: 'monograph',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'monograph-ozempic',
|
|
129
|
+
title: 'Ozempic',
|
|
130
|
+
file: '/reports/ozempic-has.html',
|
|
131
|
+
component: 'ozempic-has',
|
|
132
|
+
pdf: '/reports/ozempic-has.pdf',
|
|
133
|
+
category: 'monograph',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'monograph-tecentriq',
|
|
137
|
+
title: 'Tecentriq',
|
|
138
|
+
file: '/reports/tecentriq-has.html',
|
|
139
|
+
component: 'tecentriq-has',
|
|
140
|
+
pdf: '/reports/tecentriq-has.pdf',
|
|
141
|
+
category: 'monograph',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'monograph-yescarta',
|
|
145
|
+
title: 'Yescarta',
|
|
146
|
+
file: '/reports/yescarta-has.html',
|
|
147
|
+
component: 'yescarta-has',
|
|
148
|
+
pdf: '/reports/yescarta-has.pdf',
|
|
149
|
+
category: 'monograph',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
async function fetchStaticReports(): Promise<Report[]> {
|
|
154
|
+
const results = await Promise.all(
|
|
155
|
+
STATIC_REPORTS.map(async (s): Promise<Report | null> => {
|
|
156
|
+
// Document porte par un composant : rien a telecharger, la page le rend
|
|
157
|
+
// depuis `oip-drugs/monographs/`. Evite treize requetes HTML au demarrage.
|
|
158
|
+
if (s.component) {
|
|
159
|
+
return {
|
|
160
|
+
id: s.id,
|
|
161
|
+
title: s.title,
|
|
162
|
+
html: '',
|
|
163
|
+
category: s.category,
|
|
164
|
+
component: s.component,
|
|
165
|
+
pdf: s.pdf ?? null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch(s.file);
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
console.warn(`[useHeartReports] static ${s.file} HTTP ${res.status}`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return { id: s.id, title: s.title, html: await res.text(), category: s.category };
|
|
175
|
+
} catch (err: any) {
|
|
176
|
+
console.warn(`[useHeartReports] static ${s.file} failed:`, err?.message || err);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
return results.filter((r): r is Report => r !== null);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function useHeartReports() {
|
|
185
|
+
const reports = useState<Report[]>('oip-drugs:reports', () => []);
|
|
186
|
+
const loading = useState<boolean>('oip-drugs:reports:loading', () => false);
|
|
187
|
+
// Flips to true the first time a fetch attempt completes (success or
|
|
188
|
+
// failure). The detail page uses it to decide skeleton vs. "not
|
|
189
|
+
// found": without it the user would see a flash of "not found"
|
|
190
|
+
// before the very first fetch resolves.
|
|
191
|
+
const initialized = useState<boolean>('oip-drugs:reports:initialized', () => false);
|
|
192
|
+
|
|
193
|
+
async function fetchReports(force = false) {
|
|
194
|
+
if (loading.value) return;
|
|
195
|
+
// Skip the network call when we already have a populated cache
|
|
196
|
+
// unless the caller explicitly asked for a refresh.
|
|
197
|
+
if (!force && reports.value.length > 0) {
|
|
198
|
+
initialized.value = true;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
loading.value = true;
|
|
203
|
+
try {
|
|
204
|
+
// Rapports statiques (toujours), + Directus si configuré. Les statiques
|
|
205
|
+
// sont prioritaires : leur id est retiré de la demande, l'artefact
|
|
206
|
+
// Directus du même rapport étant souvent perdu ou dépublié.
|
|
207
|
+
const statics = await fetchStaticReports();
|
|
208
|
+
const staticIds = statics.map((s) => s.id);
|
|
209
|
+
const directus = await $fetch<Report[]>('/api/oip/reports', {
|
|
210
|
+
query: { skip: staticIds.join(',') },
|
|
211
|
+
}).catch((err: any) => {
|
|
212
|
+
console.warn('[useHeartReports] /api/oip/reports failed:', err?.message || err);
|
|
213
|
+
return [] as Report[];
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
reports.value = [...statics, ...directus];
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.warn('[useHeartReports] fetch failed', err);
|
|
219
|
+
// Keep whatever we had, don't wipe a working cache because of a
|
|
220
|
+
// transient network blip.
|
|
221
|
+
} finally {
|
|
222
|
+
loading.value = false;
|
|
223
|
+
initialized.value = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function findById(id: string): Report | null {
|
|
228
|
+
if (!id) return null;
|
|
229
|
+
return reports.value.find((r) => r.id === id) || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
reports,
|
|
234
|
+
loaded: initialized,
|
|
235
|
+
loading,
|
|
236
|
+
fetchReports,
|
|
237
|
+
findById,
|
|
238
|
+
};
|
|
239
|
+
}
|