@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.
Files changed (265) hide show
  1. package/dist/module.json +1 -1
  2. package/layer/app/app.config.ts +6 -0
  3. package/layer/app/components/OipPubmedSummary.vue +223 -0
  4. package/layer/app/components/OipResourceDrawer.vue +331 -0
  5. package/layer/app/components/OipResourceDrawerLink.vue +40 -0
  6. package/layer/app/components/OipResourceField.vue +41 -0
  7. package/layer/app/components/OipResourceGenericCard.vue +54 -0
  8. package/layer/app/components/OipResourceSource.vue +50 -0
  9. package/layer/app/components/OipResourceTags.vue +65 -0
  10. package/layer/app/components/OipResourceView.vue +107 -0
  11. package/layer/app/components/oip-drugs/fiches/AssociationFiche.vue +775 -0
  12. package/layer/app/components/oip-drugs/fiches/BrandFiche.vue +1111 -0
  13. package/layer/app/components/oip-drugs/fiches/IndicationFiche.vue +182 -0
  14. package/layer/app/components/oip-drugs/fiches/JoFiche.vue +101 -0
  15. package/layer/app/components/oip-drugs/fiches/PndsFiche.vue +279 -0
  16. package/layer/app/components/oip-drugs/heart/JoCard.vue +314 -0
  17. package/layer/app/components/oip-drugs/heart/TextHighlight.vue +326 -0
  18. package/layer/app/components/oip-drugs/heart/ansm/BrandCard.vue +187 -0
  19. package/layer/app/components/oip-drugs/heart/ansm/CisDetail.vue +363 -0
  20. package/layer/app/components/oip-drugs/heart/ansm/CisTable.vue +167 -0
  21. package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyCard.vue +373 -0
  22. package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyFiche.vue +372 -0
  23. package/layer/app/components/oip-drugs/heart/associations/TranscriptionCard.vue +55 -0
  24. package/layer/app/components/oip-drugs/heart/elastic/SourceContainerElastic.vue +103 -0
  25. package/layer/app/components/oip-drugs/heart/france/BrandGroupCard.vue +450 -0
  26. package/layer/app/components/oip-drugs/heart/mitm/Card.vue +199 -0
  27. package/layer/app/components/oip-drugs/heart/mitm/Source.vue +23 -0
  28. package/layer/app/components/oip-drugs/heart/mitm/details/Overview.vue +381 -0
  29. package/layer/app/components/oip-drugs/heart/put/Card.vue +212 -0
  30. package/layer/app/components/oip-drugs/heart/results/AccesCard.vue +696 -0
  31. package/layer/app/components/oip-drugs/heart/results/AlertCard.vue +342 -0
  32. package/layer/app/components/oip-drugs/heart/results/AsmrCursor.vue +229 -0
  33. package/layer/app/components/oip-drugs/heart/results/EvalCard.vue +370 -0
  34. package/layer/app/components/oip-drugs/heart/results/IndicCard.vue +743 -0
  35. package/layer/app/components/oip-drugs/heart/results/MatchExplain.vue +243 -0
  36. package/layer/app/components/oip-drugs/heart/results/PrefRows.vue +105 -0
  37. package/layer/app/components/oip-drugs/heart/results/details/HasHistory.vue +487 -0
  38. package/layer/app/components/oip-drugs/heart/results/details/InternationalCountryCard.vue +461 -0
  39. package/layer/app/components/oip-drugs/heart/results/details/InternationalOverview.vue +200 -0
  40. package/layer/app/components/oip-drugs/heart/results/details/InternationalPriceDrawer.vue +647 -0
  41. package/layer/app/components/oip-drugs/heart/results/details/Monograph.vue +128 -0
  42. package/layer/app/components/oip-drugs/heart/results/details/OpinionDrawer.vue +535 -0
  43. package/layer/app/components/oip-drugs/heart/results/details/PresentationDrawer.vue +738 -0
  44. package/layer/app/components/oip-drugs/heart/results/details/Price.vue +227 -0
  45. package/layer/app/components/oip-drugs/heart/results/details/PriceOverview.vue +1261 -0
  46. package/layer/app/components/oip-drugs/heart/results/details/RecordDrawer.vue +494 -0
  47. package/layer/app/components/oip-drugs/heart/results/details/RecordList.vue +426 -0
  48. package/layer/app/components/oip-drugs/heart/results/les/Card.vue +340 -0
  49. package/layer/app/components/oip-drugs/heart/results/les/Drawer.vue +67 -0
  50. package/layer/app/components/oip-drugs/heart/results/les/IndicationDetail.vue +149 -0
  51. package/layer/app/components/oip-drugs/heart/results/les/Source.vue +20 -0
  52. package/layer/app/components/oip-drugs/heart/results/les/SourceTag.vue +39 -0
  53. package/layer/app/components/oip-drugs/heart/searchPage/DynamicSource.vue +29 -0
  54. package/layer/app/components/oip-drugs/medicam/market/Chart.vue +121 -0
  55. package/layer/app/components/oip-drugs/outpatient/ProductEvolution.vue +236 -0
  56. package/layer/app/components/oip-drugs/resources/Ansm.vue +75 -0
  57. package/layer/app/components/oip-drugs/resources/Association.vue +30 -0
  58. package/layer/app/components/oip-drugs/resources/Brand.vue +65 -0
  59. package/layer/app/components/oip-drugs/resources/Contribution.vue +216 -0
  60. package/layer/app/components/oip-drugs/resources/Hospital.vue +151 -0
  61. package/layer/app/components/oip-drugs/resources/Indication.vue +54 -0
  62. package/layer/app/components/oip-drugs/resources/Jo.vue +57 -0
  63. package/layer/app/components/oip-drugs/resources/Medicam.vue +140 -0
  64. package/layer/app/components/oip-drugs/resources/Mitm.vue +84 -0
  65. package/layer/app/components/oip-drugs/resources/NationalAssembly.vue +93 -0
  66. package/layer/app/components/oip-drugs/resources/Outpatient.vue +97 -0
  67. package/layer/app/components/oip-drugs/resources/Pnds.vue +24 -0
  68. package/layer/app/components/oip-drugs/resources/Price.vue +106 -0
  69. package/layer/app/components/oip-drugs/resources/Put.vue +143 -0
  70. package/layer/app/components/oip-science/appel-a-projet/Cards.vue +162 -0
  71. package/layer/app/components/oip-science/clinical-trials/Cards.vue +382 -0
  72. package/layer/app/components/oip-science/clinical-trials/CardsGlobal.vue +273 -0
  73. package/layer/app/components/oip-science/clinical-trials/LinkedList.vue +366 -0
  74. package/layer/app/components/oip-science/clinical-trials/PairBadge.vue +25 -0
  75. package/layer/app/components/oip-science/clinical-trials/PairBody.vue +182 -0
  76. package/layer/app/components/oip-science/clinical-trials/SitesDrawer.vue +95 -0
  77. package/layer/app/components/oip-science/common/Form.vue +73 -0
  78. package/layer/app/components/oip-science/common/MatchAvgTag.vue +283 -0
  79. package/layer/app/components/oip-science/common/NoData.vue +16 -0
  80. package/layer/app/components/oip-science/common/ScoreBadge.vue +35 -0
  81. package/layer/app/components/oip-science/common/Source.vue +31 -0
  82. package/layer/app/components/oip-science/common/TableTags.vue +279 -0
  83. package/layer/app/components/oip-science/common/Timeline.vue +104 -0
  84. package/layer/app/components/oip-science/common/ToolTips.vue +89 -0
  85. package/layer/app/components/oip-science/cross/publicationPubmed.vue +484 -0
  86. package/layer/app/components/oip-science/crossData/CrossDataLocations.vue +71 -0
  87. package/layer/app/components/oip-science/ctgov/Investigators.vue +148 -0
  88. package/layer/app/components/oip-science/ctgov/Locations.vue +102 -0
  89. package/layer/app/components/oip-science/ctgov/Publications.vue +52 -0
  90. package/layer/app/components/oip-science/ctgov/Sponsors.vue +66 -0
  91. package/layer/app/components/oip-science/ctgov/StudyDescription.vue +1136 -0
  92. package/layer/app/components/oip-science/ctgov-centers/ClinicalTrials.vue +40 -0
  93. package/layer/app/components/oip-science/ctgov-centers/Publications.vue +50 -0
  94. package/layer/app/components/oip-science/ctgov-investigators/ClinicalTrials.vue +43 -0
  95. package/layer/app/components/oip-science/ctgov-investigators/Informations.vue +171 -0
  96. package/layer/app/components/oip-science/ctgov-investigators/Publications.vue +44 -0
  97. package/layer/app/components/oip-science/elastic/DateRangeFilter.vue +209 -0
  98. package/layer/app/components/oip-science/elastic/Filter.vue +719 -0
  99. package/layer/app/components/oip-science/elastic/MatchExplain.vue +152 -0
  100. package/layer/app/components/oip-science/establishments/Cards.vue +183 -0
  101. package/layer/app/components/oip-science/establishments/ClinicalTrials.vue +62 -0
  102. package/layer/app/components/oip-science/establishments/Hcps.vue +227 -0
  103. package/layer/app/components/oip-science/establishments/Informations.vue +263 -0
  104. package/layer/app/components/oip-science/establishments/LinkedList.vue +153 -0
  105. package/layer/app/components/oip-science/establishments/MatchedLabels.vue +194 -0
  106. package/layer/app/components/oip-science/establishments/PublicFinance.vue +62 -0
  107. package/layer/app/components/oip-science/euct/Investigators.vue +125 -0
  108. package/layer/app/components/oip-science/euct/Locations.vue +102 -0
  109. package/layer/app/components/oip-science/euct/Products.vue +101 -0
  110. package/layer/app/components/oip-science/euct/Publications.vue +89 -0
  111. package/layer/app/components/oip-science/euct/Sponsors.vue +86 -0
  112. package/layer/app/components/oip-science/euct/StudyDescription.vue +1075 -0
  113. package/layer/app/components/oip-science/euct-centers/ClinicalTrials.vue +40 -0
  114. package/layer/app/components/oip-science/euct-centers/Publications.vue +55 -0
  115. package/layer/app/components/oip-science/euct-investigators/ClinicalTrials.vue +41 -0
  116. package/layer/app/components/oip-science/euct-investigators/Informations.vue +171 -0
  117. package/layer/app/components/oip-science/euct-investigators/Publications.vue +51 -0
  118. package/layer/app/components/oip-science/hcps/AppelsAProjet.vue +62 -0
  119. package/layer/app/components/oip-science/hcps/Cards.vue +237 -0
  120. package/layer/app/components/oip-science/hcps/ClinicalTrials.vue +78 -0
  121. package/layer/app/components/oip-science/hcps/Informations.vue +296 -0
  122. package/layer/app/components/oip-science/hcps/LinkedList.vue +153 -0
  123. package/layer/app/components/oip-science/hcps/MatchedLabels.vue +207 -0
  124. package/layer/app/components/oip-science/hcps/Pubmed.vue +128 -0
  125. package/layer/app/components/oip-science/local/SliderTabs.vue +99 -0
  126. package/layer/app/components/oip-science/local/SliderTabsFull.vue +135 -0
  127. package/layer/app/components/oip-science/pubmed/Authors.vue +128 -0
  128. package/layer/app/components/oip-science/pubmed/Cards.vue +417 -0
  129. package/layer/app/components/oip-science/pubmed/ClinicalTrials.vue +71 -0
  130. package/layer/app/components/oip-science/resources/Phrc.vue +39 -0
  131. package/layer/app/composables/useOipChapterScroll.ts +43 -0
  132. package/layer/app/composables/useOipResourceHooks.ts +27 -0
  133. package/layer/app/composables/useOipResourceNav.ts +66 -0
  134. package/layer/app/composables/useOipResourceViews.ts +117 -0
  135. package/layer/app/composables/useOipResources.ts +83 -0
  136. package/layer/app/oip-drugs/PORTING.md +120 -0
  137. package/layer/app/oip-drugs/composables/useAssociationDetail.ts +220 -0
  138. package/layer/app/oip-drugs/composables/useAssociationExport.ts +63 -0
  139. package/layer/app/oip-drugs/composables/useAssociationLookup.ts +105 -0
  140. package/layer/app/oip-drugs/composables/useCardPrefOptions.ts +201 -0
  141. package/layer/app/oip-drugs/composables/useCurrency.ts +121 -0
  142. package/layer/app/oip-drugs/composables/useDirectusSources.ts +224 -0
  143. package/layer/app/oip-drugs/composables/useHeartBrandMatches.ts +112 -0
  144. package/layer/app/oip-drugs/composables/useHeartDetailSearches.ts +479 -0
  145. package/layer/app/oip-drugs/composables/useHeartIntlApprovals.ts +661 -0
  146. package/layer/app/oip-drugs/composables/useHeartLesIndication.ts +168 -0
  147. package/layer/app/oip-drugs/composables/useHeartMatchExplain.ts +231 -0
  148. package/layer/app/oip-drugs/composables/useHeartReports.ts +239 -0
  149. package/layer/app/oip-drugs/composables/useHeartSearchFields.ts +233 -0
  150. package/layer/app/oip-drugs/composables/useMergeByKey.ts +23 -0
  151. package/layer/app/oip-drugs/composables/useOipDrugsElastic.ts +621 -0
  152. package/layer/app/oip-drugs/composables/usePageEmpty.ts +58 -0
  153. package/layer/app/oip-drugs/composables/v-useSalesSource.ts +64 -0
  154. package/layer/app/oip-drugs/drugs.css +500 -0
  155. package/layer/app/oip-drugs/esIndices.ts +322 -0
  156. package/layer/app/oip-drugs/kinds.ts +264 -0
  157. package/layer/app/oip-drugs/locales/en.json +1571 -0
  158. package/layer/app/oip-drugs/locales/fr.json +1571 -0
  159. package/layer/app/oip-drugs/monographs/blincyto-has.vue +289 -0
  160. package/layer/app/oip-drugs/monographs/darzalex-has.vue +276 -0
  161. package/layer/app/oip-drugs/monographs/dupixent-has.vue +216 -0
  162. package/layer/app/oip-drugs/monographs/enhertu-has.vue +280 -0
  163. package/layer/app/oip-drugs/monographs/has-sein-2023.vue +1649 -0
  164. package/layer/app/oip-drugs/monographs/ibrance-has.vue +277 -0
  165. package/layer/app/oip-drugs/monographs/imfinzi-has.vue +293 -0
  166. package/layer/app/oip-drugs/monographs/keytruda-has.vue +334 -0
  167. package/layer/app/oip-drugs/monographs/kisqali-has.vue +201 -0
  168. package/layer/app/oip-drugs/monographs/opdivo-has.vue +231 -0
  169. package/layer/app/oip-drugs/monographs/ozempic-has.vue +186 -0
  170. package/layer/app/oip-drugs/monographs/tecentriq-has.vue +312 -0
  171. package/layer/app/oip-drugs/monographs/yescarta-has.vue +272 -0
  172. package/layer/app/oip-drugs/types/elastic.ts +113 -0
  173. package/layer/app/oip-drugs/types/es/agencies.ts +152 -0
  174. package/layer/app/oip-drugs/types/es/directory.ts +54 -0
  175. package/layer/app/oip-drugs/types/es/docByIndex.ts +151 -0
  176. package/layer/app/oip-drugs/types/es/openmedic.ts +67 -0
  177. package/layer/app/oip-drugs/types/es/outpatient.ts +71 -0
  178. package/layer/app/oip-drugs/types/es/portal.ts +130 -0
  179. package/layer/app/oip-drugs/types/es/retail.ts +107 -0
  180. package/layer/app/oip-drugs/types/es/statistics.ts +29 -0
  181. package/layer/app/oip-drugs/types/heart/CurrencyRate.ts +16 -0
  182. package/layer/app/oip-drugs/types/heart/ansm.ts +106 -0
  183. package/layer/app/oip-drugs/types/heart/tag.ts +29 -0
  184. package/layer/app/oip-drugs/utils/associationFields.ts +118 -0
  185. package/layer/app/oip-drugs/utils/contributions.ts +36 -0
  186. package/layer/app/oip-drugs/utils/countryMapping.ts +387 -0
  187. package/layer/app/oip-drugs/utils/esHighlight.ts +21 -0
  188. package/layer/app/oip-drugs/utils/exportSheets.ts +57 -0
  189. package/layer/app/oip-drugs/utils/heartIntlPrice.ts +228 -0
  190. package/layer/app/oip-drugs/utils/heartStyleAndFormat.ts +262 -0
  191. package/layer/app/oip-drugs/utils/heartStyleAnsm.ts +71 -0
  192. package/layer/app/oip-drugs/utils/latestDate.ts +31 -0
  193. package/layer/app/oip-drugs/utils/p-heartCardTags.ts +53 -0
  194. package/layer/app/oip-drugs/utils/priceExport.ts +269 -0
  195. package/layer/app/oip-drugs/utils/q-hasRatings.ts +43 -0
  196. package/layer/app/oip-drugs/utils/s-openCard.ts +20 -0
  197. package/layer/app/oip-drugs/utils/sourceUuids.ts +155 -0
  198. package/layer/app/oip-drugs/utils/styleAndFormat.ts +40 -0
  199. package/layer/app/oip-drugs/utils/v-salesSeries.ts +156 -0
  200. package/layer/app/oip-drugs/utils/v-scopeTags.ts +23 -0
  201. package/layer/app/oip-science/PORTING.md +93 -0
  202. package/layer/app/oip-science/assets/smartSelection.json +428 -0
  203. package/layer/app/oip-science/composables/useAuthorMatching.ts +342 -0
  204. package/layer/app/oip-science/composables/useCTCenter.ts +107 -0
  205. package/layer/app/oip-science/composables/useCTInvestigators.ts +107 -0
  206. package/layer/app/oip-science/composables/useEstablishmentCrossData.ts +139 -0
  207. package/layer/app/oip-science/composables/useEstablishmentMatching.ts +176 -0
  208. package/layer/app/oip-science/composables/useEuct.ts +418 -0
  209. package/layer/app/oip-science/composables/useEuctCenter.ts +107 -0
  210. package/layer/app/oip-science/composables/useEuctInvestigators.ts +107 -0
  211. package/layer/app/oip-science/composables/useGlobalClinicalTrials.ts +624 -0
  212. package/layer/app/oip-science/composables/useLinkedTrials.ts +83 -0
  213. package/layer/app/oip-science/composables/useOipElastic.ts +658 -0
  214. package/layer/app/oip-science/composables/useOipSearchExplain.ts +166 -0
  215. package/layer/app/oip-science/composables/usePubmed.ts +487 -0
  216. package/layer/app/oip-science/composables/useSearchFields.ts +153 -0
  217. package/layer/app/oip-science/indexConfig.json +794 -0
  218. package/layer/app/oip-science/locales/en.json +483 -0
  219. package/layer/app/oip-science/locales/fr.json +483 -0
  220. package/layer/app/oip-science/science.css +1182 -0
  221. package/layer/app/oip-science/utils/a-openCard.ts +23 -0
  222. package/layer/app/oip-science/utils/b-openCard.ts +35 -0
  223. package/layer/app/oip-science/utils/c-filterContext.ts +81 -0
  224. package/layer/app/oip-science/utils/f-types.ts +241 -0
  225. package/layer/app/oip-science/utils/frDiseaseTerms.ts +269 -0
  226. package/layer/app/oip-science/utils/matchMethod.ts +53 -0
  227. package/layer/app/oip-science/utils/sliderTabs.ts +13 -0
  228. package/layer/app/oip-science/utils/sources.ts +15 -0
  229. package/layer/app/oip-science/utils/styleAndFormat.ts +608 -0
  230. package/layer/app/oip-science/utils/toPresets.ts +7 -0
  231. package/layer/app/oip-science/utils/valueTable.ts +126 -0
  232. package/layer/app/plugins/oip-science-i18n.ts +17 -0
  233. package/layer/app/utils/oipFicheRoutes.ts +118 -0
  234. package/layer/app/utils/oipResources.ts +440 -0
  235. package/layer/nuxt.config.ts +22 -2
  236. package/layer/public/reports/blincyto-has.html +292 -0
  237. package/layer/public/reports/blincyto-has.pdf +0 -0
  238. package/layer/public/reports/darzalex-has.html +279 -0
  239. package/layer/public/reports/darzalex-has.pdf +0 -0
  240. package/layer/public/reports/dupixent-has.html +217 -0
  241. package/layer/public/reports/dupixent-has.pdf +0 -0
  242. package/layer/public/reports/enhertu-has.html +283 -0
  243. package/layer/public/reports/enhertu-has.pdf +0 -0
  244. package/layer/public/reports/has-sein-2023.html +1639 -0
  245. package/layer/public/reports/ibrance-has.html +280 -0
  246. package/layer/public/reports/ibrance-has.pdf +0 -0
  247. package/layer/public/reports/imfinzi-has.html +297 -0
  248. package/layer/public/reports/imfinzi-has.pdf +0 -0
  249. package/layer/public/reports/keytruda-has.html +337 -0
  250. package/layer/public/reports/keytruda-has.pdf +0 -0
  251. package/layer/public/reports/kisqali-has.html +202 -0
  252. package/layer/public/reports/kisqali-has.pdf +0 -0
  253. package/layer/public/reports/opdivo-has.html +232 -0
  254. package/layer/public/reports/opdivo-has.pdf +0 -0
  255. package/layer/public/reports/ozempic-has.html +188 -0
  256. package/layer/public/reports/ozempic-has.pdf +0 -0
  257. package/layer/public/reports/tecentriq-has.html +315 -0
  258. package/layer/public/reports/tecentriq-has.pdf +0 -0
  259. package/layer/public/reports/yescarta-has.html +275 -0
  260. package/layer/public/reports/yescarta-has.pdf +0 -0
  261. package/layer/server/api/oip/es.post.ts +99 -0
  262. package/layer/server/api/oip/pdf.get.ts +70 -0
  263. package/layer/server/api/oip/reports.get.ts +98 -0
  264. package/layer/server/api/oip/resource-types.get.ts +22 -0
  265. package/package.json +1 -1
