@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correspondances de la recherche sur une marque, pour la fiche produit.
|
|
3
|
+
*
|
|
4
|
+
* La clause deposee par la liste est rejouee ici, restreinte a la marque
|
|
5
|
+
* affichee et avec des inner hits elargis : le moteur renvoie alors TOUS les
|
|
6
|
+
* avis qui repondent aux termes, et non l'echantillon suffisant a une carte.
|
|
7
|
+
* C'est ce qui permet de marquer chaque avis de l'historique.
|
|
8
|
+
*/
|
|
9
|
+
import { oipEsSearch } from "../../composables/useOipResources";
|
|
10
|
+
import { heartExplainMatch } from "./useHeartMatchExplain";
|
|
11
|
+
import type { MatchExplain } from "./useHeartMatchExplain";
|
|
12
|
+
|
|
13
|
+
/** Le contexte que la liste de fr_drugs_app depose (cf. useHeartSearchContext). */
|
|
14
|
+
type HeartSearchContext = {
|
|
15
|
+
/** Termes saisis, pour le surlignage du texte. */
|
|
16
|
+
terms: string[];
|
|
17
|
+
/** Clause ES de la recherche, inner hits compris. */
|
|
18
|
+
clause: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Meme cle que fr_drugs_app : la fiche montee dans l'app y relit le contexte. */
|
|
22
|
+
const STORAGE_KEY = "oip-heart-search-context";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Lecture seule du contexte de recherche (portage de `readStored` de
|
|
26
|
+
* useHeartSearchContext) : seule la liste l'ecrit, la fiche ne fait que le relire.
|
|
27
|
+
*/
|
|
28
|
+
function useStoredSearchContext() {
|
|
29
|
+
const context = useState<HeartSearchContext | null>(STORAGE_KEY, () => null);
|
|
30
|
+
|
|
31
|
+
function readStored() {
|
|
32
|
+
if (context.value) return context.value;
|
|
33
|
+
try {
|
|
34
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
35
|
+
if (raw) context.value = JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
// Session illisible : la fiche s'affiche sans explication.
|
|
38
|
+
}
|
|
39
|
+
return context.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { readStored };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Remonte la taille des inner hits d'une clause, sans toucher a l'originale. */
|
|
46
|
+
function withLargeInnerHits(clause: any, size: number): any {
|
|
47
|
+
if (Array.isArray(clause)) return clause.map((item) => withLargeInnerHits(item, size));
|
|
48
|
+
if (!clause || typeof clause !== "object") return clause;
|
|
49
|
+
|
|
50
|
+
const copy: any = {};
|
|
51
|
+
for (const [key, value] of Object.entries<any>(clause)) {
|
|
52
|
+
if (key === "inner_hits" && value && typeof value === "object") {
|
|
53
|
+
copy[key] = { ...value, size };
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
copy[key] = withLargeInnerHits(value, size);
|
|
57
|
+
}
|
|
58
|
+
return copy;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function useHeartBrandMatches() {
|
|
62
|
+
const { locale } = useI18n();
|
|
63
|
+
const { readStored } = useStoredSearchContext();
|
|
64
|
+
|
|
65
|
+
const explain = ref<MatchExplain | null>(null);
|
|
66
|
+
const terms = ref<string[]>([]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Interroge le moteur pour la marque donnee. Sans recherche en cours, ne
|
|
70
|
+
* lance rien et laisse l'explication vide.
|
|
71
|
+
*/
|
|
72
|
+
async function load(brandGroup: string, index: string) {
|
|
73
|
+
const context = readStored();
|
|
74
|
+
if (!context || !brandGroup) {
|
|
75
|
+
explain.value = null;
|
|
76
|
+
terms.value = [];
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const body = {
|
|
81
|
+
size: 1,
|
|
82
|
+
_source: ["brand_group"],
|
|
83
|
+
query: {
|
|
84
|
+
bool: {
|
|
85
|
+
// `and` : tous les mots de la marque, sinon « KEYTRUDA » ramenait
|
|
86
|
+
// n'importe quelle marque partageant un mot avec elle.
|
|
87
|
+
must: [
|
|
88
|
+
{ match: { brand_group: { query: brandGroup, operator: "and" } } },
|
|
89
|
+
withLargeInnerHits(context.clause, 100),
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const res: any = await oipEsSearch("drugs", index, body);
|
|
97
|
+
const hit = res?.hits?.hits?.[0];
|
|
98
|
+
explain.value = hit ? heartExplainMatch(hit, locale.value) : null;
|
|
99
|
+
// Le contexte survit en sessionStorage : sans avis de cette marque qui
|
|
100
|
+
// reponde a la recherche, il vient d'une autre fiche, on ne surligne rien.
|
|
101
|
+
terms.value = hit ? context.terms : [];
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
console.warn("[heart] match explain failed:", error?.message || error);
|
|
104
|
+
explain.value = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Codes HAS des avis qui repondent a la recherche. */
|
|
109
|
+
const matchedCodes = computed(() => new Set(explain.value?.hasCodes ?? []));
|
|
110
|
+
|
|
111
|
+
return { explain, terms, matchedCodes, load };
|
|
112
|
+
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from "vue";
|
|
2
|
+
|
|
3
|
+
// Portage de useHeartDetailSearches de fr_drugs_app : les termes ne se lisent
|
|
4
|
+
// plus dans le filterStore de l'app, ils arrivent par la prop `highlight` de
|
|
5
|
+
// la fiche (cf. PORTING.md).
|
|
6
|
+
|
|
7
|
+
const STOP_WORDS = new Set([
|
|
8
|
+
// French
|
|
9
|
+
"le", "la", "les", "de", "des", "du", "un", "une", "en", "et", "à", "au", "aux",
|
|
10
|
+
"ce", "ces", "cette", "par", "pour", "sur", "avec", "dans", "est", "sont",
|
|
11
|
+
"qui", "que", "dont", "où", "ne", "pas", "plus", "ou", "se", "son", "sa", "ses",
|
|
12
|
+
"l", "d", "n", "s", "qu",
|
|
13
|
+
// English
|
|
14
|
+
"the", "a", "an", "of", "in", "on", "at", "to", "for", "and", "or", "but",
|
|
15
|
+
"is", "are", "was", "were", "be", "it", "its", "this", "that", "not", "no",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split raw search terms into individual words and filter stop words
|
|
20
|
+
*/
|
|
21
|
+
export function heartSplitSearchTerms(rawSearches: any): string[] {
|
|
22
|
+
// Les appelants lisent le filterStore, qui n'est pas type : la valeur peut
|
|
23
|
+
// etre une liste de termes, un terme seul, ou une clause deja construite.
|
|
24
|
+
const list = Array.isArray(rawSearches) ? rawSearches : rawSearches ? [rawSearches] : [];
|
|
25
|
+
const terms = list
|
|
26
|
+
.map((a: any) => (a && typeof a === "object" ? a.value : a))
|
|
27
|
+
.filter((a: any) => typeof a === "string");
|
|
28
|
+
const words = terms
|
|
29
|
+
.flatMap((term: string) => term.split(/\s+/))
|
|
30
|
+
.filter(
|
|
31
|
+
(word: string) => word.length > 1 && !STOP_WORDS.has(word.toLowerCase())
|
|
32
|
+
);
|
|
33
|
+
return [...new Set(words)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Termes de recherche de la fiche, decoupes en mots sans mots vides. Dans
|
|
38
|
+
* fr_drugs_app ils venaient des filtres « Search Filter » de toutes les pages
|
|
39
|
+
* de recherche ; ici, de la prop `highlight` passee par l'appelant.
|
|
40
|
+
*/
|
|
41
|
+
export function useHeartDetailSearches(highlight?: MaybeRefOrGetter<string[] | undefined>) {
|
|
42
|
+
const getSearches = computed(() => heartSplitSearchTerms(toValue(highlight) ?? []));
|
|
43
|
+
|
|
44
|
+
return { getSearches };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Filter match utilities ---
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract disease names from rare disease filter values by stripping ORPHA codes.
|
|
51
|
+
* e.g. "Maladie de Fabry (ORPHA:324)" → "Maladie de Fabry"
|
|
52
|
+
*/
|
|
53
|
+
export function heartGetRareHighlightTerms(filterValues: string[]): string[] {
|
|
54
|
+
if (!filterValues || filterValues.length === 0) return [];
|
|
55
|
+
return filterValues
|
|
56
|
+
.map((v) => v.replace(/\s*\(ORPHA:\d+\)\s*/gi, "").trim())
|
|
57
|
+
.filter((v) => v.length > 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Oncology tagging utilities (based on section data) ---
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Map ES field paths (from oncology_tagging section.matched_field) to FIELD_DEFS labels.
|
|
64
|
+
*/
|
|
65
|
+
const MATCHED_FIELD_MAP: Record<string, string> = {
|
|
66
|
+
"brand_history.indication": "indication",
|
|
67
|
+
"brand_history.indication_en": "indication",
|
|
68
|
+
"brand_history.syn_indication": "indication",
|
|
69
|
+
"brand_history.syn_indication_en": "indication",
|
|
70
|
+
"brand_history.relevant_indication": "indication",
|
|
71
|
+
"brand_history.relevant_indication_en": "indication",
|
|
72
|
+
"brand_history.essential": "essential",
|
|
73
|
+
"brand_history.essential_en": "essential",
|
|
74
|
+
"brand_history.conclusion": "conclusion",
|
|
75
|
+
"brand_history.conclusion_en": "conclusion",
|
|
76
|
+
"brand_history.target_population": "target_population",
|
|
77
|
+
"brand_history.target_population_en": "target_population",
|
|
78
|
+
"brand_history.syn_target_pop": "target_population",
|
|
79
|
+
"brand_history.syn_target_pop_en": "target_population",
|
|
80
|
+
"brand_history.therapeutic_strategy_place": "therapeutic_strategy_place",
|
|
81
|
+
"brand_history.therapeutic_strategy_place_en": "therapeutic_strategy_place",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function fieldDefFromKey(key: string, locale: string): { label: string; tab: string | null } | null {
|
|
85
|
+
const mapped = MATCHED_FIELD_MAP[key];
|
|
86
|
+
if (!mapped) return null;
|
|
87
|
+
const def = FIELD_DEFS.find((d) => d.fields.includes(mapped));
|
|
88
|
+
if (!def) return null;
|
|
89
|
+
return { label: locale === "en" ? def.labelEn : def.labelFr, tab: def.tab };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract highlight words (from <em> tags) for the tags matching the given filter values.
|
|
94
|
+
*/
|
|
95
|
+
export function heartGetTaggingHighlightWords(tagging: any[], filterValues: string[]): string[] {
|
|
96
|
+
if (!tagging || !Array.isArray(tagging) || !filterValues || filterValues.length === 0) return [];
|
|
97
|
+
|
|
98
|
+
const filterSet = new Set(filterValues.map((v) => v.toLowerCase().trim()));
|
|
99
|
+
const words = new Set<string>();
|
|
100
|
+
|
|
101
|
+
for (const tag of tagging) {
|
|
102
|
+
const tagLabels: string[] = [];
|
|
103
|
+
if (tag.preferred_label) tagLabels.push(tag.preferred_label.toLowerCase().trim());
|
|
104
|
+
if (tag.icd10 && Array.isArray(tag.icd10)) {
|
|
105
|
+
for (const icd of tag.icd10) {
|
|
106
|
+
if (icd.combined_label) tagLabels.push(icd.combined_label.toLowerCase().trim());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!tagLabels.some((tl) => filterSet.has(tl))) continue;
|
|
111
|
+
|
|
112
|
+
if (tag.section && Array.isArray(tag.section)) {
|
|
113
|
+
for (const sec of tag.section) {
|
|
114
|
+
if (!sec.highlight) continue;
|
|
115
|
+
const matches = sec.highlight.match(/<em>(.*?)<\/em>/gi);
|
|
116
|
+
if (matches) {
|
|
117
|
+
for (const m of matches) {
|
|
118
|
+
const word = m.replace(/<\/?em>/gi, "").trim();
|
|
119
|
+
if (word.length > 0) words.add(word);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return [...words];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TaggingExcerpt {
|
|
130
|
+
html: string;
|
|
131
|
+
/** Mots <em> de CET extrait — pour retrouver l'avis d'ou il vient (cf. heartGetBestOpinionMatch). */
|
|
132
|
+
words: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extraits de texte (avec surlignage) pour les tags qui matchent les valeurs
|
|
137
|
+
* de filtre donnees — le "pourquoi" tel que le pipeline de tagging l'a
|
|
138
|
+
* lui-meme trouve dans le texte source, pas une reconstitution cote client.
|
|
139
|
+
* `<em>` (surlignage du pipeline) devient `<mark>` : c'est cette balise que
|
|
140
|
+
* la feuille du hub peint en rose partout ailleurs.
|
|
141
|
+
*/
|
|
142
|
+
export function heartGetTaggingExcerpts(tagging: any[], filterValues: string[]): TaggingExcerpt[] {
|
|
143
|
+
if (!tagging || !Array.isArray(tagging) || !filterValues || filterValues.length === 0) return [];
|
|
144
|
+
|
|
145
|
+
const filterSet = new Set(filterValues.map((v) => v.toLowerCase().trim()));
|
|
146
|
+
const byHtml = new Map<string, TaggingExcerpt>();
|
|
147
|
+
|
|
148
|
+
for (const tag of tagging) {
|
|
149
|
+
const tagLabels: string[] = [];
|
|
150
|
+
if (tag.preferred_label) tagLabels.push(tag.preferred_label.toLowerCase().trim());
|
|
151
|
+
if (tag.icd10 && Array.isArray(tag.icd10)) {
|
|
152
|
+
for (const icd of tag.icd10) {
|
|
153
|
+
if (icd.combined_label) tagLabels.push(icd.combined_label.toLowerCase().trim());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!tagLabels.some((tl) => filterSet.has(tl))) continue;
|
|
158
|
+
|
|
159
|
+
if (tag.section && Array.isArray(tag.section)) {
|
|
160
|
+
for (const sec of tag.section) {
|
|
161
|
+
if (typeof sec.highlight !== "string" || !sec.highlight) continue;
|
|
162
|
+
const html: string = sec.highlight.replace(/<\/?em>/gi, (m: string) => (m.startsWith("</") ? "</mark>" : "<mark>"));
|
|
163
|
+
if (byHtml.has(html)) continue;
|
|
164
|
+
const words = [...sec.highlight.matchAll(/<em>(.*?)<\/em>/gi)]
|
|
165
|
+
.map((m: RegExpMatchArray) => m[1]?.trim())
|
|
166
|
+
.filter((w): w is string => !!w && w.length > 0);
|
|
167
|
+
byHtml.set(html, { html, words });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [...byHtml.values()];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Matched fields utilities ---
|
|
176
|
+
|
|
177
|
+
function removeAccents(str: string): string {
|
|
178
|
+
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function addSingleAccent(word: string, char: string, accentedChar: string): string[] {
|
|
182
|
+
const variants: string[] = [];
|
|
183
|
+
for (let i = 0; i < word.length; i++) {
|
|
184
|
+
if (word[i]!.toLowerCase() === char) {
|
|
185
|
+
variants.push(word.slice(0, i) + accentedChar + word.slice(i + 1));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return variants;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function addAccents(word: string): string[] {
|
|
192
|
+
let variants: string[] = [word];
|
|
193
|
+
if (!word.toLowerCase().includes("é")) {
|
|
194
|
+
variants = variants.concat(addSingleAccent(word, "e", "é"));
|
|
195
|
+
}
|
|
196
|
+
if (!word.toLowerCase().includes("ï")) {
|
|
197
|
+
variants = variants.concat(addSingleAccent(word, "i", "ï"));
|
|
198
|
+
}
|
|
199
|
+
return variants;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleEndingS(word: string): string[] {
|
|
203
|
+
const variants: string[] = [word];
|
|
204
|
+
if (word.toLowerCase().endsWith("s")) {
|
|
205
|
+
variants.push(word.slice(0, -1));
|
|
206
|
+
} else {
|
|
207
|
+
variants.push(word + "s");
|
|
208
|
+
}
|
|
209
|
+
return variants;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function escapeRegExp(str: string): string {
|
|
213
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildSearchVariants(searches: string[]): string[] {
|
|
217
|
+
const result: string[] = [];
|
|
218
|
+
searches.forEach((word) => {
|
|
219
|
+
result.push(word);
|
|
220
|
+
const wordWithoutAccents = removeAccents(word);
|
|
221
|
+
if (word !== wordWithoutAccents) result.push(wordWithoutAccents);
|
|
222
|
+
const accentedVariants = addAccents(word);
|
|
223
|
+
accentedVariants.forEach((v) => {
|
|
224
|
+
if (!result.includes(v)) result.push(v);
|
|
225
|
+
});
|
|
226
|
+
[word, wordWithoutAccents, ...accentedVariants].forEach((w) => {
|
|
227
|
+
handleEndingS(w).forEach((s) => {
|
|
228
|
+
if (!result.includes(s)) result.push(s);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function fieldContainsSearch(fieldValue: any, regex: RegExp): boolean {
|
|
236
|
+
if (fieldValue === null || fieldValue === undefined) return false;
|
|
237
|
+
if (typeof fieldValue === "string") return regex.test(fieldValue);
|
|
238
|
+
if (Array.isArray(fieldValue))
|
|
239
|
+
return fieldValue.some((v) => fieldContainsSearch(v, regex));
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const FIELD_DEFS: { fields: string[]; labelFr: string; labelEn: string; tab: string | null }[] = [
|
|
244
|
+
{ fields: ["indication", "syn_indication", "relevant_indication"], labelFr: "Indication", labelEn: "Indication", tab: "indic" },
|
|
245
|
+
{ fields: ["essential"], labelFr: "Synthèse", labelEn: "Summary", tab: "ess" },
|
|
246
|
+
{ fields: ["conclusion"], labelFr: "Conclusion", labelEn: "Conclusion", tab: "ccl" },
|
|
247
|
+
{ fields: ["molecule"], labelFr: "Molécule", labelEn: "Molecule", tab: null },
|
|
248
|
+
{ fields: ["company"], labelFr: "Entreprise", labelEn: "Company", tab: null },
|
|
249
|
+
{ fields: ["brand"], labelFr: "Marque", labelEn: "Brand", tab: null },
|
|
250
|
+
{ fields: ["target_population", "syn_target_pop"], labelFr: "Population cible", labelEn: "Target pop.", tab: "pop" },
|
|
251
|
+
{ fields: ["therapeutic_strategy_place"], labelFr: "Stratégie thérap.", labelEn: "Therap. strategy", tab: "ther" },
|
|
252
|
+
{ fields: ["has_code"], labelFr: "Code HAS", labelEn: "HAS Code", tab: "info" },
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get matched field labels for a single opinion based on search terms.
|
|
257
|
+
* Only returns matches if ALL search terms are found in the same opinion.
|
|
258
|
+
*/
|
|
259
|
+
export function heartGetMatchedFields(
|
|
260
|
+
opinion: any,
|
|
261
|
+
searches: string[],
|
|
262
|
+
locale: string
|
|
263
|
+
): { label: string; tab: string | null }[] {
|
|
264
|
+
if (!searches || searches.length === 0) return [];
|
|
265
|
+
|
|
266
|
+
const termRegexes = buildPerTermRegexes(searches);
|
|
267
|
+
if (termRegexes.length === 0) return [];
|
|
268
|
+
|
|
269
|
+
// Check ALL terms match in at least one same field of this opinion
|
|
270
|
+
if (!opinionMatchesAllTerms(opinion, termRegexes)) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const matched: { label: string; tab: string | null }[] = [];
|
|
275
|
+
for (const def of FIELD_DEFS) {
|
|
276
|
+
// Only mark a field as matched if ALL search terms are found in it
|
|
277
|
+
const found = def.fields.some((f) => {
|
|
278
|
+
const val = opinion[f];
|
|
279
|
+
const valEn = opinion[f + "_en"];
|
|
280
|
+
return termRegexes.every(
|
|
281
|
+
(r) => fieldContainsSearch(val, r) || fieldContainsSearch(valEn, r)
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
if (found) {
|
|
285
|
+
matched.push({
|
|
286
|
+
label: locale === "en" ? def.labelEn : def.labelFr,
|
|
287
|
+
tab: def.tab,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return matched;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildSearchRegex(searches: string[]): RegExp | null {
|
|
295
|
+
const variants = buildSearchVariants(searches);
|
|
296
|
+
const escaped = variants
|
|
297
|
+
.filter((q) => q.trim() !== "")
|
|
298
|
+
.map((q) => escapeRegExp(q));
|
|
299
|
+
if (escaped.length === 0) return null;
|
|
300
|
+
return new RegExp(`\\b(${escaped.join("|")})`, "i");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Build one regex per original search term (with its variants).
|
|
305
|
+
* Used to check that ALL terms match in the same opinion.
|
|
306
|
+
*/
|
|
307
|
+
function buildPerTermRegexes(searches: string[]): RegExp[] {
|
|
308
|
+
return searches
|
|
309
|
+
.map((term) => {
|
|
310
|
+
const variants = buildSearchVariants([term]);
|
|
311
|
+
const escaped = variants
|
|
312
|
+
.filter((q) => q.trim() !== "")
|
|
313
|
+
.map((q) => escapeRegExp(q));
|
|
314
|
+
if (escaped.length === 0) return null;
|
|
315
|
+
return new RegExp(`\\b(${escaped.join("|")})`, "i");
|
|
316
|
+
})
|
|
317
|
+
.filter((r): r is RegExp => r !== null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if ALL search terms are found within at least one same field of the opinion.
|
|
322
|
+
* This mirrors ES behavior (query_string with default_operator: AND on a single field).
|
|
323
|
+
*/
|
|
324
|
+
function opinionMatchesAllTerms(opinion: any, termRegexes: RegExp[]): boolean {
|
|
325
|
+
// At least one field must contain ALL terms
|
|
326
|
+
return FIELD_DEFS.some((def) =>
|
|
327
|
+
def.fields.some((f) => {
|
|
328
|
+
const val = opinion[f];
|
|
329
|
+
const valEn = opinion[f + "_en"];
|
|
330
|
+
return termRegexes.every(
|
|
331
|
+
(regex) =>
|
|
332
|
+
fieldContainsSearch(val, regex) || fieldContainsSearch(valEn, regex)
|
|
333
|
+
);
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface OpinionMatch {
|
|
339
|
+
fields: { label: string; tab: string | null }[];
|
|
340
|
+
opinion: any;
|
|
341
|
+
date: string;
|
|
342
|
+
hasCode: string;
|
|
343
|
+
isLatest: boolean;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Find the best matching opinion across all brand_history entries.
|
|
348
|
+
* Only returns a match if ALL search terms are found in the same opinion.
|
|
349
|
+
* Returns the opinion with the most matched fields (most recent if tied).
|
|
350
|
+
*/
|
|
351
|
+
export function heartGetBestOpinionMatch(
|
|
352
|
+
brandHistory: any[],
|
|
353
|
+
searches: string[],
|
|
354
|
+
locale: string,
|
|
355
|
+
opinionTypeFilter?: string
|
|
356
|
+
): OpinionMatch | null {
|
|
357
|
+
if (!searches || searches.length === 0) return null;
|
|
358
|
+
|
|
359
|
+
const regex = buildSearchRegex(searches);
|
|
360
|
+
if (!regex) return null;
|
|
361
|
+
|
|
362
|
+
const termRegexes = buildPerTermRegexes(searches);
|
|
363
|
+
if (termRegexes.length === 0) return null;
|
|
364
|
+
|
|
365
|
+
const filtered = opinionTypeFilter
|
|
366
|
+
? brandHistory.filter((o: any) => o.opinion_type === opinionTypeFilter)
|
|
367
|
+
: brandHistory;
|
|
368
|
+
|
|
369
|
+
if (filtered.length === 0) return null;
|
|
370
|
+
|
|
371
|
+
// Sort by date desc to know which is latest
|
|
372
|
+
const sorted = [...filtered].sort(
|
|
373
|
+
(a, b) =>
|
|
374
|
+
new Date(b.decision_date).getTime() - new Date(a.decision_date).getTime()
|
|
375
|
+
);
|
|
376
|
+
const latestOpinionId = sorted[0]?.opinion_id;
|
|
377
|
+
|
|
378
|
+
let bestMatch: OpinionMatch | null = null;
|
|
379
|
+
|
|
380
|
+
for (const opinion of sorted) {
|
|
381
|
+
// Skip opinions where not ALL search terms are found
|
|
382
|
+
if (!opinionMatchesAllTerms(opinion, termRegexes)) continue;
|
|
383
|
+
|
|
384
|
+
const matched: { label: string; tab: string | null }[] = [];
|
|
385
|
+
for (const def of FIELD_DEFS) {
|
|
386
|
+
const found = def.fields.some((f) => {
|
|
387
|
+
const val = opinion[f];
|
|
388
|
+
const valEn = opinion[f + "_en"];
|
|
389
|
+
return fieldContainsSearch(val, regex) || fieldContainsSearch(valEn, regex);
|
|
390
|
+
});
|
|
391
|
+
if (found) {
|
|
392
|
+
matched.push({
|
|
393
|
+
label: locale === "en" ? def.labelEn : def.labelFr,
|
|
394
|
+
tab: def.tab,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (matched.length > 0) {
|
|
400
|
+
if (!bestMatch || matched.length > bestMatch.fields.length) {
|
|
401
|
+
bestMatch = {
|
|
402
|
+
fields: matched,
|
|
403
|
+
opinion,
|
|
404
|
+
date: opinion.decision_date,
|
|
405
|
+
hasCode: opinion.has_code,
|
|
406
|
+
isLatest: opinion.opinion_id === latestOpinionId,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return bestMatch;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface RareExcerpt {
|
|
416
|
+
html: string;
|
|
417
|
+
date: string | null;
|
|
418
|
+
ref: string | null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Fenetre de texte gardee de chaque cote du nom de maladie trouve. */
|
|
422
|
+
const RARE_SNIPPET_RADIUS = 90;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Extraits "pourquoi ça match" pour Maladies Rares.
|
|
426
|
+
*
|
|
427
|
+
* Contrairement a `oncology_tagging`, `rare_diseases` ne porte pas de
|
|
428
|
+
* `section[].highlight` pre-calcule par un pipeline — juste le nom de la
|
|
429
|
+
* maladie (cf. `heartGetRareHighlightTerms`). On reconstruit donc l'extrait
|
|
430
|
+
* cote client : chercher ce nom dans le texte de chaque avis (memes champs
|
|
431
|
+
* que `FIELD_DEFS`, du plus recent au plus ancien) et decouper une fenetre de
|
|
432
|
+
* texte autour de la premiere occurrence trouvee, comme le fait deja
|
|
433
|
+
* `heart-text-highlight` pour le surlignage en place — ici pour un extrait
|
|
434
|
+
* court plutot que tout le champ.
|
|
435
|
+
*/
|
|
436
|
+
export function heartGetRareExcerpts(brandHistory: any[], diseaseNames: string[]): RareExcerpt[] {
|
|
437
|
+
if (!brandHistory || !Array.isArray(brandHistory) || !diseaseNames || diseaseNames.length === 0) return [];
|
|
438
|
+
|
|
439
|
+
const sorted = [...brandHistory].sort(
|
|
440
|
+
(a, b) => new Date(b.decision_date).getTime() - new Date(a.decision_date).getTime()
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const results: RareExcerpt[] = [];
|
|
444
|
+
const seen = new Set<string>();
|
|
445
|
+
|
|
446
|
+
for (const name of diseaseNames) {
|
|
447
|
+
const variants = buildSearchVariants([name]).filter((v) => v.trim() !== "");
|
|
448
|
+
if (variants.length === 0) continue;
|
|
449
|
+
const regex = new RegExp(`(${variants.map(escapeRegExp).join("|")})`, "i");
|
|
450
|
+
|
|
451
|
+
for (const opinion of sorted) {
|
|
452
|
+
let html: string | null = null;
|
|
453
|
+
|
|
454
|
+
outer: for (const def of FIELD_DEFS) {
|
|
455
|
+
for (const f of def.fields) {
|
|
456
|
+
for (const value of [opinion[f], opinion[`${f}_en`]]) {
|
|
457
|
+
if (typeof value !== "string" || !value) continue;
|
|
458
|
+
const match = regex.exec(value);
|
|
459
|
+
if (!match) continue;
|
|
460
|
+
const start = Math.max(0, match.index - RARE_SNIPPET_RADIUS);
|
|
461
|
+
const end = Math.min(value.length, match.index + match[0].length + RARE_SNIPPET_RADIUS);
|
|
462
|
+
const before = (start > 0 ? "… " : "") + value.slice(start, match.index);
|
|
463
|
+
const after = value.slice(match.index + match[0].length, end) + (end < value.length ? " …" : "");
|
|
464
|
+
html = `${before}<mark>${value.slice(match.index, match.index + match[0].length)}</mark>${after}`;
|
|
465
|
+
break outer;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (html && !seen.has(html)) {
|
|
471
|
+
seen.add(html);
|
|
472
|
+
results.push({ html, date: opinion.decision_date ?? null, ref: opinion.has_code ?? null });
|
|
473
|
+
}
|
|
474
|
+
if (html) break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return results;
|
|
479
|
+
}
|