@tekkare/romulus 0.1.250 → 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.
Files changed (270) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/BarChart.vue +1 -0
  3. package/dist/runtime/components/ChoroplethMap.vue +4 -1
  4. package/dist/runtime/internal/geo/fr-regions.json +1 -1
  5. package/dist/runtime/utils/chart.d.ts +6 -3
  6. package/dist/runtime/utils/chart.js +15 -2
  7. package/layer/app/app.config.ts +6 -0
  8. package/layer/app/components/OipPubmedSummary.vue +223 -0
  9. package/layer/app/components/OipResourceDrawer.vue +331 -0
  10. package/layer/app/components/OipResourceDrawerLink.vue +40 -0
  11. package/layer/app/components/OipResourceField.vue +41 -0
  12. package/layer/app/components/OipResourceGenericCard.vue +54 -0
  13. package/layer/app/components/OipResourceSource.vue +50 -0
  14. package/layer/app/components/OipResourceTags.vue +65 -0
  15. package/layer/app/components/OipResourceView.vue +107 -0
  16. package/layer/app/components/oip-drugs/fiches/AssociationFiche.vue +775 -0
  17. package/layer/app/components/oip-drugs/fiches/BrandFiche.vue +1111 -0
  18. package/layer/app/components/oip-drugs/fiches/IndicationFiche.vue +182 -0
  19. package/layer/app/components/oip-drugs/fiches/JoFiche.vue +101 -0
  20. package/layer/app/components/oip-drugs/fiches/PndsFiche.vue +279 -0
  21. package/layer/app/components/oip-drugs/heart/JoCard.vue +314 -0
  22. package/layer/app/components/oip-drugs/heart/TextHighlight.vue +326 -0
  23. package/layer/app/components/oip-drugs/heart/ansm/BrandCard.vue +187 -0
  24. package/layer/app/components/oip-drugs/heart/ansm/CisDetail.vue +363 -0
  25. package/layer/app/components/oip-drugs/heart/ansm/CisTable.vue +167 -0
  26. package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyCard.vue +373 -0
  27. package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyFiche.vue +372 -0
  28. package/layer/app/components/oip-drugs/heart/associations/TranscriptionCard.vue +55 -0
  29. package/layer/app/components/oip-drugs/heart/elastic/SourceContainerElastic.vue +103 -0
  30. package/layer/app/components/oip-drugs/heart/france/BrandGroupCard.vue +450 -0
  31. package/layer/app/components/oip-drugs/heart/mitm/Card.vue +199 -0
  32. package/layer/app/components/oip-drugs/heart/mitm/Source.vue +23 -0
  33. package/layer/app/components/oip-drugs/heart/mitm/details/Overview.vue +381 -0
  34. package/layer/app/components/oip-drugs/heart/put/Card.vue +212 -0
  35. package/layer/app/components/oip-drugs/heart/results/AccesCard.vue +696 -0
  36. package/layer/app/components/oip-drugs/heart/results/AlertCard.vue +342 -0
  37. package/layer/app/components/oip-drugs/heart/results/AsmrCursor.vue +229 -0
  38. package/layer/app/components/oip-drugs/heart/results/EvalCard.vue +370 -0
  39. package/layer/app/components/oip-drugs/heart/results/IndicCard.vue +743 -0
  40. package/layer/app/components/oip-drugs/heart/results/MatchExplain.vue +243 -0
  41. package/layer/app/components/oip-drugs/heart/results/PrefRows.vue +105 -0
  42. package/layer/app/components/oip-drugs/heart/results/details/HasHistory.vue +487 -0
  43. package/layer/app/components/oip-drugs/heart/results/details/InternationalCountryCard.vue +461 -0
  44. package/layer/app/components/oip-drugs/heart/results/details/InternationalOverview.vue +200 -0
  45. package/layer/app/components/oip-drugs/heart/results/details/InternationalPriceDrawer.vue +647 -0
  46. package/layer/app/components/oip-drugs/heart/results/details/Monograph.vue +128 -0
  47. package/layer/app/components/oip-drugs/heart/results/details/OpinionDrawer.vue +535 -0
  48. package/layer/app/components/oip-drugs/heart/results/details/PresentationDrawer.vue +738 -0
  49. package/layer/app/components/oip-drugs/heart/results/details/Price.vue +227 -0
  50. package/layer/app/components/oip-drugs/heart/results/details/PriceOverview.vue +1261 -0
  51. package/layer/app/components/oip-drugs/heart/results/details/RecordDrawer.vue +494 -0
  52. package/layer/app/components/oip-drugs/heart/results/details/RecordList.vue +426 -0
  53. package/layer/app/components/oip-drugs/heart/results/les/Card.vue +340 -0
  54. package/layer/app/components/oip-drugs/heart/results/les/Drawer.vue +67 -0
  55. package/layer/app/components/oip-drugs/heart/results/les/IndicationDetail.vue +149 -0
  56. package/layer/app/components/oip-drugs/heart/results/les/Source.vue +20 -0
  57. package/layer/app/components/oip-drugs/heart/results/les/SourceTag.vue +39 -0
  58. package/layer/app/components/oip-drugs/heart/searchPage/DynamicSource.vue +29 -0
  59. package/layer/app/components/oip-drugs/medicam/market/Chart.vue +121 -0
  60. package/layer/app/components/oip-drugs/outpatient/ProductEvolution.vue +236 -0
  61. package/layer/app/components/oip-drugs/resources/Ansm.vue +75 -0
  62. package/layer/app/components/oip-drugs/resources/Association.vue +30 -0
  63. package/layer/app/components/oip-drugs/resources/Brand.vue +65 -0
  64. package/layer/app/components/oip-drugs/resources/Contribution.vue +216 -0
  65. package/layer/app/components/oip-drugs/resources/Hospital.vue +151 -0
  66. package/layer/app/components/oip-drugs/resources/Indication.vue +54 -0
  67. package/layer/app/components/oip-drugs/resources/Jo.vue +57 -0
  68. package/layer/app/components/oip-drugs/resources/Medicam.vue +140 -0
  69. package/layer/app/components/oip-drugs/resources/Mitm.vue +84 -0
  70. package/layer/app/components/oip-drugs/resources/NationalAssembly.vue +93 -0
  71. package/layer/app/components/oip-drugs/resources/Outpatient.vue +97 -0
  72. package/layer/app/components/oip-drugs/resources/Pnds.vue +24 -0
  73. package/layer/app/components/oip-drugs/resources/Price.vue +106 -0
  74. package/layer/app/components/oip-drugs/resources/Put.vue +143 -0
  75. package/layer/app/components/oip-science/appel-a-projet/Cards.vue +162 -0
  76. package/layer/app/components/oip-science/clinical-trials/Cards.vue +382 -0
  77. package/layer/app/components/oip-science/clinical-trials/CardsGlobal.vue +273 -0
  78. package/layer/app/components/oip-science/clinical-trials/LinkedList.vue +366 -0
  79. package/layer/app/components/oip-science/clinical-trials/PairBadge.vue +25 -0
  80. package/layer/app/components/oip-science/clinical-trials/PairBody.vue +182 -0
  81. package/layer/app/components/oip-science/clinical-trials/SitesDrawer.vue +95 -0
  82. package/layer/app/components/oip-science/common/Form.vue +73 -0
  83. package/layer/app/components/oip-science/common/MatchAvgTag.vue +283 -0
  84. package/layer/app/components/oip-science/common/NoData.vue +16 -0
  85. package/layer/app/components/oip-science/common/ScoreBadge.vue +35 -0
  86. package/layer/app/components/oip-science/common/Source.vue +31 -0
  87. package/layer/app/components/oip-science/common/TableTags.vue +279 -0
  88. package/layer/app/components/oip-science/common/Timeline.vue +104 -0
  89. package/layer/app/components/oip-science/common/ToolTips.vue +89 -0
  90. package/layer/app/components/oip-science/cross/publicationPubmed.vue +484 -0
  91. package/layer/app/components/oip-science/crossData/CrossDataLocations.vue +71 -0
  92. package/layer/app/components/oip-science/ctgov/Investigators.vue +148 -0
  93. package/layer/app/components/oip-science/ctgov/Locations.vue +102 -0
  94. package/layer/app/components/oip-science/ctgov/Publications.vue +52 -0
  95. package/layer/app/components/oip-science/ctgov/Sponsors.vue +66 -0
  96. package/layer/app/components/oip-science/ctgov/StudyDescription.vue +1136 -0
  97. package/layer/app/components/oip-science/ctgov-centers/ClinicalTrials.vue +40 -0
  98. package/layer/app/components/oip-science/ctgov-centers/Publications.vue +50 -0
  99. package/layer/app/components/oip-science/ctgov-investigators/ClinicalTrials.vue +43 -0
  100. package/layer/app/components/oip-science/ctgov-investigators/Informations.vue +171 -0
  101. package/layer/app/components/oip-science/ctgov-investigators/Publications.vue +44 -0
  102. package/layer/app/components/oip-science/elastic/DateRangeFilter.vue +209 -0
  103. package/layer/app/components/oip-science/elastic/Filter.vue +719 -0
  104. package/layer/app/components/oip-science/elastic/MatchExplain.vue +152 -0
  105. package/layer/app/components/oip-science/establishments/Cards.vue +183 -0
  106. package/layer/app/components/oip-science/establishments/ClinicalTrials.vue +62 -0
  107. package/layer/app/components/oip-science/establishments/Hcps.vue +227 -0
  108. package/layer/app/components/oip-science/establishments/Informations.vue +263 -0
  109. package/layer/app/components/oip-science/establishments/LinkedList.vue +153 -0
  110. package/layer/app/components/oip-science/establishments/MatchedLabels.vue +194 -0
  111. package/layer/app/components/oip-science/establishments/PublicFinance.vue +62 -0
  112. package/layer/app/components/oip-science/euct/Investigators.vue +125 -0
  113. package/layer/app/components/oip-science/euct/Locations.vue +102 -0
  114. package/layer/app/components/oip-science/euct/Products.vue +101 -0
  115. package/layer/app/components/oip-science/euct/Publications.vue +89 -0
  116. package/layer/app/components/oip-science/euct/Sponsors.vue +86 -0
  117. package/layer/app/components/oip-science/euct/StudyDescription.vue +1075 -0
  118. package/layer/app/components/oip-science/euct-centers/ClinicalTrials.vue +40 -0
  119. package/layer/app/components/oip-science/euct-centers/Publications.vue +55 -0
  120. package/layer/app/components/oip-science/euct-investigators/ClinicalTrials.vue +41 -0
  121. package/layer/app/components/oip-science/euct-investigators/Informations.vue +171 -0
  122. package/layer/app/components/oip-science/euct-investigators/Publications.vue +51 -0
  123. package/layer/app/components/oip-science/hcps/AppelsAProjet.vue +62 -0
  124. package/layer/app/components/oip-science/hcps/Cards.vue +237 -0
  125. package/layer/app/components/oip-science/hcps/ClinicalTrials.vue +78 -0
  126. package/layer/app/components/oip-science/hcps/Informations.vue +296 -0
  127. package/layer/app/components/oip-science/hcps/LinkedList.vue +153 -0
  128. package/layer/app/components/oip-science/hcps/MatchedLabels.vue +207 -0
  129. package/layer/app/components/oip-science/hcps/Pubmed.vue +128 -0
  130. package/layer/app/components/oip-science/local/SliderTabs.vue +99 -0
  131. package/layer/app/components/oip-science/local/SliderTabsFull.vue +135 -0
  132. package/layer/app/components/oip-science/pubmed/Authors.vue +128 -0
  133. package/layer/app/components/oip-science/pubmed/Cards.vue +417 -0
  134. package/layer/app/components/oip-science/pubmed/ClinicalTrials.vue +71 -0
  135. package/layer/app/components/oip-science/resources/Phrc.vue +39 -0
  136. package/layer/app/composables/useOipChapterScroll.ts +43 -0
  137. package/layer/app/composables/useOipResourceHooks.ts +27 -0
  138. package/layer/app/composables/useOipResourceNav.ts +66 -0
  139. package/layer/app/composables/useOipResourceViews.ts +117 -0
  140. package/layer/app/composables/useOipResources.ts +83 -0
  141. package/layer/app/oip-drugs/PORTING.md +120 -0
  142. package/layer/app/oip-drugs/composables/useAssociationDetail.ts +220 -0
  143. package/layer/app/oip-drugs/composables/useAssociationExport.ts +63 -0
  144. package/layer/app/oip-drugs/composables/useAssociationLookup.ts +105 -0
  145. package/layer/app/oip-drugs/composables/useCardPrefOptions.ts +201 -0
  146. package/layer/app/oip-drugs/composables/useCurrency.ts +121 -0
  147. package/layer/app/oip-drugs/composables/useDirectusSources.ts +224 -0
  148. package/layer/app/oip-drugs/composables/useHeartBrandMatches.ts +112 -0
  149. package/layer/app/oip-drugs/composables/useHeartDetailSearches.ts +479 -0
  150. package/layer/app/oip-drugs/composables/useHeartIntlApprovals.ts +661 -0
  151. package/layer/app/oip-drugs/composables/useHeartLesIndication.ts +168 -0
  152. package/layer/app/oip-drugs/composables/useHeartMatchExplain.ts +231 -0
  153. package/layer/app/oip-drugs/composables/useHeartReports.ts +239 -0
  154. package/layer/app/oip-drugs/composables/useHeartSearchFields.ts +233 -0
  155. package/layer/app/oip-drugs/composables/useMergeByKey.ts +23 -0
  156. package/layer/app/oip-drugs/composables/useOipDrugsElastic.ts +621 -0
  157. package/layer/app/oip-drugs/composables/usePageEmpty.ts +58 -0
  158. package/layer/app/oip-drugs/composables/v-useSalesSource.ts +64 -0
  159. package/layer/app/oip-drugs/drugs.css +500 -0
  160. package/layer/app/oip-drugs/esIndices.ts +322 -0
  161. package/layer/app/oip-drugs/kinds.ts +264 -0
  162. package/layer/app/oip-drugs/locales/en.json +1571 -0
  163. package/layer/app/oip-drugs/locales/fr.json +1571 -0
  164. package/layer/app/oip-drugs/monographs/blincyto-has.vue +289 -0
  165. package/layer/app/oip-drugs/monographs/darzalex-has.vue +276 -0
  166. package/layer/app/oip-drugs/monographs/dupixent-has.vue +216 -0
  167. package/layer/app/oip-drugs/monographs/enhertu-has.vue +280 -0
  168. package/layer/app/oip-drugs/monographs/has-sein-2023.vue +1649 -0
  169. package/layer/app/oip-drugs/monographs/ibrance-has.vue +277 -0
  170. package/layer/app/oip-drugs/monographs/imfinzi-has.vue +293 -0
  171. package/layer/app/oip-drugs/monographs/keytruda-has.vue +334 -0
  172. package/layer/app/oip-drugs/monographs/kisqali-has.vue +201 -0
  173. package/layer/app/oip-drugs/monographs/opdivo-has.vue +231 -0
  174. package/layer/app/oip-drugs/monographs/ozempic-has.vue +186 -0
  175. package/layer/app/oip-drugs/monographs/tecentriq-has.vue +312 -0
  176. package/layer/app/oip-drugs/monographs/yescarta-has.vue +272 -0
  177. package/layer/app/oip-drugs/types/elastic.ts +113 -0
  178. package/layer/app/oip-drugs/types/es/agencies.ts +152 -0
  179. package/layer/app/oip-drugs/types/es/directory.ts +54 -0
  180. package/layer/app/oip-drugs/types/es/docByIndex.ts +151 -0
  181. package/layer/app/oip-drugs/types/es/openmedic.ts +67 -0
  182. package/layer/app/oip-drugs/types/es/outpatient.ts +71 -0
  183. package/layer/app/oip-drugs/types/es/portal.ts +130 -0
  184. package/layer/app/oip-drugs/types/es/retail.ts +107 -0
  185. package/layer/app/oip-drugs/types/es/statistics.ts +29 -0
  186. package/layer/app/oip-drugs/types/heart/CurrencyRate.ts +16 -0
  187. package/layer/app/oip-drugs/types/heart/ansm.ts +106 -0
  188. package/layer/app/oip-drugs/types/heart/tag.ts +29 -0
  189. package/layer/app/oip-drugs/utils/associationFields.ts +118 -0
  190. package/layer/app/oip-drugs/utils/contributions.ts +36 -0
  191. package/layer/app/oip-drugs/utils/countryMapping.ts +387 -0
  192. package/layer/app/oip-drugs/utils/esHighlight.ts +21 -0
  193. package/layer/app/oip-drugs/utils/exportSheets.ts +57 -0
  194. package/layer/app/oip-drugs/utils/heartIntlPrice.ts +228 -0
  195. package/layer/app/oip-drugs/utils/heartStyleAndFormat.ts +262 -0
  196. package/layer/app/oip-drugs/utils/heartStyleAnsm.ts +71 -0
  197. package/layer/app/oip-drugs/utils/latestDate.ts +31 -0
  198. package/layer/app/oip-drugs/utils/p-heartCardTags.ts +53 -0
  199. package/layer/app/oip-drugs/utils/priceExport.ts +269 -0
  200. package/layer/app/oip-drugs/utils/q-hasRatings.ts +43 -0
  201. package/layer/app/oip-drugs/utils/s-openCard.ts +20 -0
  202. package/layer/app/oip-drugs/utils/sourceUuids.ts +155 -0
  203. package/layer/app/oip-drugs/utils/styleAndFormat.ts +40 -0
  204. package/layer/app/oip-drugs/utils/v-salesSeries.ts +156 -0
  205. package/layer/app/oip-drugs/utils/v-scopeTags.ts +23 -0
  206. package/layer/app/oip-science/PORTING.md +93 -0
  207. package/layer/app/oip-science/assets/smartSelection.json +428 -0
  208. package/layer/app/oip-science/composables/useAuthorMatching.ts +342 -0
  209. package/layer/app/oip-science/composables/useCTCenter.ts +107 -0
  210. package/layer/app/oip-science/composables/useCTInvestigators.ts +107 -0
  211. package/layer/app/oip-science/composables/useEstablishmentCrossData.ts +139 -0
  212. package/layer/app/oip-science/composables/useEstablishmentMatching.ts +176 -0
  213. package/layer/app/oip-science/composables/useEuct.ts +418 -0
  214. package/layer/app/oip-science/composables/useEuctCenter.ts +107 -0
  215. package/layer/app/oip-science/composables/useEuctInvestigators.ts +107 -0
  216. package/layer/app/oip-science/composables/useGlobalClinicalTrials.ts +624 -0
  217. package/layer/app/oip-science/composables/useLinkedTrials.ts +83 -0
  218. package/layer/app/oip-science/composables/useOipElastic.ts +658 -0
  219. package/layer/app/oip-science/composables/useOipSearchExplain.ts +166 -0
  220. package/layer/app/oip-science/composables/usePubmed.ts +487 -0
  221. package/layer/app/oip-science/composables/useSearchFields.ts +153 -0
  222. package/layer/app/oip-science/indexConfig.json +794 -0
  223. package/layer/app/oip-science/locales/en.json +483 -0
  224. package/layer/app/oip-science/locales/fr.json +483 -0
  225. package/layer/app/oip-science/science.css +1182 -0
  226. package/layer/app/oip-science/utils/a-openCard.ts +23 -0
  227. package/layer/app/oip-science/utils/b-openCard.ts +35 -0
  228. package/layer/app/oip-science/utils/c-filterContext.ts +81 -0
  229. package/layer/app/oip-science/utils/f-types.ts +241 -0
  230. package/layer/app/oip-science/utils/frDiseaseTerms.ts +269 -0
  231. package/layer/app/oip-science/utils/matchMethod.ts +53 -0
  232. package/layer/app/oip-science/utils/sliderTabs.ts +13 -0
  233. package/layer/app/oip-science/utils/sources.ts +15 -0
  234. package/layer/app/oip-science/utils/styleAndFormat.ts +608 -0
  235. package/layer/app/oip-science/utils/toPresets.ts +7 -0
  236. package/layer/app/oip-science/utils/valueTable.ts +126 -0
  237. package/layer/app/plugins/oip-science-i18n.ts +17 -0
  238. package/layer/app/utils/oipFicheRoutes.ts +118 -0
  239. package/layer/app/utils/oipResources.ts +440 -0
  240. package/layer/nuxt.config.ts +22 -2
  241. package/layer/public/reports/blincyto-has.html +292 -0
  242. package/layer/public/reports/blincyto-has.pdf +0 -0
  243. package/layer/public/reports/darzalex-has.html +279 -0
  244. package/layer/public/reports/darzalex-has.pdf +0 -0
  245. package/layer/public/reports/dupixent-has.html +217 -0
  246. package/layer/public/reports/dupixent-has.pdf +0 -0
  247. package/layer/public/reports/enhertu-has.html +283 -0
  248. package/layer/public/reports/enhertu-has.pdf +0 -0
  249. package/layer/public/reports/has-sein-2023.html +1639 -0
  250. package/layer/public/reports/ibrance-has.html +280 -0
  251. package/layer/public/reports/ibrance-has.pdf +0 -0
  252. package/layer/public/reports/imfinzi-has.html +297 -0
  253. package/layer/public/reports/imfinzi-has.pdf +0 -0
  254. package/layer/public/reports/keytruda-has.html +337 -0
  255. package/layer/public/reports/keytruda-has.pdf +0 -0
  256. package/layer/public/reports/kisqali-has.html +202 -0
  257. package/layer/public/reports/kisqali-has.pdf +0 -0
  258. package/layer/public/reports/opdivo-has.html +232 -0
  259. package/layer/public/reports/opdivo-has.pdf +0 -0
  260. package/layer/public/reports/ozempic-has.html +188 -0
  261. package/layer/public/reports/ozempic-has.pdf +0 -0
  262. package/layer/public/reports/tecentriq-has.html +315 -0
  263. package/layer/public/reports/tecentriq-has.pdf +0 -0
  264. package/layer/public/reports/yescarta-has.html +275 -0
  265. package/layer/public/reports/yescarta-has.pdf +0 -0
  266. package/layer/server/api/oip/es.post.ts +99 -0
  267. package/layer/server/api/oip/pdf.get.ts +70 -0
  268. package/layer/server/api/oip/reports.get.ts +98 -0
  269. package/layer/server/api/oip/resource-types.get.ts +22 -0
  270. 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
+ }