@@ -0,0 +1,105 @@
1
+ /**
2
+ * De l'ecriture d'un nom d'association a sa fiche (portage de
3
+ * useAssociationLookup de fr_drugs_app).
4
+ *
5
+ * Les contributions patients des avis HAS nomment l'association qui les signe
6
+ * (`association_label`, et son regroupement `association_label_regroup`), mais
7
+ * ces noms ne sont pas ceux de l'annuaire : sur les 156 associations qui
8
+ * contribuent, 43 portent dans les avis un nom que l'annuaire n'a pas comme
9
+ * `_id`. Pointer la fiche avec le nom de l'avis menait donc une fois sur
10
+ * quatre a un « Association introuvable ».
11
+ *
12
+ * L'annuaire, lui, liste toutes les ecritures connues d'une association dans
13
+ * `association_label`, separees par `; ` : ce sont elles qui font le pont.
14
+ * C'est la reciproque de `useAssociationDetail`.
15
+ *
16
+ * Les libelles de l'annuaire sont des `keyword` sans sous-champ analyse : un
17
+ * nom noye dans la liste des variantes ne se trouve donc pas par `match`,
18
+ * d'ou la regexp qui borne le nom aux separateurs.
19
+ */
20
+ import { ES_INDEX } from "../esIndices";
21
+ import { ASSOCIATION_FIELDS } from "../utils/associationFields";
22
+ import useOipDrugsElastic from "./useOipDrugsElastic";
23
+
24
+ /** Caracteres que la syntaxe des regexp Elasticsearch se reserve. */
25
+ const REGEXP_SPECIAL = /([.?+*|{}[\]()"\\#@&<>~])/g;
26
+
27
+ const escapeRegexp = (value: string) => value.replace(REGEXP_SPECIAL, "\\$1");
28
+
29
+ const key = (name: unknown) => String(name ?? "").trim().toLowerCase();
30
+
31
+ export function useAssociationLookup() {
32
+ const { queryCustomElastic } = useOipDrugsElastic();
33
+
34
+ /** Ecriture d'un nom (en minuscules) vers l'`_id` de la fiche. */
35
+ const ids = ref<Record<string, string>>({});
36
+
37
+ /** Noms deja demandes, resolus ou non : une page ne les redemande pas. */
38
+ const asked = new Set<string>();
39
+
40
+ /**
41
+ * Resout un paquet de noms en une requete. Appelable a chaque arrivee de
42
+ * donnees : seuls les noms encore inconnus repartent.
43
+ */
44
+ async function load(names: unknown[]) {
45
+ const wanted = [...new Set(names.map((name) => String(name ?? "").trim()))]
46
+ .filter((name) => name !== "" && !asked.has(key(name)));
47
+ if (wanted.length === 0) return;
48
+ wanted.forEach((name) => asked.add(key(name)));
49
+
50
+ // Trois chemins pour un meme nom : tel quel dans le nom regroupe de
51
+ // l'annuaire, a la casse pres, ou parmi ses variantes.
52
+ const should: any[] = [{ terms: { [ASSOCIATION_FIELDS.label]: wanted } }];
53
+ wanted.forEach((name) => {
54
+ const escaped = escapeRegexp(name);
55
+ should.push({
56
+ regexp: { [ASSOCIATION_FIELDS.label]: { value: escaped, case_insensitive: true } },
57
+ });
58
+ should.push({
59
+ regexp: {
60
+ [ASSOCIATION_FIELDS.labelVariants]: {
61
+ value: `(.*; )?${escaped}(; .*)?`,
62
+ case_insensitive: true,
63
+ },
64
+ },
65
+ });
66
+ });
67
+
68
+ const res: any = await queryCustomElastic({
69
+ index: ES_INDEX.ASSOCIATION_DIRECTORY,
70
+ customQuery: {
71
+ size: 200,
72
+ _source: [ASSOCIATION_FIELDS.label, ASSOCIATION_FIELDS.labelVariants],
73
+ query: { bool: { should, minimum_should_match: 1 } },
74
+ } as any,
75
+ allAnswer: true,
76
+ });
77
+
78
+ const found: Record<string, string> = {};
79
+ (res?.hits?.hits ?? []).forEach((hit: any) => {
80
+ const source = hit?._source ?? {};
81
+ const writings = [
82
+ source[ASSOCIATION_FIELDS.label] ?? hit._id,
83
+ ...String(source[ASSOCIATION_FIELDS.labelVariants] ?? "").split(";"),
84
+ ];
85
+ writings.forEach((writing) => {
86
+ const name = key(writing);
87
+ // Premiere fiche gagnante : une meme variante peut figurer dans deux
88
+ // fiches, et rien dans l'annuaire ne dit laquelle prime.
89
+ if (name && !(name in found) && !(name in ids.value)) found[name] = hit._id;
90
+ });
91
+ });
92
+ ids.value = { ...ids.value, ...found };
93
+ }
94
+
95
+ /** La fiche qui reconnait l'une de ces ecritures, s'il y en a une. */
96
+ function idFor(...names: unknown[]): string | null {
97
+ for (const name of names) {
98
+ const id = ids.value[key(name)];
99
+ if (id) return id;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ return { ids, load, idFor };
105
+ }
@@ -0,0 +1,201 @@
1
+ import type { HeartTagColor } from "../types/heart/tag";
2
+ /**
3
+ * Catalogue des options du panneau « Personnaliser », par famille de page.
4
+ *
5
+ * Une option = une rangee facultative sur la carte de resultat. Ce fichier dit
6
+ * ce qu'on peut demander (libelle, icone, teinte des etiquettes) ; ce sont les
7
+ * cartes qui savent ou lire la valeur, et HeartResultsPrefRows qui la dessine.
8
+ * L'etat arrive en prop `prefs` (cf. defaultCardPrefs).
9
+ *
10
+ * N'entrent ici que des champs absents du corps de la carte : on ne propose pas
11
+ * d'afficher ce qui est deja affiche. Une option sans donnee sur un resultat ne
12
+ * fait pas de rangee vide, la carte la saute.
13
+ */
14
+
15
+ /**
16
+ * Familles de page (ex-store `cardPrefs` de fr_drugs_app). Le choix de
17
+ * l'utilisateur vivait dans ce store, garde en localStorage ; dans le layer
18
+ * il arrive en prop `prefs` (cle d'option → affichee), par defaut
19
+ * `defaultCardPrefs()` : tout masque, comme dans fr_drugs_app.
20
+ */
21
+ export type CardPrefScope = "has" | "alert" | "ansm" | "les" | "mitm" | "pnds" | "put";
22
+
23
+ /** Cle d'option → affichee. Absent = masque. */
24
+ export type CardPrefs = Record<string, boolean>;
25
+
26
+ /** Les valeurs par defaut de fr_drugs_app : rien de coche. */
27
+ export function defaultCardPrefs(): CardPrefs {
28
+ return {};
29
+ }
30
+
31
+ export type CardPrefOption = {
32
+ key: string;
33
+ fr: string;
34
+ en: string;
35
+ /** Nom d'icone Phosphor, sans le prefixe « Ph ». */
36
+ icon: string;
37
+ /** Teinte des etiquettes de la rangee (variantes RmTag). */
38
+ variant?: HeartTagColor;
39
+ };
40
+
41
+ export type CardPrefGroup = {
42
+ fr: string;
43
+ en: string;
44
+ options: CardPrefOption[];
45
+ };
46
+
47
+ export const CARD_PREF_GROUPS: Record<CardPrefScope, CardPrefGroup[]> = {
48
+ // Avis HAS (onglets de la page Recherche) : carte de marque.
49
+ has: [
50
+ {
51
+ fr: "Tags de la marque",
52
+ en: "Brand tags",
53
+ options: [
54
+ { key: "rare", fr: "Maladies rares", en: "Rare diseases", icon: "Virus" },
55
+ { key: "oncoIcd10", fr: "Oncologie CIM-10", en: "Oncology ICD-10", icon: "ListNumbers", variant: "gray" },
56
+ { key: "oncoTerms", fr: "Termes oncologie", en: "Oncology terms", icon: "Stethoscope", variant: "gray" },
57
+ { key: "genes", fr: "Gènes", en: "Genes", icon: "Dna", variant: "violet" },
58
+ ],
59
+ },
60
+ {
61
+ fr: "Dernier avis",
62
+ en: "Latest opinion",
63
+ options: [
64
+ { key: "smr", fr: "SMR / ASMR", en: "SMR / ASMR", icon: "Scales", variant: "indigo" },
65
+ { key: "theme", fr: "Thème", en: "Theme", icon: "Tag", variant: "sky" },
66
+ ],
67
+ },
68
+ ],
69
+
70
+ // Alertes de securite ANSM : carte de marque alertee. Les tags viennent de
71
+ // la marque, comme sur les avis HAS (meme index).
72
+ alert: [
73
+ {
74
+ fr: "Tags de la marque",
75
+ en: "Brand tags",
76
+ options: [
77
+ { key: "rare", fr: "Maladies rares", en: "Rare diseases", icon: "Virus" },
78
+ { key: "oncoIcd10", fr: "Oncologie CIM-10", en: "Oncology ICD-10", icon: "ListNumbers", variant: "gray" },
79
+ { key: "oncoTerms", fr: "Termes oncologie", en: "Oncology terms", icon: "Stethoscope", variant: "gray" },
80
+ { key: "genes", fr: "Gènes", en: "Genes", icon: "Dna", variant: "violet" },
81
+ ],
82
+ },
83
+ {
84
+ fr: "Alerte",
85
+ en: "Alert",
86
+ options: [
87
+ { key: "cis", fr: "Codes CIS", en: "CIS codes", icon: "Barcode", variant: "gray" },
88
+ ],
89
+ },
90
+ ],
91
+
92
+ // Repertoire ANSM : le tableau des CIS d'une marque. Ici une option n'ajoute
93
+ // pas une rangee sous la carte mais une colonne au tableau, les champs
94
+ // vivant sur la specialite et non sur la marque.
95
+ ansm: [
96
+ {
97
+ fr: "Colonnes du tableau",
98
+ en: "Table columns",
99
+ options: [
100
+ { key: "form", fr: "Forme pharmaceutique", en: "Pharmaceutical form", icon: "Pill" },
101
+ { key: "route", fr: "Voie d'administration", en: "Route of administration", icon: "Syringe" },
102
+ { key: "procedure", fr: "Type de procédure", en: "Procedure type", icon: "Stamp" },
103
+ { key: "generic", fr: "Groupe générique", en: "Generic group", icon: "CopySimple" },
104
+ ],
105
+ },
106
+ ],
107
+
108
+ // Referentiel LES (JO) : carte d'indication.
109
+ les: [
110
+ {
111
+ fr: "Tags de l'indication",
112
+ en: "Indication tags",
113
+ options: [
114
+ { key: "rare", fr: "Maladies rares", en: "Rare diseases", icon: "Virus" },
115
+ { key: "oncoIcd10", fr: "Oncologie CIM-10", en: "Oncology ICD-10", icon: "ListNumbers", variant: "gray" },
116
+ { key: "oncoTerms", fr: "Termes oncologie", en: "Oncology terms", icon: "Stethoscope", variant: "gray" },
117
+ ],
118
+ },
119
+ {
120
+ fr: "Inscription",
121
+ en: "Listing",
122
+ options: [
123
+ { key: "statut", fr: "Statut d'inscription", en: "Listing status", icon: "Certificate", variant: "sky" },
124
+ { key: "endDate", fr: "Fin de prise en charge", en: "End of coverage", icon: "CalendarX", variant: "amber" },
125
+ ],
126
+ },
127
+ {
128
+ fr: "Produit",
129
+ en: "Product",
130
+ options: [
131
+ { key: "drugType", fr: "Type de produit", en: "Product type", icon: "Pill", variant: "indigo" },
132
+ { key: "ucd", fr: "Codes UCD", en: "UCD codes", icon: "Barcode", variant: "gray" },
133
+ ],
134
+ },
135
+ ],
136
+
137
+ // Medicaments d'interet therapeutique majeur : carte CIS.
138
+ mitm: [
139
+ {
140
+ fr: "Dernier avis",
141
+ en: "Latest opinion",
142
+ options: [
143
+ { key: "hasReason", fr: "Motif de l'avis", en: "Opinion reason", icon: "Scales", variant: "indigo" },
144
+ { key: "hasCode", fr: "Référence HAS", en: "HAS reference", icon: "Hash", variant: "gray" },
145
+ { key: "opinionCount", fr: "Nombre d'avis", en: "Opinion count", icon: "Files", variant: "sky" },
146
+ ],
147
+ },
148
+ {
149
+ fr: "Produit",
150
+ en: "Product",
151
+ options: [
152
+ { key: "atc4", fr: "Classe ATC4", en: "ATC4 class", icon: "Tag", variant: "gray" },
153
+ { key: "sameMolecule", fr: "Marques de la molécule", en: "Brands of the molecule", icon: "Pill", variant: "violet" },
154
+ { key: "prices", fr: "Prix et remboursement", en: "Price and reimbursement", icon: "Money", variant: "emerald" },
155
+ ],
156
+ },
157
+ ],
158
+
159
+ // PNDS : carte de pathologie.
160
+ pnds: [
161
+ {
162
+ fr: "Tags de la pathologie",
163
+ en: "Pathology tags",
164
+ options: [
165
+ { key: "rare", fr: "Maladies rares", en: "Rare diseases", icon: "Virus" },
166
+ ],
167
+ },
168
+ {
169
+ fr: "Protocole",
170
+ en: "Protocol",
171
+ options: [
172
+ { key: "docs", fr: "Documents", en: "Documents", icon: "Files", variant: "gray" },
173
+ ],
174
+ },
175
+ ],
176
+
177
+ // P.U.T. : carte de protocole d'utilisation therapeutique.
178
+ put: [
179
+ {
180
+ fr: "Produit",
181
+ en: "Product",
182
+ options: [
183
+ { key: "brand", fr: "Marque", en: "Brand", icon: "Pill", variant: "indigo" },
184
+ { key: "dci", fr: "DCI", en: "INN", icon: "Flask", variant: "violet" },
185
+ { key: "therapeuticArea", fr: "Aire thérapeutique", en: "Therapeutic area", icon: "Stethoscope", variant: "sky" },
186
+ ],
187
+ },
188
+ {
189
+ fr: "Protocole",
190
+ en: "Protocol",
191
+ options: [
192
+ { key: "docs", fr: "Documents", en: "Documents", icon: "Files", variant: "gray" },
193
+ ],
194
+ },
195
+ ],
196
+ };
197
+
198
+ /** Les options d'une page, a plat et dans l'ordre d'affichage. */
199
+ export function cardPrefOptions(scope: CardPrefScope): CardPrefOption[] {
200
+ return (CARD_PREF_GROUPS[scope] ?? []).flatMap((group) => group.options);
201
+ }
@@ -0,0 +1,121 @@
1
+ import { ES_INDEX } from "../esIndices";
2
+ import type { CurrencyRateSource } from "../types/heart/CurrencyRate";
3
+ import useOipDrugsElastic from "./useOipDrugsElastic";
4
+
5
+ /**
6
+ * Portage du store `CurrencyStore` de fr_drugs_app (useCurrencyStore), avec
7
+ * le chargement que faisait useHeartCurrency dans les layouts de l'app : les
8
+ * taux de change sont une donnee de reference, lue une fois par session dans
9
+ * l'index CURRENCY_RATE et partagee par toutes les fiches.
10
+ *
11
+ * Memes lecteurs que le store (`getCurrency`, `getCurrencyByCountryCode`...).
12
+ * `currencies` est une ref (le store la deballait) : `currencies.value`.
13
+ * `load()` est appele a la creation ; un composant qui a besoin des taux
14
+ * avant de rendre peut l'attendre (`await useCurrency().load()`).
15
+ */
16
+
17
+ let pending: Promise<void> | null = null;
18
+
19
+ export function useCurrency() {
20
+ const currencies = useState<CurrencyRateSource[] | null>("oip-drugs:currencies", () => null);
21
+
22
+ function setCurrency(list: CurrencyRateSource[]) {
23
+ currencies.value = list;
24
+ }
25
+
26
+ function load(): Promise<void> {
27
+ if (currencies.value) return Promise.resolve();
28
+ pending ??= useOipDrugsElastic()
29
+ .queryCustomElastic({
30
+ index: ES_INDEX.CURRENCY_RATE,
31
+ customQuery: {
32
+ from: 0,
33
+ query: {
34
+ bool: {
35
+ must: [{ match_all: {} }],
36
+ },
37
+ },
38
+ size: 20,
39
+ track_total_hits: true,
40
+ },
41
+ })
42
+ .then((res: any) => {
43
+ if (res?.results?.length) setCurrency(res.results);
44
+ })
45
+ .finally(() => {
46
+ pending = null;
47
+ });
48
+ return pending;
49
+ }
50
+
51
+ function getCurrency(currency: string): CurrencyRateSource | undefined {
52
+ return currencies.value?.find((x) => x.country_code === currency);
53
+ }
54
+
55
+ // Équivalence entre devises et codes ISO des pays
56
+ function getCurrencyToCountryMapping() {
57
+ return {
58
+ "USD": "US", // United States (pas dans la liste mais USD principal)
59
+ "EUR": ["DE", "ES", "FR", "IT"], // Germany, Spain, France, Italy
60
+ "CHF": "CH", // Switzerland
61
+ "BRL": "BR", // Brazil
62
+ "SEK": "SE", // Sweden (pas dans la liste mais SEK principal)
63
+ "GBP": "GB", // United Kingdom
64
+ "NOK": "NO", // Norway
65
+ "AUD": "AU", // Australia
66
+ "PLN": "PL", // Poland
67
+ "JPY": "JP", // Japan
68
+ "DKK": "DK", // Denmark
69
+ "KRW": "KR", // South Korea
70
+ };
71
+ }
72
+
73
+ // Fonction inverse : obtenir la devise à partir du code pays
74
+ function getCountryToCurrencyMapping() {
75
+ return {
76
+ "AU": "AUD", // Australia
77
+ "BR": "BRL", // Brazil
78
+ "CH": "CHF", // Switzerland
79
+ "DE": "EUR", // Germany
80
+ "DK": "DKK", // Denmark
81
+ "ES": "EUR", // Spain
82
+ "FR": "EUR", // France
83
+ "GB": "GBP", // United Kingdom
84
+ "IT": "EUR", // Italy
85
+ "JP": "JPY", // Japan
86
+ "KR": "KRW", // South Korea
87
+ "NO": "NOK", // Norway
88
+ "PL": "PLN", // Poland
89
+ };
90
+ }
91
+
92
+ // Obtenir la devise d'un pays spécifique
93
+ function getCurrencyByCountryCode(countryCode: string): string | null {
94
+ const mapping = getCountryToCurrencyMapping();
95
+ return mapping[countryCode as keyof typeof mapping] || null;
96
+ }
97
+
98
+ // Obtenir les pays qui utilisent une devise spécifique
99
+ function getCountriesByCurrency(currency: string): string | string[] | null {
100
+ const mapping = getCurrencyToCountryMapping();
101
+ return mapping[currency as keyof typeof mapping] || null;
102
+ }
103
+
104
+ if (import.meta.client) load();
105
+
106
+ return {
107
+ currencies,
108
+ load,
109
+ setCurrency,
110
+ getCurrency,
111
+ getCurrencyToCountryMapping,
112
+ getCountryToCurrencyMapping,
113
+ getCurrencyByCountryCode,
114
+ getCountriesByCurrency,
115
+ };
116
+ }
117
+
118
+ /** Le nom du store d'origine, pour les portages qui l'appelaient. */
119
+ export const useCurrencyStore = useCurrency;
120
+
121
+ export default useCurrency;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * useDirectusSources - le referentiel Directus des sources citees par l'app.
3
+ *
4
+ * Une source s'affiche a deux endroits : le panneau « Sources » du rail et le
5
+ * pied de chaque carte (« Source : X, verifie le Y »). Les deux doivent lire
6
+ * le meme referentiel, sinon la meme source porte deux dates differentes selon
7
+ * l'endroit ou on la lit. Le panneau lisait deja Directus ; les cartes
8
+ * lisaient une agregation Elasticsearch sur le dernier millesime de la donnee,
9
+ * ce qui faisait passer « 2025 » (millesime) pour une date de verification.
10
+ *
11
+ * Tout passe donc par ici : un uuid par jeu, une requete groupee vers
12
+ * /api/oip/sources (route du layer Romulus, qui proxie Directus cote serveur), un cache partage.
13
+ */
14
+
15
+ import type { RmExportSource } from "@tekkare/romulus/utils/export";
16
+
17
+ /** uuid Directus de chaque jeu cite par l'app, par cle de jeu. */
18
+ export const SOURCE_UUIDS = {
19
+ medicam: "71949060-2e0f-4493-beab-6c8200dbec21",
20
+ openmedic: "3d52dae5-7f70-4700-8633-10dd0a30eb26",
21
+ phmev: "7184cd9a-9f31-4f4b-99de-0d284f04d974",
22
+ sus: "cf928a21-47a3-4722-923a-0d8642dc900c",
23
+ retrocession: "ba08baa4-d29b-44c3-bd8a-dff04737f525",
24
+ price: "a4d3d627-97fe-4a8e-ac1c-f926e2d3f566",
25
+ has: "6f0bfe9a-bc58-4feb-9e9c-f37a0ac9ceba",
26
+ nct: "224f4621-f6e7-4635-a7d9-371e212d5ca6",
27
+ euct: "a81861dc-3a42-4019-a93b-e0e83edcaf41",
28
+ finess: "545ba237-b92d-47fc-b336-25972873d0b2",
29
+ rpps: "fee124b8-7cb7-41a0-825b-6a70ce975834",
30
+ jca: "4a311879-080e-4c26-bfc2-5c3a80f9758b",
31
+ } as const;
32
+
33
+ export type SourceKey = keyof typeof SOURCE_UUIDS;
34
+
35
+ /** Ce qu'une carte a besoin de dire : qui produit la donnee, et depuis quand. */
36
+ export interface SourceMeta {
37
+ /** « Assurance Maladie (Open Medic) » : le producteur, puis le jeu. */
38
+ name: string;
39
+ /** Date de notre dernier chargement, mise au format de la langue lue. */
40
+ verified: string;
41
+ /**
42
+ * Le surnom Directus du jeu (« DE - PRICE TRACKER », « AEMPS »). Court, il
43
+ * nomme la source la ou « Producteur (jeu) » deborde, par exemple quand une
44
+ * carte en cite deux.
45
+ */
46
+ nickname?: string;
47
+ }
48
+
49
+ interface DirectusSource {
50
+ source_uuid: string;
51
+ Nickname?: string | null;
52
+ latest_download_date?: string | null;
53
+ translations?: { languages_code?: string; source_name?: string | null }[] | null;
54
+ provider?: {
55
+ translations?: { languages_code?: string; provider_name?: string | null }[] | null;
56
+ } | null;
57
+ }
58
+
59
+ /** Tant que Directus n'a pas repondu, le pied de carte reste muet plutot que faux. */
60
+ export const PENDING = "…";
61
+
62
+ function pickTranslation(
63
+ rows: { languages_code?: string; [k: string]: any }[] | null | undefined,
64
+ field: string,
65
+ lang: string,
66
+ ) {
67
+ const list = rows ?? [];
68
+ const wanted = lang === "fr" ? "fr-FR" : "en-US";
69
+ const hit =
70
+ list.find((t) => t.languages_code === wanted) ??
71
+ list.find((t) => t.languages_code === "en-US") ??
72
+ list[0];
73
+ const value = hit?.[field];
74
+ return typeof value === "string" ? value.trim() : "";
75
+ }
76
+
77
+ /**
78
+ * « Producteur (jeu) », la forme qu'avaient les libelles ecrits en dur. Un nom
79
+ * de jeu qui porte deja une parenthese passe apres une virgule : imbriquer
80
+ * deux parentheses se lit mal.
81
+ */
82
+ function buildName(source: DirectusSource, lang: string) {
83
+ const dataset =
84
+ pickTranslation(source.translations, "source_name", lang) ||
85
+ (source.Nickname ?? "").trim();
86
+ const provider = pickTranslation(
87
+ source.provider?.translations,
88
+ "provider_name",
89
+ lang,
90
+ );
91
+ if (!provider) return dataset;
92
+ if (!dataset) return provider;
93
+ if (dataset.includes("(")) return `${provider}, ${dataset}`;
94
+ return `${provider} (${dataset})`;
95
+ }
96
+
97
+ function formatVerified(raw: string | null | undefined, lang: string) {
98
+ if (!raw) return PENDING;
99
+ const parsed = new Date(raw);
100
+ if (Number.isNaN(parsed.getTime())) return String(raw);
101
+ return parsed.toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US", {
102
+ day: "numeric",
103
+ month: "long",
104
+ year: "numeric",
105
+ });
106
+ }
107
+
108
+ export default function useDirectusSources() {
109
+ const cache = useState<Record<string, DirectusSource>>(
110
+ "oip-drugs:directus:sources",
111
+ () => ({}),
112
+ );
113
+ const inflight = useState<Promise<void> | null>(
114
+ "oip-drugs:directus:sources:inflight",
115
+ () => null,
116
+ );
117
+
118
+ /** Charge les uuids absents du cache. Idempotent, et groupe en une requete. */
119
+ async function load(uuids: string[]) {
120
+ // Une requete en vol ne porte pas forcement nos uuids : on l'attend, puis
121
+ // on recalcule ce qui manque encore, sinon les pieds de carte restaient
122
+ // sur « … ».
123
+ while (inflight.value) await inflight.value;
124
+ const missing = [...new Set(uuids)].filter((u) => u && !cache.value[u]);
125
+ if (missing.length === 0) return;
126
+
127
+ inflight.value = $fetch<{ sources: DirectusSource[] }>("/api/oip/sources", {
128
+ method: "POST",
129
+ body: { uuids: missing },
130
+ })
131
+ .then((res) => {
132
+ for (const source of res?.sources ?? []) {
133
+ cache.value[source.source_uuid] = source;
134
+ }
135
+ })
136
+ .catch((e: unknown) => {
137
+ console.warn("[useDirectusSources] load failed", e);
138
+ })
139
+ .finally(() => {
140
+ inflight.value = null;
141
+ });
142
+
143
+ return inflight.value;
144
+ }
145
+
146
+ /** Charge tous les jeux du registre : l'app en cite une dizaine, pas mille. */
147
+ function loadAll() {
148
+ return load(Object.values(SOURCE_UUIDS));
149
+ }
150
+
151
+ /**
152
+ * Source adressee par son uuid : Hub-R cite ses sources comme ca (un uuid
153
+ * par onglet de fiche), sans passer par les cles du registre.
154
+ */
155
+ function metaByUuid(uuid: string, lang: string): SourceMeta {
156
+ const source = uuid ? cache.value[uuid] : undefined;
157
+ if (!source) return { name: PENDING, verified: PENDING };
158
+ return {
159
+ name: buildName(source, lang),
160
+ verified: formatVerified(source.latest_download_date, lang),
161
+ nickname: (source.Nickname ?? "").trim() || undefined,
162
+ };
163
+ }
164
+
165
+ function meta(key: SourceKey, lang: string): SourceMeta {
166
+ return metaByUuid(SOURCE_UUIDS[key], lang);
167
+ }
168
+
169
+ /**
170
+ * Date brute (ISO) de la source, pour les composants qui la formatent
171
+ * eux-memes (RmDate en pied de tiroir).
172
+ */
173
+ function verifiedRaw(uuid: string): string | null {
174
+ return (uuid ? cache.value[uuid]?.latest_download_date : null) ?? null;
175
+ }
176
+
177
+ /**
178
+ * Plusieurs jeux cites par une meme carte (le portail lit les trois canaux a
179
+ * la fois). La date retenue est la PLUS ANCIENNE : une donnee agregee n'est
180
+ * a jour que jusqu'a son entree la plus vieille.
181
+ */
182
+ /**
183
+ * Les sources d'une liste d'uuids telles que les generateurs d'export les
184
+ * attendent : le libelle « Producteur (jeu) » et la date brute de notre
185
+ * dernier chargement, que le classeur, le rapport et l'image formatent
186
+ * eux-memes. Une source que Directus n'a pas encore rendue est omise plutot
187
+ * que citee sans nom.
188
+ */
189
+ function exportSources(uuids: string[], lang: string): RmExportSource[] {
190
+ return [...new Set(uuids)]
191
+ .map((uuid) => cache.value[uuid])
192
+ .filter((source): source is DirectusSource => !!source)
193
+ .map((source) => ({
194
+ name: buildName(source, lang),
195
+ date: source.latest_download_date ?? null,
196
+ }));
197
+ }
198
+
199
+ function combinedMeta(keys: SourceKey[], lang: string): SourceMeta {
200
+ const sources = keys
201
+ .map((k) => cache.value[SOURCE_UUIDS[k]])
202
+ .filter(Boolean) as DirectusSource[];
203
+ if (sources.length === 0) return { name: PENDING, verified: PENDING };
204
+
205
+ const providers = [
206
+ ...new Set(
207
+ sources
208
+ .map((s) => pickTranslation(s.provider?.translations, "provider_name", lang))
209
+ .filter(Boolean),
210
+ ),
211
+ ];
212
+ const dates = sources
213
+ .map((s) => s.latest_download_date)
214
+ .filter(Boolean) as string[];
215
+ const oldest = dates.sort()[0];
216
+
217
+ return {
218
+ name: providers.join(" & ") || PENDING,
219
+ verified: formatVerified(oldest, lang),
220
+ };
221
+ }
222
+
223
+ return { cache, load, loadAll, meta, metaByUuid, verifiedRaw, combinedMeta, exportSources };
224
+ }