@tekkare/romulus 0.1.251 → 0.1.252
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/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,1111 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Fiche marque HAS (portage de pages/hubr/heart/details/[id].vue de
|
|
3
|
+
fr_drugs_app).
|
|
4
|
+
|
|
5
|
+
Ce que la page lisait dans sa route arrive en props (`brand`, `tab`, `cis`,
|
|
6
|
+
`code`, `indication`) ; l'onglet choisi part par `update:tab` au lieu
|
|
7
|
+
d'ecrire `?tab=`. L'export et le bouton Projet restent dans la page de
|
|
8
|
+
fr_drugs_app, qui les pose dans le slot `actions` (le slot recoit les donnees
|
|
9
|
+
de la fiche) ; dans le tiroir d'un projet, rien n'y est passe.
|
|
10
|
+
-->
|
|
11
|
+
<template>
|
|
12
|
+
<div class="oip-drugs arg_page_layout">
|
|
13
|
+
<RmPageHeader
|
|
14
|
+
:have-return="haveReturn"
|
|
15
|
+
:tabs="headerTabs"
|
|
16
|
+
:tabs-label="locale === 'en' ? 'Record sections' : 'Sections de la fiche'"
|
|
17
|
+
@tab="selectTab"
|
|
18
|
+
>
|
|
19
|
+
<template #title>
|
|
20
|
+
<span class="oip-drugs-brand__title">{{ title }}</span>
|
|
21
|
+
<slot name="title-link" />
|
|
22
|
+
</template>
|
|
23
|
+
<template v-if="$slots.actions" #actions>
|
|
24
|
+
<slot
|
|
25
|
+
name="actions"
|
|
26
|
+
:product="resultsQuery"
|
|
27
|
+
:brand-id="brandId"
|
|
28
|
+
:brand="title"
|
|
29
|
+
:tab="currentTabKey"
|
|
30
|
+
:tab-label="currentTabLabel"
|
|
31
|
+
:tabs="availableTabs"
|
|
32
|
+
:prices="resultsQueryPrice"
|
|
33
|
+
:intl-prices="resultsQueryIntlPrice"
|
|
34
|
+
:intl-records="intlRecords"
|
|
35
|
+
:ansm="resultsQueryAnsm"
|
|
36
|
+
:mitm="resultsQueryMitm"
|
|
37
|
+
:put="resultsQueryPut"
|
|
38
|
+
:les="resultsQueryLes"
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
41
|
+
</RmPageHeader>
|
|
42
|
+
|
|
43
|
+
<RmLoadingState v-if="showLoading" class="oip-drugs-brand__state" :lines="6" />
|
|
44
|
+
<RmEmptyState
|
|
45
|
+
v-else-if="showEmpty"
|
|
46
|
+
class="oip-drugs-brand__state"
|
|
47
|
+
icon="Pill"
|
|
48
|
+
:title="locale === 'en' ? 'No record for this brand' : 'Aucune fiche pour cette marque'"
|
|
49
|
+
:text="brandMatch"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
v-else-if="chosenTab === monographTabIndex && monographId"
|
|
54
|
+
class="arg_page_layout mono-bleed"
|
|
55
|
+
>
|
|
56
|
+
<OipDrugsHeartResultsDetailsMonograph :id="monographId!" :pdf="monographPdf" />
|
|
57
|
+
</div>
|
|
58
|
+
<div v-else-if="chosenTab === overviewTabIndex" class="arg_page_layout">
|
|
59
|
+
<!-- Historique complet en cartes, groupees par annee ; le detail d'un
|
|
60
|
+
avis s'ouvre dans un tiroir plutot que sous la liste. -->
|
|
61
|
+
<OipDrugsHeartResultsDetailsHasHistory
|
|
62
|
+
v-if="resultsQuery !== null"
|
|
63
|
+
:product="resultsQuery"
|
|
64
|
+
:matched-codes="brandMatches.matchedCodes.value"
|
|
65
|
+
:search-terms="searchTerms"
|
|
66
|
+
@select="drawerOpinion = $event"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<!-- Le tiroir a son propre etat : il ne s'ouvre que sur un avis choisi
|
|
70
|
+
dans la liste, jamais des l'arrivee sur l'onglet. -->
|
|
71
|
+
<OipDrugsHeartResultsDetailsOpinionDrawer
|
|
72
|
+
:opinion="drawerOpinion"
|
|
73
|
+
:checked-at="hasCheckedAt"
|
|
74
|
+
:search-terms="searchTerms"
|
|
75
|
+
@close="drawerOpinion = null"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
<div v-else-if="chosenTab === alertTabIndex && alertTabIndex > 0" class="arg_page_layout">
|
|
79
|
+
<OipDrugsHeartResultsDetailsRecordList
|
|
80
|
+
:title="locale === 'en' ? 'Safety alerts' : 'Alertes de sécurité'"
|
|
81
|
+
:subtitle="locale === 'en' ? 'alerts recorded for the brand' : 'alertes recensées pour la marque'"
|
|
82
|
+
:items="alertRecords"
|
|
83
|
+
:empty-label="locale === 'en' ? 'No safety alerts for this product.' : 'Aucune alerte de sécurité pour ce produit.'"
|
|
84
|
+
@select="drawerRecord = $event"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
<div v-else-if="chosenTab === priceTabIndex && priceTabIndex > 0" class="arg_page_layout">
|
|
88
|
+
<OipDrugsHeartResultsDetailsPriceOverview
|
|
89
|
+
:product="resultsQueryPrice"
|
|
90
|
+
:brand="title"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div
|
|
94
|
+
v-else-if="chosenTab === internationalTabIndex && internationalTabIndex > 0"
|
|
95
|
+
class="arg_page_layout"
|
|
96
|
+
>
|
|
97
|
+
<OipDrugsHeartResultsDetailsInternationalOverview
|
|
98
|
+
:docs="resultsQueryIntlPrice"
|
|
99
|
+
:records="intlRecords"
|
|
100
|
+
:loading="intlLoading"
|
|
101
|
+
:brand="title"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
<div v-else-if="chosenTab === ansmTabIndex && ansmTabIndex > 0" class="arg_page_layout">
|
|
105
|
+
<div v-if="resultsQueryAnsm.length > 1" class="cis-switch">
|
|
106
|
+
<span class="oip_filter_label cis-switch-label">
|
|
107
|
+
{{ locale === 'en' ? 'Specialty (CIS):' : 'Spécialité (CIS) :' }}
|
|
108
|
+
</span>
|
|
109
|
+
<button
|
|
110
|
+
v-for="(h, i) in resultsQueryAnsm"
|
|
111
|
+
:key="h._id"
|
|
112
|
+
type="button"
|
|
113
|
+
class="cis-chip"
|
|
114
|
+
:class="{ active: i === selectedAnsmIndex }"
|
|
115
|
+
@click="selectedAnsmIndex = i"
|
|
116
|
+
>
|
|
117
|
+
{{ heartStripBrandPrefix(h._source.cis_label, brandMatch) }}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
<OipDrugsHeartAnsmCisDetail
|
|
121
|
+
v-if="selectedAnsmCis"
|
|
122
|
+
:cis="selectedAnsmCis"
|
|
123
|
+
:sku-count="resultsQueryAnsm.length"
|
|
124
|
+
:avis-count="overviewHistory.length"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<div v-else-if="chosenTab === mitmTabIndex && mitmTabIndex > 0" class="arg_page_layout">
|
|
128
|
+
<div v-if="resultsQueryMitm.length > 1" class="cis-switch">
|
|
129
|
+
<span class="oip_filter_label cis-switch-label">
|
|
130
|
+
{{ locale === 'en' ? 'Specialty (CIS):' : 'Spécialité (CIS) :' }}
|
|
131
|
+
</span>
|
|
132
|
+
<button
|
|
133
|
+
v-for="(m, i) in resultsQueryMitm"
|
|
134
|
+
:key="m._id"
|
|
135
|
+
type="button"
|
|
136
|
+
class="cis-chip"
|
|
137
|
+
:class="{ active: i === selectedMitmIndex }"
|
|
138
|
+
@click="selectedMitmIndex = i"
|
|
139
|
+
>
|
|
140
|
+
{{ heartStripBrandPrefix(m._source.cis_label, brandMatch) }}
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
<OipDrugsHeartMitmDetailsOverview
|
|
144
|
+
v-if="selectedMitmCis"
|
|
145
|
+
:cis="selectedMitmCis"
|
|
146
|
+
@select="drawerRecord = $event"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
<div v-else-if="chosenTab === putTabIndex && putTabIndex > 0" class="arg_page_layout">
|
|
150
|
+
<OipDrugsHeartResultsDetailsRecordList
|
|
151
|
+
:title="locale === 'en' ? 'Temporary use protocols' : 'Protocoles d’utilisation thérapeutique'"
|
|
152
|
+
:subtitle="locale === 'en' ? 'protocols recorded for the brand' : 'protocoles recensés pour la marque'"
|
|
153
|
+
:items="putRecords"
|
|
154
|
+
:empty-label="locale === 'en' ? 'No protocol for this product.' : 'Aucun protocole pour ce produit.'"
|
|
155
|
+
@select="drawerRecord = $event"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
<div v-else-if="chosenTab === lesTabIndex && lesTabIndex > 0" class="arg_page_layout">
|
|
159
|
+
<OipDrugsHeartResultsDetailsRecordList
|
|
160
|
+
:title="locale === 'en' ? 'Liste en sus indications' : 'Indications en liste en sus'"
|
|
161
|
+
:subtitle="locale === 'en' ? 'indications recorded for the brand' : 'indications recensées pour la marque'"
|
|
162
|
+
:items="lesRecords"
|
|
163
|
+
:empty-label="locale === 'en' ? 'No liste en sus indication for this product.' : 'Aucune indication en liste en sus pour ce produit.'"
|
|
164
|
+
@select="drawerLes = $event"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
<div
|
|
168
|
+
v-else-if="chosenTab === contributionsTabIndex && contributionsTabIndex > 0"
|
|
169
|
+
class="arg_page_layout"
|
|
170
|
+
>
|
|
171
|
+
<OipDrugsHeartResultsDetailsRecordList
|
|
172
|
+
:title="locale === 'en' ? 'Patient contributions' : 'Contributions patients'"
|
|
173
|
+
:subtitle="locale === 'en' ? 'contributions recorded for the brand' : 'contributions recensées pour la marque'"
|
|
174
|
+
:items="contributionRecords"
|
|
175
|
+
:empty-label="locale === 'en' ? 'No patient contribution.' : 'Aucune contribution patient.'"
|
|
176
|
+
@select="drawerRecord = $event"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Tiroir commun aux onglets Alertes, PUT et Contributions : un seul est
|
|
181
|
+
visible a la fois, il suit l'enregistrement choisi. -->
|
|
182
|
+
<OipDrugsHeartResultsDetailsRecordDrawer
|
|
183
|
+
:record="drawerRecord"
|
|
184
|
+
:source-label="drawerRecord?.sourceLabel"
|
|
185
|
+
:checked-at="hasCheckedAt"
|
|
186
|
+
@close="drawerRecord = null"
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
<!-- Tiroir de l'onglet LES : le detail de l'indication, tel que le montre
|
|
190
|
+
la page indication/[id]. -->
|
|
191
|
+
<OipDrugsHeartResultsLesDrawer
|
|
192
|
+
:code="drawerLes?.code ?? null"
|
|
193
|
+
:title="drawerLes?.title"
|
|
194
|
+
:date="drawerLes?.date"
|
|
195
|
+
@close="drawerLes = null"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
200
|
+
<script setup lang="ts">
|
|
201
|
+
import { ES_INDEX } from "../../../oip-drugs/esIndices";
|
|
202
|
+
import useOipDrugsElastic from "../../../oip-drugs/composables/useOipDrugsElastic";
|
|
203
|
+
import useDirectusSources from "../../../oip-drugs/composables/useDirectusSources";
|
|
204
|
+
import { useAssociationLookup } from "../../../oip-drugs/composables/useAssociationLookup";
|
|
205
|
+
import useHeartIntlApprovals from "../../../oip-drugs/composables/useHeartIntlApprovals";
|
|
206
|
+
import useHeartBrandMatches from "../../../oip-drugs/composables/useHeartBrandMatches";
|
|
207
|
+
import { HEART_LES_INDEX } from "../../../oip-drugs/composables/useHeartLesIndication";
|
|
208
|
+
import { HEART_SOURCE_UUIDS } from "../../../oip-drugs/utils/sourceUuids";
|
|
209
|
+
import { HEART_INTL_PRICE_COUNTRIES } from "../../../oip-drugs/utils/heartIntlPrice";
|
|
210
|
+
import { heartFixMojibake } from "../../../oip-drugs/utils/heartStyleAndFormat";
|
|
211
|
+
import { heartStripBrandPrefix } from "../../../oip-drugs/utils/heartStyleAnsm";
|
|
212
|
+
import { drugsAssociationPath } from "../../../oip-drugs/kinds";
|
|
213
|
+
import { OIP_RESOURCE_NAV } from "../../../composables/useOipResourceNav";
|
|
214
|
+
|
|
215
|
+
const props = withDefaults(
|
|
216
|
+
defineProps<{
|
|
217
|
+
/** La marque (brand_group HAS), ex `route.params.id`. */
|
|
218
|
+
brand: string;
|
|
219
|
+
/** Cle d'onglet demandee (ex `?tab=`), alias francais acceptes. */
|
|
220
|
+
tab?: string;
|
|
221
|
+
/** CIS preselectionne dans ANSM (`_id`) ou MITM (`cis_code`), ex `?cis=`. */
|
|
222
|
+
cis?: string;
|
|
223
|
+
/** Code HAS (ex `?code=`). Garde pour le contrat : rien ne le relisait. */
|
|
224
|
+
code?: string;
|
|
225
|
+
/** Indication LES a ouvrir dans son tiroir (avec `tab=les`), ex `?indication=`. */
|
|
226
|
+
indication?: string;
|
|
227
|
+
lang?: "fr" | "en";
|
|
228
|
+
/** Termes de recherche a surligner. */
|
|
229
|
+
highlight?: string[];
|
|
230
|
+
/** Fleche de retour du bandeau (page de fr_drugs_app). */
|
|
231
|
+
haveReturn?: boolean;
|
|
232
|
+
}>(),
|
|
233
|
+
{
|
|
234
|
+
tab: "",
|
|
235
|
+
cis: "",
|
|
236
|
+
code: "",
|
|
237
|
+
indication: "",
|
|
238
|
+
lang: undefined,
|
|
239
|
+
highlight: () => [],
|
|
240
|
+
haveReturn: false,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const emit = defineEmits<{ "update:tab": [key: string] }>();
|
|
245
|
+
|
|
246
|
+
const { queryCustomElastic } = useOipDrugsElastic();
|
|
247
|
+
const { locale: i18nLocale, t } = useI18n();
|
|
248
|
+
const locale = computed<"fr" | "en">(() => props.lang ?? (i18nLocale.value === "en" ? "en" : "fr"));
|
|
249
|
+
|
|
250
|
+
const resultsQuery = ref(null as any);
|
|
251
|
+
/** `_id` ES de la marque, ce que le bouton « Projet » range. */
|
|
252
|
+
const brandId = ref<string | null>(null);
|
|
253
|
+
const resultsQueryAnsm = ref<Array<{ _id: string; _source: any }>>([]);
|
|
254
|
+
const resultsQueryMitm = ref<Array<{ _id: string; _source: any }>>([]);
|
|
255
|
+
const resultsQueryPut = ref<Array<{ _id: string; _source: any }>>([]);
|
|
256
|
+
const resultsQueryLes = ref<any[]>([]);
|
|
257
|
+
const indexQuery = ES_INDEX.BRAND_HAS;
|
|
258
|
+
const isLoadingQuery = ref(true);
|
|
259
|
+
/** Les sections secondaires (prix, ANSM, MITM...) sont-elles encore en route ? */
|
|
260
|
+
const isLoadingSecondary = ref(true);
|
|
261
|
+
|
|
262
|
+
// La marque telle que le lien la porte (cf. drugsBrandPath). Les liens
|
|
263
|
+
// partages avant heartBrandPath ecrivaient « / » en « + » : `lanchQuery` se
|
|
264
|
+
// replie sur cette ecriture si la HAS ne connait pas la marque telle quelle.
|
|
265
|
+
const brandMatch = ref(String(props.brand ?? ""));
|
|
266
|
+
|
|
267
|
+
const title = computed(() => resultsQuery.value?.brand_group || brandMatch.value);
|
|
268
|
+
|
|
269
|
+
const hasAlerts = computed(() => {
|
|
270
|
+
return resultsQuery.value?.brand_history?.some(
|
|
271
|
+
(h: any) => h.document_type === 'alert' && h.alert_details?.length > 0
|
|
272
|
+
) ?? false;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const alertHistory = computed(() => {
|
|
276
|
+
if (!resultsQuery.value?.brand_history) return [];
|
|
277
|
+
return resultsQuery.value.brand_history
|
|
278
|
+
.filter((h: any) => h.document_type === 'alert' && h.alert_details?.length > 0)
|
|
279
|
+
.sort((a: any, b: any) =>
|
|
280
|
+
new Date(b.security_end_date).getTime() - new Date(a.security_end_date).getTime()
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const overviewHistory = computed(() => {
|
|
285
|
+
if (!resultsQuery.value?.brand_history) return [];
|
|
286
|
+
return resultsQuery.value.brand_history.filter(
|
|
287
|
+
(h: any) => h.document_type !== 'alert'
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const hasContributions = computed(
|
|
292
|
+
() => (resultsQuery.value?.patient_contribution?.length ?? 0) > 0
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Monographie : si le produit dispose d'une note d'intelligence HAS, on ajoute
|
|
296
|
+
// un onglet "Monographie" EN PREMIER.
|
|
297
|
+
const MONOGRAPH_BRANDS = [
|
|
298
|
+
'blincyto', 'darzalex', 'dupixent', 'enhertu', 'ibrance', 'imfinzi', 'keytruda',
|
|
299
|
+
'kisqali', 'opdivo', 'ozempic', 'tecentriq', 'yescarta',
|
|
300
|
+
];
|
|
301
|
+
const monographId = computed(() => {
|
|
302
|
+
const bg = (resultsQuery.value?.brand_group || '').toLowerCase();
|
|
303
|
+
const brand = MONOGRAPH_BRANDS.find((b) => bg.includes(b));
|
|
304
|
+
return brand ? `${brand}-has` : null;
|
|
305
|
+
});
|
|
306
|
+
const hasMonograph = computed(() => monographId.value !== null);
|
|
307
|
+
const monographPdf = computed(() =>
|
|
308
|
+
monographId.value ? `/reports/${monographId.value}.pdf` : null
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Prix France, prix internationaux et registres etrangers : declares avant la
|
|
312
|
+
// liste des onglets, qui les lit.
|
|
313
|
+
const resultsQueryPrice = ref<any[]>([]);
|
|
314
|
+
const resultsQueryIntlPrice = ref<any[]>([]);
|
|
315
|
+
const { records: intlRecords, isLoading: intlLoading, load: loadIntlApprovals } =
|
|
316
|
+
useHeartIntlApprovals();
|
|
317
|
+
const intlDecisionCount = computed(() =>
|
|
318
|
+
Object.values(intlRecords.value as Record<string, any[]>).reduce((sum, list) => sum + list.length, 0),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Onglets disponibles (l'ordre = l'ordre d'affichage). La MARQUE est le pivot :
|
|
322
|
+
// HAS n'apparait que s'il existe un avis, ANSM que s'il existe des CIS, etc.
|
|
323
|
+
const availableTabs = computed(() => {
|
|
324
|
+
const en = locale.value === 'en';
|
|
325
|
+
const tabs: { key: string; label: string }[] = [];
|
|
326
|
+
if (hasMonograph.value) tabs.push({ key: 'monograph', label: en ? 'Monograph' : 'Monographie' });
|
|
327
|
+
if (resultsQuery.value) tabs.push({ key: 'overview', label: 'HAS' });
|
|
328
|
+
if (hasContributions.value) tabs.push({ key: 'contributions', label: en ? 'Patient Contribution' : 'Contribution Patient' });
|
|
329
|
+
if (resultsQueryPrice.value.length > 0) tabs.push({ key: 'price', label: t('oipDrugs.heart.details.tabs.price') });
|
|
330
|
+
if (resultsQueryIntlPrice.value.length > 0 || intlDecisionCount.value > 0) tabs.push({ key: 'international', label: en ? 'International view' : 'Regard international' });
|
|
331
|
+
if (resultsQueryAnsm.value.length > 0) tabs.push({ key: 'ansm', label: 'ANSM' });
|
|
332
|
+
if (resultsQueryMitm.value.length > 0) tabs.push({ key: 'mitm', label: 'MITM' });
|
|
333
|
+
if (resultsQueryPut.value.length > 0) tabs.push({ key: 'put', label: 'PUT' });
|
|
334
|
+
if (resultsQueryLes.value.length > 0) tabs.push({ key: 'les', label: 'LES' });
|
|
335
|
+
// Les alertes ferment la liste : elles completent la fiche, elles ne la
|
|
336
|
+
// presentent pas.
|
|
337
|
+
if (hasAlerts.value) tabs.push({ key: 'alerts', label: en ? 'Safety Alerts' : 'Alertes' });
|
|
338
|
+
return tabs;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const chosenTab = ref(1);
|
|
342
|
+
|
|
343
|
+
// Onglets du bandeau : RmPageHeader travaille sur des cles, la fiche garde son
|
|
344
|
+
// indice a partir de 1 pour choisir le bloc rendu.
|
|
345
|
+
const headerTabs = computed(() =>
|
|
346
|
+
availableTabs.value.map((tb, index) => ({
|
|
347
|
+
key: tb.key,
|
|
348
|
+
label: tb.label,
|
|
349
|
+
active: chosenTab.value === index + 1,
|
|
350
|
+
})),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
function selectTab(key: string) {
|
|
354
|
+
const index = availableTabs.value.findIndex((tb) => tb.key === key);
|
|
355
|
+
if (index >= 0) chosenTab.value = index + 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Ecritures francaises acceptees dans `tab`, pour les liens partages. */
|
|
359
|
+
const TAB_ALIASES: Record<string, string> = {
|
|
360
|
+
prix: 'price',
|
|
361
|
+
alertes: 'alerts',
|
|
362
|
+
monographie: 'monograph',
|
|
363
|
+
contributions_patients: 'contributions',
|
|
364
|
+
avis: 'overview',
|
|
365
|
+
has: 'overview',
|
|
366
|
+
liste_en_sus: 'les',
|
|
367
|
+
regard_international: 'international',
|
|
368
|
+
prix_internationaux: 'international',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const normalizeTab = (rawKey: string) => TAB_ALIASES[String(rawKey).toLowerCase()] ?? rawKey;
|
|
372
|
+
|
|
373
|
+
// Les onglets sont numerotes a partir de 1 (le 1er onglet = 1) ; 0 = onglet absent.
|
|
374
|
+
const tabIndexOf = (rawKey: string) => {
|
|
375
|
+
const key = normalizeTab(rawKey);
|
|
376
|
+
const i = availableTabs.value.findIndex((tb) => tb.key === key);
|
|
377
|
+
return i === -1 ? 0 : i + 1;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const monographTabIndex = computed(() => tabIndexOf('monograph'));
|
|
381
|
+
const overviewTabIndex = computed(() => tabIndexOf('overview'));
|
|
382
|
+
const contributionsTabIndex = computed(() => tabIndexOf('contributions'));
|
|
383
|
+
const alertTabIndex = computed(() => tabIndexOf('alerts'));
|
|
384
|
+
const priceTabIndex = computed(() => tabIndexOf('price'));
|
|
385
|
+
const internationalTabIndex = computed(() => tabIndexOf('international'));
|
|
386
|
+
const ansmTabIndex = computed(() => tabIndexOf('ansm'));
|
|
387
|
+
const mitmTabIndex = computed(() => tabIndexOf('mitm'));
|
|
388
|
+
const putTabIndex = computed(() => tabIndexOf('put'));
|
|
389
|
+
const lesTabIndex = computed(() => tabIndexOf('les'));
|
|
390
|
+
|
|
391
|
+
/** L'onglet ouvert, par sa cle et son libelle. */
|
|
392
|
+
const currentTabKey = computed(() => availableTabs.value[chosenTab.value - 1]?.key ?? null);
|
|
393
|
+
const currentTabLabel = computed(() => availableTabs.value[chosenTab.value - 1]?.label ?? "");
|
|
394
|
+
|
|
395
|
+
/** Chargement : tant que la HAS n'a pas repondu, ou qu'aucun onglet n'est encore la. */
|
|
396
|
+
const showLoading = computed(
|
|
397
|
+
() => isLoadingQuery.value || (isLoadingSecondary.value && availableTabs.value.length === 0),
|
|
398
|
+
);
|
|
399
|
+
/** Aucune section pour cette marque, une fois tout charge. */
|
|
400
|
+
const showEmpty = computed(() => !showLoading.value && availableTabs.value.length === 0);
|
|
401
|
+
|
|
402
|
+
// CIS selectionne dans l'onglet ANSM (selecteur de specialites de la marque).
|
|
403
|
+
const selectedAnsmIndex = ref(0);
|
|
404
|
+
const selectedAnsmCis = computed(
|
|
405
|
+
() => resultsQueryAnsm.value[selectedAnsmIndex.value]?._source ?? null
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// CIS selectionne dans l'onglet MITM.
|
|
409
|
+
const selectedMitmIndex = ref(0);
|
|
410
|
+
const selectedMitmCis = computed(
|
|
411
|
+
() => resultsQueryMitm.value[selectedMitmIndex.value]?._source ?? null
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
/** Avis ouvert dans le tiroir de l'onglet HAS. */
|
|
415
|
+
const drawerOpinion = ref<any>(null);
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Libelles et dates de source des tiroirs : Directus, comme le panneau
|
|
419
|
+
* « Sources » du rail.
|
|
420
|
+
*/
|
|
421
|
+
const { load: loadSourceMeta, metaByUuid, verifiedRaw } = useDirectusSources();
|
|
422
|
+
loadSourceMeta(Object.values(HEART_SOURCE_UUIDS));
|
|
423
|
+
const sourceName = (uuid: string) => metaByUuid(uuid, locale.value).name;
|
|
424
|
+
|
|
425
|
+
/** Date de derniere verification de la source HAS, montree en pied de tiroir. */
|
|
426
|
+
const hasCheckedAt = computed(() => verifiedRaw(HEART_SOURCE_UUIDS.HAS));
|
|
427
|
+
|
|
428
|
+
/** Enregistrement ouvert dans le tiroir des onglets Alertes, PUT et Contributions. */
|
|
429
|
+
const drawerRecord = ref<any>(null);
|
|
430
|
+
|
|
431
|
+
/** Indication LES ouverte dans son tiroir (code, titre, date de la liste). */
|
|
432
|
+
const drawerLes = ref<any>(null);
|
|
433
|
+
|
|
434
|
+
/** Premiere valeur remplie ; a defaut de traduction on garde le francais. */
|
|
435
|
+
function pickText(...values: any[]) {
|
|
436
|
+
for (const value of values) {
|
|
437
|
+
if (typeof value === "string" && value.trim()) return heartFixMojibake(value.trim());
|
|
438
|
+
}
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Une alerte par code CIS, comme le detail d'alerte le faisait deja. */
|
|
443
|
+
const alertRecords = computed(() =>
|
|
444
|
+
alertHistory.value.flatMap((entry: any, entryIndex: number) => {
|
|
445
|
+
const codes = [...new Set((entry.alert_details ?? []).map((a: any) => a.cis_code))];
|
|
446
|
+
return codes.map((code) => {
|
|
447
|
+
const details = (entry.alert_details ?? []).filter((a: any) => a.cis_code === code);
|
|
448
|
+
const first = details[0] ?? {};
|
|
449
|
+
const message = pickText(
|
|
450
|
+
locale.value === "en" ? first.display_message_en : first.display_message,
|
|
451
|
+
first.display_message,
|
|
452
|
+
first.display_message_en
|
|
453
|
+
);
|
|
454
|
+
const brands = [...new Set(details.map((a: any) => a.brand).filter(Boolean))].join(", ");
|
|
455
|
+
return {
|
|
456
|
+
// Deux alertes d'un meme avis peuvent viser le meme CIS : l'indice
|
|
457
|
+
// de l'entree garde la cle unique.
|
|
458
|
+
key: `${entry.has_code || entry.opinion_id || ""}-${entryIndex}-${code}`,
|
|
459
|
+
typeLabel: locale.value === "en" ? "Safety alert" : "Alerte de sécurité",
|
|
460
|
+
icon: "Warning",
|
|
461
|
+
date: entry.security_end_date || null,
|
|
462
|
+
ref: code ? `CIS ${code}` : null,
|
|
463
|
+
url: first.url_link || null,
|
|
464
|
+
title: message || (locale.value === "en" ? "Safety alert" : "Alerte de sécurité"),
|
|
465
|
+
excerpt: brands || null,
|
|
466
|
+
sourceLabel: sourceName(HEART_SOURCE_UUIDS.ANSM),
|
|
467
|
+
sections: [
|
|
468
|
+
{ key: "message", label: "Message", body: message },
|
|
469
|
+
{
|
|
470
|
+
key: "brands",
|
|
471
|
+
label: locale.value === "en" ? "Brands concerned" : "Spécialités concernées",
|
|
472
|
+
body: brands,
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
/** Pre-AMM ou post-AMM, accents et langue mis de cote. */
|
|
481
|
+
function aapTone(label: string) {
|
|
482
|
+
const value = (label || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
483
|
+
if (value.includes("pre")) return "pre" as const;
|
|
484
|
+
if (value.includes("post")) return "post" as const;
|
|
485
|
+
return "neutral" as const;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Statut d'un P.U.T : « en cours » se lit comme une bonne nouvelle. */
|
|
489
|
+
function putTone(status: string): "good" | "neutral" {
|
|
490
|
+
const value = (status || "").toLowerCase();
|
|
491
|
+
if (value.includes("en cours") || value.includes("ongoing")) return "good";
|
|
492
|
+
return "neutral";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const putRecords = computed(() =>
|
|
496
|
+
(resultsQueryPut.value ?? []).map((hit: any) => {
|
|
497
|
+
const put = hit._source ?? {};
|
|
498
|
+
const english = locale.value === "en";
|
|
499
|
+
const status = pickText(english ? put.status_en : put.status_fr, put.status_fr);
|
|
500
|
+
const indication = pickText(
|
|
501
|
+
english ? put.indication_en : put.indication_fr,
|
|
502
|
+
put.indication_fr
|
|
503
|
+
);
|
|
504
|
+
const objective = pickText(
|
|
505
|
+
english ? put.objective_en : put.objective_fr,
|
|
506
|
+
put.objective_fr
|
|
507
|
+
);
|
|
508
|
+
const efficacy = pickText(
|
|
509
|
+
english ? put.efficacy_precision_en : put.efficacy_precision_fr,
|
|
510
|
+
put.efficacy_precision_fr
|
|
511
|
+
);
|
|
512
|
+
const periodicity = pickText(
|
|
513
|
+
english ? put.put_periodicity_en : put.put_periodicity_fr,
|
|
514
|
+
put.put_periodicity_fr
|
|
515
|
+
);
|
|
516
|
+
const aapType = pickText(english ? put.aap_type_en : put.aap_type_fr, put.aap_type_fr);
|
|
517
|
+
const area = pickText(
|
|
518
|
+
english ? put.therapeutic_area_en : put.therapeutic_area_fr,
|
|
519
|
+
put.therapeutic_area_fr
|
|
520
|
+
);
|
|
521
|
+
const links = [...(put.tekkare_doc_links ?? []), ...(put.tekkare_pdf_links ?? [])];
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
key: hit._id,
|
|
525
|
+
typeLabel: "P.U.T",
|
|
526
|
+
icon: "ClipboardText",
|
|
527
|
+
date: put.authorization_date || null,
|
|
528
|
+
badge: aapType || null,
|
|
529
|
+
badgeTone: aapTone(aapType),
|
|
530
|
+
url: links[0] || null,
|
|
531
|
+
title: pickText(put.specialty_dci) || "P.U.T",
|
|
532
|
+
excerpt: objective || indication || null,
|
|
533
|
+
status: status || null,
|
|
534
|
+
statusTone: putTone(status),
|
|
535
|
+
sourceLabel: sourceName(HEART_SOURCE_UUIDS.HAS),
|
|
536
|
+
sections: [
|
|
537
|
+
{ key: "objective", label: t("oipDrugs.heart.put.card.objectives"), body: objective },
|
|
538
|
+
{ key: "indication", label: t("oipDrugs.heart.put.card.indication"), body: indication },
|
|
539
|
+
{ key: "efficacy", label: t("oipDrugs.heart.put.card.efficacy"), body: efficacy },
|
|
540
|
+
{
|
|
541
|
+
key: "context",
|
|
542
|
+
label: english ? "Framework" : "Cadre",
|
|
543
|
+
body: [
|
|
544
|
+
periodicity && `${t("oipDrugs.heart.put.card.periodicity")} : ${periodicity}`,
|
|
545
|
+
aapType && `${t("oipDrugs.heart.put.card.aapType")} : ${aapType}`,
|
|
546
|
+
area && `${english ? "Therapeutic area" : "Aire thérapeutique"} : ${area}`,
|
|
547
|
+
]
|
|
548
|
+
.filter(Boolean)
|
|
549
|
+
.join("\n"),
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
};
|
|
553
|
+
})
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Indications en liste en sus de la marque : une entree par code d'indication,
|
|
558
|
+
* les lignes produit rassemblees dessous. La date est la plus recente
|
|
559
|
+
* inscription ; le tiroir montre ensuite le detail complet du code.
|
|
560
|
+
*/
|
|
561
|
+
const lesRecords = computed(() => {
|
|
562
|
+
const byCode = new Map<string, any[]>();
|
|
563
|
+
for (const row of resultsQueryLes.value) {
|
|
564
|
+
if (!row?.indication) continue;
|
|
565
|
+
if (!byCode.has(row.indication)) byCode.set(row.indication, []);
|
|
566
|
+
byCode.get(row.indication)!.push(row);
|
|
567
|
+
}
|
|
568
|
+
return [...byCode.entries()]
|
|
569
|
+
.map(([code, rows]) => {
|
|
570
|
+
const label: string = rows[0].indication_label_fr || "";
|
|
571
|
+
const dates = rows.map((r) => r.start_date).filter(Boolean).sort();
|
|
572
|
+
const products = [...new Set(rows.map((r) => r.french_label).filter(Boolean))];
|
|
573
|
+
// « OUI » seul ne dit rien : on garde la question qu'il resout.
|
|
574
|
+
const flagged = rows.find((r) => r.statut);
|
|
575
|
+
const status = flagged
|
|
576
|
+
? [flagged.statut_title, flagged.statut].filter(Boolean).join(" : ")
|
|
577
|
+
: null;
|
|
578
|
+
return {
|
|
579
|
+
key: code,
|
|
580
|
+
code,
|
|
581
|
+
typeLabel: "Liste en sus",
|
|
582
|
+
icon: "ListChecks",
|
|
583
|
+
date: dates[dates.length - 1] ?? null,
|
|
584
|
+
ref: code,
|
|
585
|
+
badge: rows[0].source || null,
|
|
586
|
+
title: label ? label.charAt(0) + label.slice(1).toLowerCase() : `Indication ${code}`,
|
|
587
|
+
excerpt: products.join(", ") || null,
|
|
588
|
+
status,
|
|
589
|
+
sourceLabel: sourceName(HEART_SOURCE_UUIDS.LES),
|
|
590
|
+
};
|
|
591
|
+
})
|
|
592
|
+
.sort((a, b) => new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime());
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
/** Rubriques d'une contribution, dans l'ordre de la fiche d'origine. */
|
|
596
|
+
const CONTRIBUTION_FIELDS = [
|
|
597
|
+
{ field: "drug_indication", label: "indication" },
|
|
598
|
+
{ field: "synthesis", label: "synthesis" },
|
|
599
|
+
{ field: "impact", label: "impact" },
|
|
600
|
+
{ field: "method", label: "method" },
|
|
601
|
+
{ field: "experience_drug", label: "experienceDrug" },
|
|
602
|
+
{ field: "experience_current_treatment", label: "experienceCurrent" },
|
|
603
|
+
{ field: "additional_info", label: "additional" },
|
|
604
|
+
];
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* La fiche de l'association qui signe chaque contribution. Le renvoi
|
|
608
|
+
* n'apparait qu'une fois l'association reconnue (cf. useAssociationLookup).
|
|
609
|
+
*/
|
|
610
|
+
const { load: loadAssociations, idFor: associationIdFor } = useAssociationLookup();
|
|
611
|
+
|
|
612
|
+
watch(
|
|
613
|
+
() => resultsQuery.value?.patient_contribution,
|
|
614
|
+
(contributions: any[] | undefined) => {
|
|
615
|
+
if (!contributions?.length) return;
|
|
616
|
+
loadAssociations(
|
|
617
|
+
contributions.flatMap((contribution: any) => [
|
|
618
|
+
contribution?.association_label_regroup,
|
|
619
|
+
contribution?.association_label,
|
|
620
|
+
]),
|
|
621
|
+
);
|
|
622
|
+
},
|
|
623
|
+
{ immediate: true },
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const contributionRecords = computed(() =>
|
|
627
|
+
[...(resultsQuery.value?.patient_contribution ?? [])]
|
|
628
|
+
.sort(
|
|
629
|
+
(a: any, b: any) =>
|
|
630
|
+
new Date(b.contribution_date).getTime() - new Date(a.contribution_date).getTime()
|
|
631
|
+
)
|
|
632
|
+
.map((contribution: any, index: number) => {
|
|
633
|
+
const regroup = contribution.association_label_regroup || contribution.association_label;
|
|
634
|
+
const original =
|
|
635
|
+
contribution.association_label &&
|
|
636
|
+
contribution.association_label_regroup &&
|
|
637
|
+
contribution.association_label !== contribution.association_label_regroup
|
|
638
|
+
? contribution.association_label
|
|
639
|
+
: null;
|
|
640
|
+
|
|
641
|
+
const associationId = associationIdFor(regroup, contribution.association_label);
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
key: contribution.has_code ? `${contribution.has_code}-${index}` : String(index),
|
|
645
|
+
typeLabel: locale.value === "en" ? "Patient contribution" : "Contribution patient",
|
|
646
|
+
// `to` : chemin de fr_drugs_app, suivi par useOipResourceNav dans le tiroir.
|
|
647
|
+
link: associationId
|
|
648
|
+
? {
|
|
649
|
+
label: t("oipDrugs.heart.details.patientContrib.seeAssociation"),
|
|
650
|
+
to: drugsAssociationPath(associationId),
|
|
651
|
+
icon: "UsersThree",
|
|
652
|
+
}
|
|
653
|
+
: null,
|
|
654
|
+
// Sept rubriques : en onglets elles debordaient sur trois lignes.
|
|
655
|
+
layout: "accordion" as const,
|
|
656
|
+
icon: "Users",
|
|
657
|
+
date: contribution.contribution_date || null,
|
|
658
|
+
ref: contribution.request_nature || null,
|
|
659
|
+
url: contribution.contribution_url || null,
|
|
660
|
+
title: regroup || "-",
|
|
661
|
+
excerpt: pickText(contribution.synthesis) || original,
|
|
662
|
+
sourceLabel: sourceName(HEART_SOURCE_UUIDS.HAS),
|
|
663
|
+
sections: CONTRIBUTION_FIELDS.map((section) => ({
|
|
664
|
+
key: section.field,
|
|
665
|
+
label: t(`oipDrugs.heart.details.patientContrib.${section.label}`),
|
|
666
|
+
body: pickText(contribution[section.field]),
|
|
667
|
+
})),
|
|
668
|
+
};
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Pour eviter les bugs lors de la navigation : une reponse d'une marque
|
|
673
|
+
// precedente (ou d'une fiche demontee) ne s'ecrit pas.
|
|
674
|
+
const isMounted = ref(false);
|
|
675
|
+
let generation = 0;
|
|
676
|
+
const live = (gen: number) => isMounted.value && gen === generation;
|
|
677
|
+
|
|
678
|
+
watch(
|
|
679
|
+
() => chosenTab.value,
|
|
680
|
+
(val) => {
|
|
681
|
+
// Changer d'onglet ferme le tiroir : sinon le detail d'une alerte reste
|
|
682
|
+
// ouvert par-dessus l'onglet suivant.
|
|
683
|
+
drawerRecord.value = null;
|
|
684
|
+
// L'onglet ouvert remonte a l'appelant (ex `?tab=` de la page).
|
|
685
|
+
const key = availableTabs.value[val - 1]?.key;
|
|
686
|
+
if (isMounted.value && key && normalizeTab(props.tab) !== key) {
|
|
687
|
+
emit("update:tab", key);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
// L'appelant change l'onglet (retour arriere de la page, lien interne...).
|
|
693
|
+
watch(
|
|
694
|
+
() => props.tab,
|
|
695
|
+
(key) => {
|
|
696
|
+
if (!key || isLoadingQuery.value) return;
|
|
697
|
+
const index = tabIndexOf(key);
|
|
698
|
+
if (index > 0 && index !== chosenTab.value) chosenTab.value = index;
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
// `and` sur tous les mots, et l'ecriture exacte remontee en tete : avec un
|
|
703
|
+
// seul resultat, « KEYTRUDA » ne doit pas ramener une marque voisine.
|
|
704
|
+
const customQuery = (brand: string) => ({
|
|
705
|
+
from: 0,
|
|
706
|
+
query: {
|
|
707
|
+
bool: {
|
|
708
|
+
must: [{ match: { brand_group: { query: brand, operator: "and" } } }],
|
|
709
|
+
should: [{ term: { "brand_group.keyword": brand } }],
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
size: 1,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const lanchQuery = async (gen: number) => {
|
|
716
|
+
if (!live(gen)) return;
|
|
717
|
+
|
|
718
|
+
isLoadingQuery.value = true;
|
|
719
|
+
// Reponse entiere (`allAnswer`) plutot que `onlyOne` : le bouton « Projet »
|
|
720
|
+
// range la fiche par son `_id`, que `onlyOne` ne rend pas.
|
|
721
|
+
const fetchBrand = (brand: string) =>
|
|
722
|
+
queryCustomElastic({
|
|
723
|
+
index: indexQuery,
|
|
724
|
+
customQuery: customQuery(brand),
|
|
725
|
+
allAnswer: true,
|
|
726
|
+
})
|
|
727
|
+
.then((r: any) => r?.hits?.hits?.[0] ?? null)
|
|
728
|
+
.catch((error) => {
|
|
729
|
+
console.error('Error fetching data:', error);
|
|
730
|
+
return null;
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
let hit = await fetchBrand(brandMatch.value);
|
|
734
|
+
// Repli pour les liens partages avant heartBrandPath (« / » ecrit « + »).
|
|
735
|
+
if (!hit?._source?.brand_group && brandMatch.value.includes(" + ")) {
|
|
736
|
+
const legacy = brandMatch.value.replace(" + ", " / ");
|
|
737
|
+
hit = await fetchBrand(legacy);
|
|
738
|
+
if (hit?._source?.brand_group && live(gen)) brandMatch.value = legacy;
|
|
739
|
+
}
|
|
740
|
+
// Sur erreur ES le composable rend un objet vide : sans `brand_group`, ce
|
|
741
|
+
// n'est pas une fiche, on ne l'affiche pas.
|
|
742
|
+
if (hit?._source?.brand_group && live(gen)) {
|
|
743
|
+
resultsQuery.value = hit._source;
|
|
744
|
+
brandId.value = hit._id;
|
|
745
|
+
}
|
|
746
|
+
if (live(gen)) {
|
|
747
|
+
isLoadingQuery.value = false;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// Les requetes secondaires lisent `brandMatch` au lancement, pas a la
|
|
752
|
+
// declaration : le repli de `lanchQuery` a pu le changer entre-temps.
|
|
753
|
+
const customQueryPrices = () => ({
|
|
754
|
+
from: 0,
|
|
755
|
+
_source: [],
|
|
756
|
+
query: {
|
|
757
|
+
bool: {
|
|
758
|
+
must: [{ term: { "brand.keyword": brandMatch.value } }],
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
size: 100,
|
|
762
|
+
track_total_hits: true,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const launchQueryPrices = async (gen: number) => {
|
|
766
|
+
if (!live(gen)) return;
|
|
767
|
+
await queryCustomElastic({
|
|
768
|
+
index: ES_INDEX.DRUG_CARDS,
|
|
769
|
+
customQuery: customQueryPrices(),
|
|
770
|
+
}).then((res) => {
|
|
771
|
+
if (res && res.results && res.results.length > 0 && live(gen)) {
|
|
772
|
+
resultsQueryPrice.value = res.results;
|
|
773
|
+
}
|
|
774
|
+
}).catch((error) => {
|
|
775
|
+
console.error('Error fetching prices:', error);
|
|
776
|
+
});
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// Suivi mondial des prix : ce que les autres pays publient pour la marque.
|
|
780
|
+
// Le prefixe rattrape les declinaisons de gamme (« KEYTRUDA QLEX »).
|
|
781
|
+
const launchQueryIntlPrice = async (gen: number) => {
|
|
782
|
+
if (!live(gen)) return;
|
|
783
|
+
const res = await queryCustomElastic({
|
|
784
|
+
index: ES_INDEX.WW_PRICE_TRACKER,
|
|
785
|
+
customQuery: {
|
|
786
|
+
from: 0,
|
|
787
|
+
query: {
|
|
788
|
+
bool: {
|
|
789
|
+
should: [
|
|
790
|
+
{ term: { "brand.keyword": brandMatch.value } },
|
|
791
|
+
{ prefix: { "brand.keyword": `${brandMatch.value} ` } },
|
|
792
|
+
],
|
|
793
|
+
minimum_should_match: 1,
|
|
794
|
+
// Le perimetre des pays est filtre ici, pas seulement a l'affichage.
|
|
795
|
+
filter: [
|
|
796
|
+
{ terms: { "country.country_iso_2.keyword": HEART_INTL_PRICE_COUNTRIES } },
|
|
797
|
+
],
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
size: 200,
|
|
801
|
+
track_total_hits: true,
|
|
802
|
+
} as any,
|
|
803
|
+
}).catch((error) => {
|
|
804
|
+
console.error("Error fetching international prices:", error);
|
|
805
|
+
return null;
|
|
806
|
+
});
|
|
807
|
+
if (res?.results && live(gen)) {
|
|
808
|
+
resultsQueryIntlPrice.value = res.results;
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// ANSM (autorisation) : toutes les specialites CIS de la marque.
|
|
813
|
+
const customQueryAnsm = () => ({
|
|
814
|
+
from: 0,
|
|
815
|
+
query: { bool: { must: [{ term: { "brand.keyword": brandMatch.value } }] } },
|
|
816
|
+
size: 200,
|
|
817
|
+
sort: [{ authorization_date: { order: "desc", missing: "_last" } }],
|
|
818
|
+
track_total_hits: true,
|
|
819
|
+
});
|
|
820
|
+
const launchQueryAnsm = async (gen: number) => {
|
|
821
|
+
if (!live(gen)) return;
|
|
822
|
+
const res = await queryCustomElastic({
|
|
823
|
+
index: ES_INDEX.CIS_ANSM,
|
|
824
|
+
customQuery: customQueryAnsm() as any,
|
|
825
|
+
allAnswer: true,
|
|
826
|
+
}).catch((error) => {
|
|
827
|
+
console.error("Error fetching ANSM:", error);
|
|
828
|
+
return null;
|
|
829
|
+
});
|
|
830
|
+
if (res?.hits?.hits && live(gen)) {
|
|
831
|
+
resultsQueryAnsm.value = res.hits.hits;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// MITM (liste des medicaments d'interet therapeutique majeur) de la marque.
|
|
836
|
+
const customQueryMitm = () => ({
|
|
837
|
+
from: 0,
|
|
838
|
+
query: { bool: { must: [{ term: { "brand.keyword": brandMatch.value } }] } },
|
|
839
|
+
size: 200,
|
|
840
|
+
track_total_hits: true,
|
|
841
|
+
});
|
|
842
|
+
const launchQueryMitm = async (gen: number) => {
|
|
843
|
+
if (!live(gen)) return;
|
|
844
|
+
const res = await queryCustomElastic({
|
|
845
|
+
index: ES_INDEX.MITM_BDPM,
|
|
846
|
+
customQuery: customQueryMitm(),
|
|
847
|
+
allAnswer: true,
|
|
848
|
+
}).catch((error) => {
|
|
849
|
+
console.error("Error fetching MITM:", error);
|
|
850
|
+
return null;
|
|
851
|
+
});
|
|
852
|
+
if (res?.hits?.hits && live(gen)) {
|
|
853
|
+
// Deduplication par cis_code (plusieurs snapshots du meme CIS) : on garde
|
|
854
|
+
// le plus recent via @timestamp.
|
|
855
|
+
const byCis = new Map<string, any>();
|
|
856
|
+
for (const h of res.hits.hits) {
|
|
857
|
+
const key = h._source?.cis_code ?? h._id;
|
|
858
|
+
const existing = byCis.get(key);
|
|
859
|
+
if (
|
|
860
|
+
!existing ||
|
|
861
|
+
new Date(h._source?.["@timestamp"] || 0).getTime() >
|
|
862
|
+
new Date(existing._source?.["@timestamp"] || 0).getTime()
|
|
863
|
+
) {
|
|
864
|
+
byCis.set(key, h);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
resultsQueryMitm.value = [...byCis.values()];
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
// PUT (protocoles d'utilisation therapeutique / acces precoce) de la marque.
|
|
872
|
+
const customQueryPut = () => ({
|
|
873
|
+
from: 0,
|
|
874
|
+
query: { bool: { must: [{ term: { "brand.keyword": brandMatch.value } }] } },
|
|
875
|
+
size: 200,
|
|
876
|
+
sort: [{ authorization_date: { order: "desc", missing: "_last" } }],
|
|
877
|
+
track_total_hits: true,
|
|
878
|
+
});
|
|
879
|
+
const launchQueryPut = async (gen: number) => {
|
|
880
|
+
if (!live(gen)) return;
|
|
881
|
+
const res = await queryCustomElastic({
|
|
882
|
+
index: ES_INDEX.PUT_HAS,
|
|
883
|
+
customQuery: customQueryPut() as any,
|
|
884
|
+
allAnswer: true,
|
|
885
|
+
}).catch((error) => {
|
|
886
|
+
console.error("Error fetching PUT:", error);
|
|
887
|
+
return null;
|
|
888
|
+
});
|
|
889
|
+
if (res?.hits?.hits && live(gen)) {
|
|
890
|
+
resultsQueryPut.value = res.hits.hits;
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// LES (liste en sus) : nom exact d'abord, puis nom contenu, les libelles LES
|
|
895
|
+
// pouvant porter un dosage ou un regroupement que la marque HAS n'a pas.
|
|
896
|
+
const launchQueryLes = async (gen: number) => {
|
|
897
|
+
if (!live(gen)) return;
|
|
898
|
+
const run = (clause: any) =>
|
|
899
|
+
queryCustomElastic({
|
|
900
|
+
index: HEART_LES_INDEX,
|
|
901
|
+
customQuery: { from: 0, query: { bool: { must: [clause] } }, size: 200, track_total_hits: true },
|
|
902
|
+
cluster: "pubmed",
|
|
903
|
+
}).catch((error) => {
|
|
904
|
+
console.error("Error fetching LES:", error);
|
|
905
|
+
return null;
|
|
906
|
+
});
|
|
907
|
+
let res = await run({ term: { "brand.keyword": brandMatch.value } });
|
|
908
|
+
if (!res?.results?.length) res = await run({ wildcard: { "brand.keyword": `*${brandMatch.value}*` } });
|
|
909
|
+
if (res?.results && live(gen)) {
|
|
910
|
+
resultsQueryLes.value = res.results;
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
/** Ce que la recherche a matche sur cette marque, pour marquer les avis. */
|
|
915
|
+
const brandMatches = useHeartBrandMatches();
|
|
916
|
+
|
|
917
|
+
/** Termes surlignes : ceux du moteur pour cette marque, sinon ceux de l'appelant. */
|
|
918
|
+
const searchTerms = computed<string[]>(() =>
|
|
919
|
+
brandMatches.terms.value?.length ? brandMatches.terms.value : props.highlight ?? [],
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Onglet demande par la prop `tab` (ansm, alerts, price, overview...). Rend
|
|
924
|
+
* faux tant que la section n'est pas chargee : l'appelant reessaie plus tard.
|
|
925
|
+
*/
|
|
926
|
+
function openRequestedTab() {
|
|
927
|
+
const requestedIdx = props.tab ? tabIndexOf(props.tab) : 0;
|
|
928
|
+
if (requestedIdx === 0) return false;
|
|
929
|
+
chosenTab.value = requestedIdx;
|
|
930
|
+
// tab=les + indication=<code> : le tiroir de l'indication s'ouvre d'emblee.
|
|
931
|
+
if (normalizeTab(props.tab) === 'les' && props.indication) {
|
|
932
|
+
const record = lesRecords.value.find((r) => r.code === props.indication);
|
|
933
|
+
if (record) drawerLes.value = record;
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Remet la fiche a zero, pour une autre marque. */
|
|
939
|
+
function reset() {
|
|
940
|
+
resultsQuery.value = null;
|
|
941
|
+
brandId.value = null;
|
|
942
|
+
resultsQueryPrice.value = [];
|
|
943
|
+
resultsQueryIntlPrice.value = [];
|
|
944
|
+
resultsQueryAnsm.value = [];
|
|
945
|
+
resultsQueryMitm.value = [];
|
|
946
|
+
resultsQueryPut.value = [];
|
|
947
|
+
resultsQueryLes.value = [];
|
|
948
|
+
selectedAnsmIndex.value = 0;
|
|
949
|
+
selectedMitmIndex.value = 0;
|
|
950
|
+
drawerOpinion.value = null;
|
|
951
|
+
drawerRecord.value = null;
|
|
952
|
+
drawerLes.value = null;
|
|
953
|
+
chosenTab.value = 1;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function loadAll() {
|
|
957
|
+
const gen = ++generation;
|
|
958
|
+
isLoadingSecondary.value = true;
|
|
959
|
+
await lanchQuery(gen);
|
|
960
|
+
if (!live(gen)) return;
|
|
961
|
+
|
|
962
|
+
// L'onglet se pose des que la HAS a repondu. Par defaut l'historique HAS
|
|
963
|
+
// (jamais la monographie), a defaut le premier onglet disponible.
|
|
964
|
+
const tabOpened = openRequestedTab();
|
|
965
|
+
if (!tabOpened) chosenTab.value = overviewTabIndex.value > 0 ? overviewTabIndex.value : 1;
|
|
966
|
+
|
|
967
|
+
const brand = resultsQuery.value?.brand_group || brandMatch.value;
|
|
968
|
+
await Promise.all([
|
|
969
|
+
launchQueryPrices(gen),
|
|
970
|
+
launchQueryIntlPrice(gen),
|
|
971
|
+
loadIntlApprovals(brand),
|
|
972
|
+
launchQueryAnsm(gen),
|
|
973
|
+
launchQueryMitm(gen),
|
|
974
|
+
launchQueryPut(gen),
|
|
975
|
+
launchQueryLes(gen),
|
|
976
|
+
brandMatches.load(brand, indexQuery),
|
|
977
|
+
]);
|
|
978
|
+
if (!live(gen)) return;
|
|
979
|
+
isLoadingSecondary.value = false;
|
|
980
|
+
|
|
981
|
+
// Preselection du CIS (clic sur un dosage precis dans une carte ANSM : cis=<_id>)
|
|
982
|
+
if (props.cis) {
|
|
983
|
+
const i = resultsQueryAnsm.value.findIndex((h) => h._id === props.cis);
|
|
984
|
+
if (i !== -1) selectedAnsmIndex.value = i;
|
|
985
|
+
// MITM : la carte passe le cis_code (pas le _id du doc)
|
|
986
|
+
const j = resultsQueryMitm.value.findIndex(
|
|
987
|
+
(m) => String(m._source?.cis_code) === String(props.cis)
|
|
988
|
+
);
|
|
989
|
+
if (j !== -1) selectedMitmIndex.value = j;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// L'onglet demande dependait d'une section secondaire (prix, ANSM, LES...).
|
|
993
|
+
if (!tabOpened) openRequestedTab();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
onMounted(() => {
|
|
997
|
+
isMounted.value = true;
|
|
998
|
+
loadAll();
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Une autre marque dans la meme instance (tiroir reutilise) : on recharge.
|
|
1002
|
+
watch(
|
|
1003
|
+
() => props.brand,
|
|
1004
|
+
(brand) => {
|
|
1005
|
+
if (!isMounted.value) return;
|
|
1006
|
+
brandMatch.value = String(brand ?? "");
|
|
1007
|
+
reset();
|
|
1008
|
+
loadAll();
|
|
1009
|
+
}
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
// Le tiroir d'un projet fournit sa navigation : la fiche y est embarquee.
|
|
1013
|
+
const embedded = inject(OIP_RESOURCE_NAV, null) !== null;
|
|
1014
|
+
|
|
1015
|
+
onBeforeUnmount(() => {
|
|
1016
|
+
isMounted.value = false;
|
|
1017
|
+
if (import.meta.client && !embedded) {
|
|
1018
|
+
document.body.classList.remove("is-monograph-fullbleed");
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// Onglet Monographie : on neutralise la largeur max du conteneur de page
|
|
1023
|
+
// (.oip_container) pour que le document occupe toute la zone de contenu.
|
|
1024
|
+
// Dans le tiroir d'un projet, la classe elargirait la page dessous.
|
|
1025
|
+
watchEffect(() => {
|
|
1026
|
+
if (import.meta.client && !embedded) {
|
|
1027
|
+
document.body.classList.toggle(
|
|
1028
|
+
"is-monograph-fullbleed",
|
|
1029
|
+
monographTabIndex.value > 0 && chosenTab.value === monographTabIndex.value
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// Le reste (libelle d'onglet, marque, sections secondaires) sert a la page de
|
|
1035
|
+
// fr_drugs_app : elle declare l'export du cadre depuis son script, ou le slot
|
|
1036
|
+
// `actions` n'est pas lisible. Memes donnees que les props de ce slot.
|
|
1037
|
+
defineExpose({
|
|
1038
|
+
product: resultsQuery,
|
|
1039
|
+
brandId,
|
|
1040
|
+
currentTabKey,
|
|
1041
|
+
availableTabs,
|
|
1042
|
+
currentTabLabel,
|
|
1043
|
+
brand: title,
|
|
1044
|
+
prices: resultsQueryPrice,
|
|
1045
|
+
intlPrices: resultsQueryIntlPrice,
|
|
1046
|
+
intlRecords,
|
|
1047
|
+
ansm: resultsQueryAnsm,
|
|
1048
|
+
mitm: resultsQueryMitm,
|
|
1049
|
+
put: resultsQueryPut,
|
|
1050
|
+
les: resultsQueryLes,
|
|
1051
|
+
});
|
|
1052
|
+
</script>
|
|
1053
|
+
|
|
1054
|
+
<style scoped>
|
|
1055
|
+
/* Le titre suit la typo du bandeau (RmPageHeader) ; le lien du tiroir
|
|
1056
|
+
(`title-link`) passe dessous. */
|
|
1057
|
+
.oip-drugs-brand__title {
|
|
1058
|
+
display: block;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.oip-drugs-brand__state {
|
|
1062
|
+
box-sizing: border-box;
|
|
1063
|
+
width: 100%;
|
|
1064
|
+
padding: 24px 32px;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/* Selecteur de specialite (CIS) de la marque, dans l'onglet Autorisation. */
|
|
1068
|
+
.cis-switch {
|
|
1069
|
+
display: flex;
|
|
1070
|
+
flex-wrap: wrap;
|
|
1071
|
+
align-items: center;
|
|
1072
|
+
justify-content: flex-start;
|
|
1073
|
+
align-self: flex-start;
|
|
1074
|
+
width: 100%;
|
|
1075
|
+
gap: 8px;
|
|
1076
|
+
margin-block: 16px;
|
|
1077
|
+
}
|
|
1078
|
+
.cis-switch-label {
|
|
1079
|
+
margin-right: 4px;
|
|
1080
|
+
}
|
|
1081
|
+
.cis-chip {
|
|
1082
|
+
border: 1px solid var(--light, #ececf2);
|
|
1083
|
+
background: var(--white);
|
|
1084
|
+
color: var(--dark);
|
|
1085
|
+
border-radius: 999px;
|
|
1086
|
+
padding: 6px 14px;
|
|
1087
|
+
font-size: 13px;
|
|
1088
|
+
font-weight: 600;
|
|
1089
|
+
cursor: pointer;
|
|
1090
|
+
transition: all 0.15s;
|
|
1091
|
+
}
|
|
1092
|
+
.cis-chip:hover {
|
|
1093
|
+
border-color: var(--primary);
|
|
1094
|
+
color: var(--primary);
|
|
1095
|
+
}
|
|
1096
|
+
.cis-chip.active {
|
|
1097
|
+
background: var(--primary);
|
|
1098
|
+
border-color: var(--primary);
|
|
1099
|
+
color: var(--white);
|
|
1100
|
+
}
|
|
1101
|
+
</style>
|
|
1102
|
+
|
|
1103
|
+
<style>
|
|
1104
|
+
/* Onglet Monographie : neutralise la largeur max + le padding du conteneur
|
|
1105
|
+
de page (full-bleed). Classe posee sur <body> par la fiche, hors tiroir. */
|
|
1106
|
+
body.is-monograph-fullbleed .oip_container {
|
|
1107
|
+
max-width: none !important;
|
|
1108
|
+
margin: 0 !important;
|
|
1109
|
+
padding: 0 !important;
|
|
1110
|
+
}
|
|
1111
|
+
</style>
|