@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,168 @@
1
+ import { ES_INDEX } from "../esIndices";
2
+ import useOipDrugsElastic from "./useOipDrugsElastic";
3
+ /**
4
+ * Liste en sus (LES) : lecture d'une indication.
5
+ *
6
+ * L'index LES tient une ligne par couple indication x produit. Une indication
7
+ * (code CPEMB13, I000711...) se lit donc en rassemblant ses lignes : la carte
8
+ * d'identite (libelle, produits, marques) et l'historique d'inscription de
9
+ * chaque produit. Ces lectures servent a la page `indication/[id]` et au tiroir
10
+ * LES de la fiche marque, pour que les deux montrent exactement la meme chose.
11
+ */
12
+
13
+ /** Index de la liste en sus, sur le cluster « pubmed ». */
14
+ export const HEART_LES_INDEX = ES_INDEX.INDICATION_SEARCH_HAS;
15
+
16
+ /** Colonnes d'historique, montrees seulement si au moins un produit les renseigne. */
17
+ const HISTORY_FIELDS = [
18
+ // `flag` (OUI / NON) et `tag` sont rendus en pastilles par indicationDetail.vue.
19
+ { field: "statut", type: "flag", name: "Status", frName: "Statut" },
20
+ { field: "drug_type", type: "tag", name: "Drug Type", frName: "Type" },
21
+ { field: "start_date", type: "date", name: "Start Date", frName: "Date Début" },
22
+ { field: "amm_date", type: "date", name: "AMM Date", frName: "Date AMM" },
23
+ { field: "end_date", type: "date", name: "End Date", frName: "Date Fin" },
24
+ { field: "end_motive", type: "", name: "End Motive", frName: "Motif Fin" },
25
+ {
26
+ field: "end_date_treatment",
27
+ type: "date",
28
+ name: "Treatment End Date",
29
+ frName: "Date Fin Traitement",
30
+ },
31
+ {
32
+ field: "end_date_contract",
33
+ type: "date",
34
+ name: "Contract End Date",
35
+ frName: "Date Fin Contrat",
36
+ },
37
+ ];
38
+
39
+ export type LesHistoryField = (typeof HISTORY_FIELDS)[number];
40
+
41
+ /** Toutes les lignes d'une indication. */
42
+ export function useHeartLesIndicationLoader() {
43
+ const { queryCustomElastic } = useOipDrugsElastic();
44
+ const results = ref<any[] | null>(null);
45
+ const isLoading = ref(false);
46
+
47
+ async function load(code: string) {
48
+ isLoading.value = true;
49
+ const res = await queryCustomElastic({
50
+ index: HEART_LES_INDEX,
51
+ // `indication` est analyse : un `match` ramenait les indications
52
+ // voisines. La page liste agrege sur `indication.keyword`, meme champ ici.
53
+ customQuery: { query: { term: { "indication.keyword": code } }, size: 100 },
54
+ cluster: "pubmed",
55
+ }).catch(() => null);
56
+ results.value = res?.results ?? null;
57
+ isLoading.value = false;
58
+ }
59
+
60
+ return { results, isLoading, load };
61
+ }
62
+
63
+ /** Ce que la carte LES attend : l'indication et ses produits. */
64
+ export function heartLesCardData(code: string, results: any[] | null) {
65
+ if (!results?.length) return null;
66
+ return {
67
+ indication: code,
68
+ indication_label_fr: results[0].indication_label_fr ?? "",
69
+ all_brand_by_ind:
70
+ results[0].all_brand_by_ind ??
71
+ results.map((r: any) => `[${r.brand} (${r.owner_regroup})]`).join(""),
72
+ products: results.map((r: any) => ({
73
+ atc5_fr: r.atc5_fr,
74
+ brand: r.brand,
75
+ owner_regroup: r.owner_regroup,
76
+ source: r.source,
77
+ start_date: r.start_date,
78
+ })),
79
+ };
80
+ }
81
+
82
+ export function heartLesAvailableFields(results: any[] | null): LesHistoryField[] {
83
+ if (!results) return [];
84
+ return HISTORY_FIELDS.filter((f) => results.find((a) => a[f.field] !== null));
85
+ }
86
+
87
+ /**
88
+ * En-tete de la colonne statut. Le titre de l'index (« Accès et prise en charge
89
+ * au titre de l'accès précoce ») tient sur six lignes dans une colonne : on le
90
+ * ramene a l'essentiel, la valeur OUI / NON dit le reste.
91
+ */
92
+ function shortStatusTitle(title: string | null | undefined, fr: boolean) {
93
+ const value = title ?? "";
94
+ if (/acc[eè]s pr[eé]coce/i.test(value)) return fr ? "Accès précoce" : "Early access";
95
+ if (/liste en sus/i.test(value)) return fr ? "Liste en sus" : "Liste en sus";
96
+ return value;
97
+ }
98
+
99
+ /**
100
+ * Table d'historique : un produit par ligne, du plus recent au plus ancien.
101
+ *
102
+ * Peu de colonnes, pour tenir sans defilement horizontal : le fabricant, l'UCD
103
+ * et le type montent en pastilles dans la cellule du produit, et les dates de
104
+ * debut et de fin forment une seule periode.
105
+ */
106
+ export function heartLesHistoryTable(
107
+ results: any[] | null,
108
+ locale: string,
109
+ companyHeader: string,
110
+ ) {
111
+ const fields = heartLesAvailableFields(results);
112
+ const has = (field: string) => fields.some((f) => f.field === field);
113
+ const fr = locale === "fr";
114
+ const statusName = has("statut")
115
+ ? shortStatusTitle(results?.find((a) => a.statut_title !== null)?.statut_title, fr)
116
+ : "";
117
+
118
+ const header: string[] = [fr ? "Produit" : "Product", "Source"];
119
+ const type: string[] = ["product", "source"];
120
+ const size: number[] = [4, 2];
121
+ const cell: ((row: any) => any)[] = [
122
+ (row) => ({
123
+ label: row.french_label,
124
+ ucd: row.ucd13_code ? row.ucd13_code : row.ucd7_code,
125
+ company: row.owner_regroup,
126
+ type: has("drug_type") ? row.drug_type : null,
127
+ }),
128
+ (row) => row.source,
129
+ ];
130
+
131
+ let periodDone = false;
132
+ for (const f of fields) {
133
+ // Deja dans la cellule produit.
134
+ if (f.field === "drug_type") continue;
135
+ // Debut et fin : une seule colonne « Periode ».
136
+ if (f.field === "start_date" || f.field === "end_date") {
137
+ if (periodDone) continue;
138
+ periodDone = true;
139
+ header.push(fr ? "Période" : "Period");
140
+ type.push("period");
141
+ size.push(3);
142
+ cell.push((row) => ({ start: row.start_date ?? null, end: row.end_date ?? null }));
143
+ continue;
144
+ }
145
+ header.push(f.name === "Status" ? statusName : fr ? f.frName : f.name);
146
+ type.push(f.type);
147
+ size.push(f.field === "end_motive" ? 3 : 2);
148
+ cell.push((row) => row[f.field]);
149
+ }
150
+
151
+ const data = (results ?? []).map((row: any) => ({ values: cell.map((get) => get(row)) }));
152
+
153
+ const time = (value: any) => (value ? new Date(value).getTime() || 0 : 0);
154
+ const periodIndex = type.indexOf("period");
155
+ const dateIndex = type.indexOf("date");
156
+ if (periodIndex >= 0) {
157
+ data.sort((a, b) => time(b.values[periodIndex]?.start) - time(a.values[periodIndex]?.start));
158
+ } else if (dateIndex >= 0) {
159
+ data.sort((a, b) => time(b.values[dateIndex]) - time(a.values[dateIndex]));
160
+ }
161
+
162
+ return {
163
+ header,
164
+ type,
165
+ style: { align: type.map(() => "left" as const), size },
166
+ data,
167
+ };
168
+ }
@@ -0,0 +1,231 @@
1
+ import { heartSearchFieldGroup, heartSearchGroupByKey, heartSearchGroupLabel } from "./useHeartSearchFields";
2
+
3
+ /**
4
+ * Pourquoi un resultat sort.
5
+ *
6
+ * L'explication vient d'Elasticsearch, pas d'une reconstitution cote client :
7
+ * chaque clause de recherche embarque des `inner_hits` nommes `search:<famille>`
8
+ * qui renvoient les avis matches et les extraits surlignes, et chaque filtre
9
+ * nested embarque les siens, nommes d'apres le filtre. Le moteur dit donc
10
+ * lui-meme OU il a trouve les termes et COMBIEN d'avis sont concernes — la ou
11
+ * une detection par expression reguliere ne pouvait qu'approcher son analyse
12
+ * (accents, pluriels, synonymes, proximite des mots).
13
+ */
14
+
15
+ export type MatchFieldExplain = {
16
+ /** Cle de famille (`indication`, `essential`...). */
17
+ key: string;
18
+ label: string;
19
+ /** Onglet de la fiche produit qui porte ce contenu. */
20
+ tab: string | null;
21
+ /** Nombre d'avis de la marque ou la famille matche. */
22
+ count: number;
23
+ /** Extraits surlignes en `<mark>`, tels que renvoyes par le moteur, avec la date de l'avis d'ou ils viennent. */
24
+ fragments: { html: string; date: string | null; ref?: string | null }[];
25
+ };
26
+
27
+ export type MatchExplain = {
28
+ /** Familles de champs ou les termes ont ete trouves, la plus fournie d'abord. */
29
+ fields: MatchFieldExplain[];
30
+ /** Filtres retenus par ce resultat, avec le nombre d'avis concernes. */
31
+ filters: { name: string; label: string; count: number }[];
32
+ /** Identifiants des avis matches, pour marquer les cartes de la fiche. */
33
+ opinionIds: string[];
34
+ hasCodes: string[];
35
+ };
36
+
37
+ const SEARCH_PREFIX = "search:";
38
+
39
+ /**
40
+ * Surlignage d'un champ `keyword` : faute de sous-champ analyse, le moteur
41
+ * traite la valeur comme un seul terme et la renvoie entierement enveloppee.
42
+ * L'annuaire des associations en donne l'exemple — chercher « keytruda »
43
+ * peignait en rose les quinze appellations de l'association d'un bloc. Un tel
44
+ * extrait ne montre rien : la famille garde sa pastille, l'extrait tombe.
45
+ */
46
+ const WHOLE_VALUE = /^<mark>[^<]*<\/mark>$/;
47
+
48
+ /**
49
+ * Bloc `highlight` a poser a la racine d'une requete sur un index plat.
50
+ * Les balises sont demandees en `<mark>`, comme pour les inner hits : c'est la
51
+ * balise que la feuille du hub peint en rose.
52
+ */
53
+ export function heartSearchHighlight(fields: string[]): Record<string, any> | undefined {
54
+ if (!fields.length) return undefined;
55
+ return {
56
+ pre_tags: ["<mark>"],
57
+ post_tags: ["</mark>"],
58
+ fields: Object.fromEntries(
59
+ fields.map((field) => [field, { number_of_fragments: 2, fragment_size: 180 }]),
60
+ ),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Bloc `highlight` depose par la barre de recherche dans le magasin de
66
+ * requetes. Une page le passe tel quel a `queryElastic`.
67
+ */
68
+ export function heartHighlightFromQueries(queries: any[]): Record<string, any> | undefined {
69
+ return queries.find((query: any) => query?.highlight)?.highlight;
70
+ }
71
+
72
+ /**
73
+ * Filtres dont les inner hits ne disent rien a l'utilisateur. `searchType` est
74
+ * le perimetre de l'onglet courant, pose sur toutes les requetes : sans filtre
75
+ * choisi, chaque carte affichait « Correspondance dans 7 indications ».
76
+ * `patientContribution` est l'inner hit de ce meme perimetre sur l'onglet
77
+ * Contributions (cf. heartSearchType) : chaque carte affichait sinon
78
+ * « Filtres retenus patientContribution 3 ».
79
+ */
80
+ const SILENT_FILTERS = new Set(["Take", "Sort", "Type", "Search Filter", "searchType", "patientContribution"]);
81
+
82
+ /**
83
+ * Nom technique du filtre (`filterName` pose sur `HeartElastic*Filter`) vers
84
+ * son libelle affichable. Sans cette table, la pastille montre le nom de
85
+ * variable tel quel (« oncologyIcd10 ») plutot que ce que l'utilisateur a
86
+ * choisi dans le panneau de filtres.
87
+ */
88
+ const FILTER_LABELS: Record<string, { fr: string; en: string }> = {
89
+ motif: { fr: "Motif", en: "Reason" },
90
+ brand: { fr: "Marque", en: "Brand" },
91
+ molecule: { fr: "Molécule", en: "Molecule" },
92
+ atc: { fr: "ACT4", en: "ACT4" },
93
+ ATC4: { fr: "ACT4", en: "ACT4" },
94
+ company: { fr: "Fabricant", en: "Manufacturer" },
95
+ smr: { fr: "SMR", en: "SMR" },
96
+ asmr: { fr: "ASMR", en: "ASMR" },
97
+ impact: { fr: "Impact CT", en: "CT Impact" },
98
+ biosimilar: { fr: "Biosimilaire", en: "Biosimilar" },
99
+ containsRare: { fr: "Maladies Rares", en: "Rare Diseases" },
100
+ rare: { fr: "Maladies Rares", en: "Rare Diseases" },
101
+ containsOnco: { fr: "Oncologie", en: "Oncology" },
102
+ oncologyTerms: { fr: "Oncology Terms", en: "Oncology Terms" },
103
+ oncologyIcd10: { fr: "Oncology ICD-10", en: "Oncology ICD-10" },
104
+ periodRange: { fr: "Période", en: "Period" },
105
+ association: { fr: "Association", en: "Association" },
106
+ parliamentarySession: { fr: "Session parlementaire", en: "Parliamentary session" },
107
+ opinion: { fr: "Avis", en: "Opinion" },
108
+ status: { fr: "Statut", en: "Status" },
109
+ target: { fr: "Cible", en: "Target" },
110
+ Distributions: { fr: "Distribution", en: "Distribution" },
111
+ };
112
+
113
+ function filterLabel(name: string, locale: string): string {
114
+ const known = FILTER_LABELS[name];
115
+ return known ? (locale === "en" ? known.en : known.fr) : name;
116
+ }
117
+
118
+ function fragmentsOf(innerHit: any): { html: string; date: string | null }[] {
119
+ return (innerHit?.hits?.hits ?? []).flatMap((hit: any) => {
120
+ const date = hit?._source?.decision_date ?? null;
121
+ return (Object.values(hit?.highlight ?? {}).flat() as string[]).map((html) => ({ html, date }));
122
+ });
123
+ }
124
+
125
+ function idsOf(innerHit: any) {
126
+ const ids: string[] = [];
127
+ const codes: string[] = [];
128
+ for (const hit of innerHit?.hits?.hits ?? []) {
129
+ const source = hit?._source ?? {};
130
+ if (source.opinion_id) ids.push(String(source.opinion_id));
131
+ if (source.has_code) codes.push(String(source.has_code));
132
+ }
133
+ return { ids, codes };
134
+ }
135
+
136
+ /**
137
+ * Lit les inner hits d'un resultat et en tire l'explication.
138
+ * Retourne `null` quand le resultat ne porte aucune explication — recherche
139
+ * vide, ou requete lancee sans inner hits.
140
+ */
141
+ export function heartExplainMatch(hit: any, locale: string): MatchExplain | null {
142
+ const inner = hit?.inner_hits;
143
+ const highlight = hit?.highlight;
144
+ if ((!inner || typeof inner !== "object") && (!highlight || typeof highlight !== "object")) {
145
+ return null;
146
+ }
147
+
148
+ const fields: MatchFieldExplain[] = [];
149
+ const filters: { name: string; label: string; count: number }[] = [];
150
+ const opinionIds = new Set<string>();
151
+ const hasCodes = new Set<string>();
152
+
153
+ for (const [name, innerHit] of Object.entries<any>(inner ?? {})) {
154
+ const count = innerHit?.hits?.total?.value ?? 0;
155
+ if (!count) continue;
156
+
157
+ if (name.startsWith(SEARCH_PREFIX)) {
158
+ const group = heartSearchGroupByKey(name.slice(SEARCH_PREFIX.length));
159
+ fields.push({
160
+ key: group.key,
161
+ label: heartSearchGroupLabel(group, locale),
162
+ tab: group.tab,
163
+ count,
164
+ fragments: fragmentsOf(innerHit),
165
+ });
166
+ const { ids, codes } = idsOf(innerHit);
167
+ ids.forEach((id) => opinionIds.add(id));
168
+ codes.forEach((code) => hasCodes.add(code));
169
+ continue;
170
+ }
171
+
172
+ if (!SILENT_FILTERS.has(name)) filters.push({ name, label: filterLabel(name, locale), count });
173
+ }
174
+
175
+ // Highlight racine : un champ par cle, regroupe en famille comme les inner
176
+ // hits le sont deja. Sans decompte — la ligne matchee est le resultat
177
+ // lui-meme.
178
+ const byGroup = new Map<string, MatchFieldExplain>();
179
+ for (const [field, fragments] of Object.entries<any>(highlight ?? {})) {
180
+ const list = (Array.isArray(fragments) ? fragments : []).filter(
181
+ (html: any) => typeof html === "string" && html,
182
+ );
183
+ if (!list.length) continue;
184
+ const group = heartSearchFieldGroup(field);
185
+ if (!byGroup.has(group.key)) {
186
+ byGroup.set(group.key, {
187
+ key: group.key,
188
+ label: heartSearchGroupLabel(group, locale),
189
+ tab: group.tab,
190
+ count: 0,
191
+ fragments: [],
192
+ });
193
+ }
194
+ const entry = byGroup.get(group.key)!;
195
+ // Une meme famille couvre les deux langues et la variante sans accents du
196
+ // champ : le meme passage y revient a l'identique.
197
+ for (const html of list) {
198
+ if (WHOLE_VALUE.test(html)) continue;
199
+ if (!entry.fragments.some((fragment) => fragment.html === html)) {
200
+ entry.fragments.push({ html, date: null });
201
+ }
202
+ }
203
+ }
204
+ fields.push(...byGroup.values());
205
+
206
+ if (!fields.length && !filters.length) return null;
207
+
208
+ fields.sort((a, b) => b.count - a.count || b.fragments.length - a.fragments.length);
209
+ filters.sort((a, b) => a.name.localeCompare(b.name));
210
+
211
+ return {
212
+ fields,
213
+ filters,
214
+ opinionIds: [...opinionIds],
215
+ hasCodes: [...hasCodes],
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Termes de recherche saisis pour la liste courante, pour les rappeler dans
221
+ * l'explication. fr_drugs_app les relisait dans le filterStore de sa page de
222
+ * recherche ; le layer n'a pas ce store : la fiche les recoit en prop
223
+ * (`highlight`) et les passe ici. Meme peigne que l'original : liste de
224
+ * termes, terme seul ou clause `{ value }`.
225
+ */
226
+ export function heartCurrentSearchTerms(values: unknown): string[] {
227
+ const list = Array.isArray(values) ? values : values ? [values] : [];
228
+ return list
229
+ .map((value: any) => (value && typeof value === "object" ? value.value : value))
230
+ .filter((value: any) => typeof value === "string" && value.trim());
231
+ }
@@ -0,0 +1,239 @@
1
+ // useHeartReports, la liste des rapports cures du rail : ceux livres avec
2
+ // le layer (public/reports) et ceux que Directus publie.
3
+ //
4
+ // Les artefacts Directus passent par /api/oip/reports : l'appel direct depuis le
5
+ // navigateur emportait le token dans le bundle et echouait en CORS, l'instance
6
+ // n'autorisant pas l'origine de l'app.
7
+ //
8
+ // Shares state between the sidebar and the /reports/[id] page via
9
+ // `useState` (Nuxt's app-level singleton). Re-fetches on every consumer
10
+ // mount when the cache is empty so a transient failure (e.g. Directus
11
+ // permission being toggled mid-edit) doesn't leave the sidebar section
12
+ // hidden until a full reload.
13
+
14
+ export type ReportCategory = 'report' | 'monograph';
15
+
16
+ export interface Report {
17
+ id: string;
18
+ title: string;
19
+ html: string;
20
+ category: ReportCategory;
21
+ /**
22
+ * Nom du composant de `oip-drugs/monographs/` qui porte le document. Quand il est
23
+ * la, la page de rapport rend le document dans le DOM de l'app (comme
24
+ * l'onglet Monographie d'une fiche marque) au lieu d'une iframe, et le HTML
25
+ * de `public/reports/` n'est pas charge.
26
+ */
27
+ component?: string;
28
+ /** PDF equivalent, quand il existe dans `public/reports/`. */
29
+ pdf?: string | null;
30
+ }
31
+
32
+ // Rapports servis en STATIQUE (HTML livré dans public/reports/) plutôt que
33
+ // via Directus (utile quand l'artefact Directus a été perdu/dépublié). Ils
34
+ // sont prioritaires sur Directus (dédup par id) et chargés même si
35
+ // l'intégration Directus n'est pas configurée.
36
+ interface StaticReport {
37
+ id: string;
38
+ title: string;
39
+ file: string; // chemin servi depuis public/, pour le repli en iframe
40
+ category: ReportCategory;
41
+ /** Composant de `oip-drugs/monographs/` a rendre a la place de l'iframe. */
42
+ component?: string;
43
+ /** PDF equivalent servi depuis public/reports/. */
44
+ pdf?: string;
45
+ }
46
+
47
+ const STATIC_REPORTS: StaticReport[] = [
48
+ {
49
+ id: '79381b66-fd6b-41b8-8a0f-1a0acc37d69c',
50
+ title: 'Avis HAS, cancer du sein 2023-2026',
51
+ file: '/reports/has-sein-2023.html',
52
+ component: 'has-sein-2023',
53
+ category: 'report',
54
+ },
55
+ {
56
+ id: 'monograph-blincyto',
57
+ title: 'Blincyto',
58
+ file: '/reports/blincyto-has.html',
59
+ component: 'blincyto-has',
60
+ pdf: '/reports/blincyto-has.pdf',
61
+ category: 'monograph',
62
+ },
63
+ {
64
+ id: 'monograph-darzalex',
65
+ title: 'Darzalex',
66
+ file: '/reports/darzalex-has.html',
67
+ component: 'darzalex-has',
68
+ pdf: '/reports/darzalex-has.pdf',
69
+ category: 'monograph',
70
+ },
71
+ {
72
+ id: 'monograph-dupixent',
73
+ title: 'Dupixent',
74
+ file: '/reports/dupixent-has.html',
75
+ component: 'dupixent-has',
76
+ pdf: '/reports/dupixent-has.pdf',
77
+ category: 'monograph',
78
+ },
79
+ {
80
+ id: 'monograph-enhertu',
81
+ title: 'Enhertu',
82
+ file: '/reports/enhertu-has.html',
83
+ component: 'enhertu-has',
84
+ pdf: '/reports/enhertu-has.pdf',
85
+ category: 'monograph',
86
+ },
87
+ {
88
+ id: 'monograph-ibrance',
89
+ title: 'Ibrance',
90
+ file: '/reports/ibrance-has.html',
91
+ component: 'ibrance-has',
92
+ pdf: '/reports/ibrance-has.pdf',
93
+ category: 'monograph',
94
+ },
95
+ {
96
+ id: 'monograph-imfinzi',
97
+ title: 'Imfinzi',
98
+ file: '/reports/imfinzi-has.html',
99
+ component: 'imfinzi-has',
100
+ pdf: '/reports/imfinzi-has.pdf',
101
+ category: 'monograph',
102
+ },
103
+ {
104
+ id: 'monograph-keytruda',
105
+ title: 'Keytruda',
106
+ file: '/reports/keytruda-has.html',
107
+ component: 'keytruda-has',
108
+ pdf: '/reports/keytruda-has.pdf',
109
+ category: 'monograph',
110
+ },
111
+ {
112
+ id: 'monograph-kisqali',
113
+ title: 'Kisqali',
114
+ file: '/reports/kisqali-has.html',
115
+ component: 'kisqali-has',
116
+ pdf: '/reports/kisqali-has.pdf',
117
+ category: 'monograph',
118
+ },
119
+ {
120
+ id: 'monograph-opdivo',
121
+ title: 'Opdivo',
122
+ file: '/reports/opdivo-has.html',
123
+ component: 'opdivo-has',
124
+ pdf: '/reports/opdivo-has.pdf',
125
+ category: 'monograph',
126
+ },
127
+ {
128
+ id: 'monograph-ozempic',
129
+ title: 'Ozempic',
130
+ file: '/reports/ozempic-has.html',
131
+ component: 'ozempic-has',
132
+ pdf: '/reports/ozempic-has.pdf',
133
+ category: 'monograph',
134
+ },
135
+ {
136
+ id: 'monograph-tecentriq',
137
+ title: 'Tecentriq',
138
+ file: '/reports/tecentriq-has.html',
139
+ component: 'tecentriq-has',
140
+ pdf: '/reports/tecentriq-has.pdf',
141
+ category: 'monograph',
142
+ },
143
+ {
144
+ id: 'monograph-yescarta',
145
+ title: 'Yescarta',
146
+ file: '/reports/yescarta-has.html',
147
+ component: 'yescarta-has',
148
+ pdf: '/reports/yescarta-has.pdf',
149
+ category: 'monograph',
150
+ },
151
+ ];
152
+
153
+ async function fetchStaticReports(): Promise<Report[]> {
154
+ const results = await Promise.all(
155
+ STATIC_REPORTS.map(async (s): Promise<Report | null> => {
156
+ // Document porte par un composant : rien a telecharger, la page le rend
157
+ // depuis `oip-drugs/monographs/`. Evite treize requetes HTML au demarrage.
158
+ if (s.component) {
159
+ return {
160
+ id: s.id,
161
+ title: s.title,
162
+ html: '',
163
+ category: s.category,
164
+ component: s.component,
165
+ pdf: s.pdf ?? null,
166
+ };
167
+ }
168
+ try {
169
+ const res = await fetch(s.file);
170
+ if (!res.ok) {
171
+ console.warn(`[useHeartReports] static ${s.file} HTTP ${res.status}`);
172
+ return null;
173
+ }
174
+ return { id: s.id, title: s.title, html: await res.text(), category: s.category };
175
+ } catch (err: any) {
176
+ console.warn(`[useHeartReports] static ${s.file} failed:`, err?.message || err);
177
+ return null;
178
+ }
179
+ }),
180
+ );
181
+ return results.filter((r): r is Report => r !== null);
182
+ }
183
+
184
+ export function useHeartReports() {
185
+ const reports = useState<Report[]>('oip-drugs:reports', () => []);
186
+ const loading = useState<boolean>('oip-drugs:reports:loading', () => false);
187
+ // Flips to true the first time a fetch attempt completes (success or
188
+ // failure). The detail page uses it to decide skeleton vs. "not
189
+ // found": without it the user would see a flash of "not found"
190
+ // before the very first fetch resolves.
191
+ const initialized = useState<boolean>('oip-drugs:reports:initialized', () => false);
192
+
193
+ async function fetchReports(force = false) {
194
+ if (loading.value) return;
195
+ // Skip the network call when we already have a populated cache
196
+ // unless the caller explicitly asked for a refresh.
197
+ if (!force && reports.value.length > 0) {
198
+ initialized.value = true;
199
+ return;
200
+ }
201
+
202
+ loading.value = true;
203
+ try {
204
+ // Rapports statiques (toujours), + Directus si configuré. Les statiques
205
+ // sont prioritaires : leur id est retiré de la demande, l'artefact
206
+ // Directus du même rapport étant souvent perdu ou dépublié.
207
+ const statics = await fetchStaticReports();
208
+ const staticIds = statics.map((s) => s.id);
209
+ const directus = await $fetch<Report[]>('/api/oip/reports', {
210
+ query: { skip: staticIds.join(',') },
211
+ }).catch((err: any) => {
212
+ console.warn('[useHeartReports] /api/oip/reports failed:', err?.message || err);
213
+ return [] as Report[];
214
+ });
215
+
216
+ reports.value = [...statics, ...directus];
217
+ } catch (err) {
218
+ console.warn('[useHeartReports] fetch failed', err);
219
+ // Keep whatever we had, don't wipe a working cache because of a
220
+ // transient network blip.
221
+ } finally {
222
+ loading.value = false;
223
+ initialized.value = true;
224
+ }
225
+ }
226
+
227
+ function findById(id: string): Report | null {
228
+ if (!id) return null;
229
+ return reports.value.find((r) => r.id === id) || null;
230
+ }
231
+
232
+ return {
233
+ reports,
234
+ loaded: initialized,
235
+ loading,
236
+ fetchReports,
237
+ findById,
238
+ };
239
+ }