@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,57 @@
1
+ /**
2
+ * Passerelle entre les anciens exports et le panneau d'export du cadre.
3
+ *
4
+ * Les modules du portail construisaient leur classeur a la main, avec SheetJS :
5
+ * une liste d'objets plats par onglet, l'intitule de colonne servant de cle
6
+ * (`obj["Prix public (€)"] = ...`). Le panneau, lui, attend le schema de
7
+ * `write-excel-file` (une colonne = un intitule plus un lecteur de ligne), le
8
+ * meme que les exports XLSX de heart.
9
+ *
10
+ * Plutot que de retranscrire a la main les centaines de colonnes deja ecrites,
11
+ * on derive le schema des cles des lignes : ce sont deja les intitules, dans
12
+ * l'ordre voulu. Une page qui veut typer une colonne (nombre, date, largeur)
13
+ * declare son schema directement, ce helper n'est la que pour reprendre
14
+ * l'existant sans le reecrire.
15
+ */
16
+ import type { RmExportSheet } from "@tekkare/romulus/utils/export";
17
+
18
+ /** Un onglet tel que les anciens `download.vue` le construisent. */
19
+ export interface FlatSheet {
20
+ name: string;
21
+ rows: Record<string, any>[];
22
+ /** Colonne qui titre les fiches du rapport PDF (defaut : la premiere). */
23
+ titleColumn?: string;
24
+ /** Colonne posee sous le titre de la fiche. */
25
+ subtitleColumn?: string;
26
+ }
27
+
28
+ /**
29
+ * Toutes les cles rencontrees, dans l'ordre d'apparition. Les lignes d'un meme
30
+ * onglet ne portent pas toujours les memes champs (une valeur absente n'est pas
31
+ * posee sur l'objet) : se fier a la premiere ligne perdrait des colonnes.
32
+ */
33
+ function columnsOf(rows: Record<string, any>[]): string[] {
34
+ const seen = new Set<string>();
35
+ rows.forEach((row) => Object.keys(row ?? {}).forEach((key) => seen.add(key)));
36
+ return [...seen];
37
+ }
38
+
39
+ /**
40
+ * Convertit des onglets plats en onglets du panneau. Les onglets vides sont
41
+ * ecartes : le classeur n'a pas a porter une feuille sans ligne, et un onglet
42
+ * vide ferait proposer un format tabulaire sur une page qui n'a rien a sortir.
43
+ */
44
+ export function exportSheets(sheets: FlatSheet[]): RmExportSheet[] {
45
+ return sheets
46
+ .filter((sheet) => sheet.rows.length > 0)
47
+ .map((sheet) => ({
48
+ name: sheet.name,
49
+ schema: columnsOf(sheet.rows).map((column) => ({
50
+ column,
51
+ value: (row: any) => row?.[column] ?? null,
52
+ })),
53
+ data: sheet.rows,
54
+ titleColumn: sheet.titleColumn,
55
+ subtitleColumn: sheet.subtitleColumn,
56
+ }));
57
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Prix internationaux : perimetre, devises, organismes et mise en forme des
3
+ * lignes du suivi mondial (`ope__ww_price_tracker__tekkare`).
4
+ *
5
+ * Le perimetre est celui du regard international de la fiche produit :
6
+ * Allemagne, Espagne, Italie, Royaume-Uni et Etats-Unis, les cinq pays dont
7
+ * l'app lit aussi le registre d'autorisation. La table des devises EST la
8
+ * liste des pays : la requete de la fiche filtre dessus, et un pays absent
9
+ * d'ici ne peut apparaitre nulle part.
10
+ */
11
+ export const HEART_INTL_PRICE_CURRENCY: Record<string, string> = {
12
+ DE: "EUR",
13
+ ES: "EUR",
14
+ GB: "GBP",
15
+ IT: "EUR",
16
+ US: "USD",
17
+ };
18
+
19
+ /**
20
+ * L'organisme qui publie les prix de chaque pays. Le suivi mondial agrege des
21
+ * referentiels nationaux : citer une source unique reviendrait a attribuer a
22
+ * l'Assurance Maladie des tarifs allemands ou americains.
23
+ */
24
+ export const HEART_INTL_PRICE_SOURCE: Record<string, string> = {
25
+ DE: "Vidal GmbH (Gelbe Liste Pharmindex)",
26
+ ES: "Vidal Vademecum",
27
+ GB: "NHSBSA (dm+d)",
28
+ IT: "AIFA (Transparency List History)",
29
+ US: "CMS (ASP) / Department of Veterans Affairs",
30
+ };
31
+
32
+ /** Les codes pays du perimetre, tels que la requete Elasticsearch les filtre. */
33
+ export const HEART_INTL_PRICE_COUNTRIES = Object.keys(HEART_INTL_PRICE_CURRENCY);
34
+
35
+ /** Libelles des types de prix du suivi mondial (price_type_id). */
36
+ export const HEART_PRICE_TYPE_LABELS: Record<number, string> = {
37
+ 1: "Ex Factory",
38
+ 2: "Wholesale",
39
+ 11: "Public/Retail",
40
+ 12: "Reimbursement",
41
+ 13: "Government",
42
+ 14: "Reimbursement Rate",
43
+ 21: "Reimbursement",
44
+ 101: "FSS",
45
+ 102: "Big4",
46
+ 103: "NC",
47
+ 104: "ASP",
48
+ 201: "AIP",
49
+ 202: "AUP",
50
+ 203: "AUP pr DDD",
51
+ 204: "DDD",
52
+ 2350: "Private Pediatric",
53
+ 2351: "Private Pediatric (VFC)",
54
+ 2352: "Private Adult",
55
+ 2353: "CDC Pediatric",
56
+ 2354: "CDC Pediatric (VFC)",
57
+ 2355: "CDC Adult",
58
+ };
59
+
60
+ /**
61
+ * Ordre du prix de reference : le prix fabricant d'abord, parce que c'est le
62
+ * seul qui se compare d'un pays a l'autre sans les marges locales ; a defaut
63
+ * le prix de gros, puis le prix public, puis les tarifs de remboursement.
64
+ */
65
+ const REFERENCE_ORDER = [1, 2, 201, 101, 11, 202, 102, 103, 13, 12, 21, 14, 203, 204];
66
+
67
+ /**
68
+ * Types ecartes du prix de reference : l'ASP americain est un prix moyen au
69
+ * milligramme (il ne se compare pas a un conditionnement) et les tarifs
70
+ * vaccinaux repondent a des circuits d'achat particuliers.
71
+ */
72
+ const EXCLUDED_TYPES = new Set([104, 2350, 2351, 2352, 2353, 2354, 2355]);
73
+
74
+ export interface HeartIntlPriceType {
75
+ type: number;
76
+ label: string;
77
+ local: number;
78
+ eur: number | null;
79
+ date: string | null;
80
+ variation: number | null;
81
+ }
82
+
83
+ /** Un changement de prix, tel que l'index le date. */
84
+ export interface HeartIntlPriceChange extends HeartIntlPriceType {
85
+ /** Le prix qui s'appliquait avant ce changement. */
86
+ previous: number | null;
87
+ /** « NEW », « N » (inchange), « Y »... tel que la source l'ecrit. */
88
+ changed: string | null;
89
+ /** Date de la collecte qui a rapporte ce changement. */
90
+ sourceUpdate: string | null;
91
+ }
92
+
93
+ export interface HeartIntlPriceRow {
94
+ key: string;
95
+ iso: string;
96
+ country: string;
97
+ product: string;
98
+ category: string | null;
99
+ manufacturer: string | null;
100
+ localId: string | null;
101
+ currency: string;
102
+ typeLabel: string;
103
+ priceLocal: number;
104
+ priceEur: number | null;
105
+ variation: number | null;
106
+ variationPct: number | null;
107
+ date: string | null;
108
+ sourceLabel: string | null;
109
+ /** Le dernier prix connu de chaque type publie sur la presentation. */
110
+ types: HeartIntlPriceType[];
111
+ /** Tous les changements de prix, du plus recent au plus ancien. */
112
+ history: HeartIntlPriceChange[];
113
+ /** Ce que l'index sait du produit, pour le tiroir de detail. */
114
+ brandLocal: string | null;
115
+ molecule: string | null;
116
+ atc: string | null;
117
+ marketedOn: string | null;
118
+ lastPriceUpdate: string | null;
119
+ lastSourceUpdate: string | null;
120
+ }
121
+
122
+ /** Le dernier prix connu de chaque type, dans un document du suivi. */
123
+ function latestByType(doc: any) {
124
+ const latest = new Map<number, any>();
125
+ for (const entry of doc?.price_history ?? []) {
126
+ if (entry?.new_price == null) continue;
127
+ const known = latest.get(entry.price_type_id);
128
+ if (!known || new Date(entry.application_date) > new Date(known.application_date)) {
129
+ latest.set(entry.price_type_id, entry);
130
+ }
131
+ }
132
+ return latest;
133
+ }
134
+
135
+ /**
136
+ * Une ligne par presentation tarifee d'un pays du perimetre.
137
+ *
138
+ * `rate` donne le taux de conversion vers l'euro de la devise du pays ; sans
139
+ * taux connu, la ligne garde son prix local et son prix en euros reste nul,
140
+ * plutot que de sortir un montant faux.
141
+ *
142
+ * Le meme document peut arriver deux fois (l'index garde des instantanes) : la
143
+ * cle pays + identifiant local + libelle les rassemble, le plus recent gagne.
144
+ */
145
+ export function heartIntlPriceRows(
146
+ docs: any[],
147
+ rate: (iso: string) => number | null,
148
+ ): HeartIntlPriceRow[] {
149
+ const byKey = new Map<string, HeartIntlPriceRow>();
150
+
151
+ for (const doc of docs ?? []) {
152
+ const iso = String(doc?.country?.country_iso_2 || doc?.country_iso_2 || "").toUpperCase();
153
+ if (!HEART_INTL_PRICE_CURRENCY[iso]) continue;
154
+
155
+ const latest = latestByType(doc);
156
+ const referenceType = REFERENCE_ORDER.find(
157
+ (type) => latest.has(type) && !EXCLUDED_TYPES.has(type),
158
+ );
159
+ if (referenceType === undefined) continue;
160
+
161
+ const reference = latest.get(referenceType);
162
+ const eurRate = rate(iso);
163
+ const toEur = (value: number) => (eurRate === null ? null : value * eurRate);
164
+ const key = `${iso}-${doc?.local_drug_id ?? ""}-${doc?.label_local ?? ""}`;
165
+ const variation = reference.price_variation || null;
166
+ const previous =
167
+ typeof variation === "number" && variation !== 0 ? reference.new_price - variation : null;
168
+
169
+ const row: HeartIntlPriceRow = {
170
+ key,
171
+ iso,
172
+ country: doc?.country?.country_label || doc?.country_name || iso,
173
+ product: doc?.label_local || doc?.brand || "",
174
+ category: doc?.price_category && doc.price_category !== "Unknown" ? doc.price_category : null,
175
+ manufacturer: doc?.manufacturer || null,
176
+ localId: doc?.local_drug_id ? String(doc.local_drug_id) : null,
177
+ currency: HEART_INTL_PRICE_CURRENCY[iso]!,
178
+ typeLabel: HEART_PRICE_TYPE_LABELS[referenceType] || `Type ${referenceType}`,
179
+ priceLocal: reference.new_price,
180
+ priceEur: toEur(reference.new_price),
181
+ variation,
182
+ variationPct:
183
+ previous && previous !== 0 ? ((variation as number) / previous) * 100 : null,
184
+ date: reference.application_date ?? doc?.last_price_update ?? null,
185
+ sourceLabel: HEART_INTL_PRICE_SOURCE[iso] ?? null,
186
+ types: [...latest.entries()]
187
+ .map(([type, entry]) => ({
188
+ type,
189
+ label: HEART_PRICE_TYPE_LABELS[type] || `Type ${type}`,
190
+ local: entry.new_price as number,
191
+ eur: toEur(entry.new_price),
192
+ date: entry.application_date ?? null,
193
+ variation: entry.price_variation || null,
194
+ }))
195
+ .sort((a, b) => b.local - a.local),
196
+ history: (doc?.price_history ?? [])
197
+ .filter((entry: any) => entry?.new_price != null)
198
+ .map((entry: any) => ({
199
+ type: entry.price_type_id,
200
+ label: HEART_PRICE_TYPE_LABELS[entry.price_type_id] || `Type ${entry.price_type_id}`,
201
+ local: entry.new_price as number,
202
+ eur: toEur(entry.new_price),
203
+ date: entry.application_date ?? null,
204
+ variation: entry.price_variation ?? null,
205
+ previous: entry.last_price ?? null,
206
+ changed: entry.status_changed ?? null,
207
+ sourceUpdate: entry.source_update ?? null,
208
+ }))
209
+ .sort(
210
+ (a: HeartIntlPriceChange, b: HeartIntlPriceChange) =>
211
+ new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime(),
212
+ ),
213
+ brandLocal: doc?.brand || null,
214
+ molecule: doc?.molecule_en || doc?.molecule_local || null,
215
+ atc: doc?.atc5_code || doc?.atc_who || doc?.atc_local || null,
216
+ marketedOn: doc?.commercialization_date || null,
217
+ lastPriceUpdate: doc?.last_price_update ?? null,
218
+ lastSourceUpdate: doc?.last_source_update ?? null,
219
+ };
220
+
221
+ const known = byKey.get(key);
222
+ if (!known || new Date(row.date || 0) > new Date(known.date || 0)) byKey.set(key, row);
223
+ }
224
+
225
+ // Le plus cher d'abord : c'est le conditionnement de reference du pays, les
226
+ // fractions de gamme se lisent en dessous.
227
+ return [...byKey.values()].sort((a, b) => (b.priceEur ?? 0) - (a.priceEur ?? 0));
228
+ }
@@ -0,0 +1,262 @@
1
+ // Le suivi PostHog reste dans l'app qui monte les fiches : le layer ne
2
+ // l'embarque pas. Les deux fonctions gardent leur nom pour les portages, et
3
+ // ne font rien.
4
+ export const heartSendToPosthog = (_eventName: string, _eventProps: any) => {};
5
+
6
+ export const heartCaptureException = (_message: string, _properties: {}) => {};
7
+
8
+ export const heartFormatNumber = (value: any) => {
9
+ if(!value && value !== 0) return 'N/A';
10
+ return value.toLocaleString('fr-FR');
11
+ };
12
+
13
+ export const heartFormatDate = (value: any) => {
14
+ if(!value || value === '-') return '-';
15
+ const date = new Date(value);
16
+ if (isNaN(date.getTime())) return '-';
17
+ return date.toLocaleDateString('fr-FR', {
18
+ // weekday: 'long',
19
+ year: 'numeric',
20
+ month: 'short',
21
+ day: 'numeric',
22
+ });
23
+ };
24
+
25
+ export const heartGetIconfromField = (field: any) => {
26
+ field = field.toLowerCase()
27
+ // Avant le test sur 'label' : les champs de l'annuaire s'appellent
28
+ // association_label*, ils tomberaient sinon dans l'icone des produits.
29
+ if (field.includes('association')) {
30
+ return 'UsersThree'
31
+ }
32
+ // `Flask`, et non `Molecule` : Phosphor n'a pas d'icone de ce nom, RmIcon
33
+ // repliait donc sur son point d'interrogation — le type de suggestion le
34
+ // plus frequent s'affichait sans icone. C'est aussi celle que l'ecran
35
+ // Screening pose deja sur ses molecules.
36
+ else if (field.includes('molecule') || field.includes('ingredient') || field === 'atc5_fr') {
37
+ return 'Flask'
38
+ }
39
+ else if (field.includes('manufacturer') || field.includes('company') || field.includes('supplier')) {
40
+ return 'Factory'
41
+ }
42
+ else if (field.includes('atc') || field.includes('category')) {
43
+ return 'TagSimple'
44
+ }
45
+ else if (field.includes('theme')) {
46
+ return 'Bookmarks'
47
+ }
48
+
49
+ else if (field.includes('label') || field.includes('product') || field.includes('name')) {
50
+ return 'Pill'
51
+ }
52
+ else if (field.includes('brand')) {
53
+ return 'Package'
54
+ }
55
+ else if (field.includes('indication') | field.includes('notes')) {
56
+ return 'FirstAid'
57
+ }
58
+ else return 'CrosshairSimple'
59
+ }
60
+
61
+ /**
62
+ * Libelle lisible du type d'une suggestion.
63
+ *
64
+ * Prend la cle d'icone rendue par `heartGetIconfromField` plutot que le champ
65
+ * brut : cette fonction porte deja tout l'arbre de decision (quels champs sont
66
+ * une molecule, une marque, un fabricant...). En repartir evite d'entretenir
67
+ * deux classifications qui divergeraient au premier champ ajoute.
68
+ */
69
+ const SUGGESTION_TYPE_LABELS: Record<string, { fr: string; en: string }> = {
70
+ Flask: { fr: "Molécule", en: "Molecule" },
71
+ UsersThree: { fr: "Association", en: "Association" },
72
+ Package: { fr: "Marque", en: "Brand" },
73
+ Factory: { fr: "Fabricant", en: "Manufacturer" },
74
+ TagSimple: { fr: "Classe ATC", en: "ATC class" },
75
+ Bookmarks: { fr: "Thème", en: "Theme" },
76
+ Pill: { fr: "Produit", en: "Product" },
77
+ FirstAid: { fr: "Indication", en: "Indication" },
78
+ };
79
+
80
+ export const heartGetTypeLabelFromIcon = (icon: string, lang = "fr") => {
81
+ const entry = SUGGESTION_TYPE_LABELS[icon];
82
+ if (!entry) return null;
83
+ return lang === "en" ? entry.en : entry.fr;
84
+ };
85
+
86
+ export const heartFilterKeys = (obj: any, keysToRemove: any) => {
87
+ const newObj = { ...obj };
88
+ keysToRemove.forEach((key: any) => {
89
+ delete newObj[key];
90
+ });
91
+ return newObj;
92
+ }
93
+
94
+ export const heartMapSmr = (value: string, lang: string, type: string) => {
95
+ function getObj(obj: any, value: any) {
96
+ for (const key in obj) {
97
+ if (obj[key] === value) {
98
+ return key;
99
+ }
100
+ }
101
+ }
102
+
103
+ const smr = {
104
+ en: {
105
+ 'Commentaires': 'Comments',
106
+ 'Faible': 'Low',
107
+ 'Important': 'Important',
108
+ 'Insuffisant': 'Insufficient',
109
+ 'Majeur': 'Major',
110
+ 'Modéré': 'Moderate',
111
+ 'Non précisé': 'Not assessed',
112
+ },
113
+ fr: {
114
+ 'Commentaires': 'Commentaires',
115
+ 'Faible': 'Faible',
116
+ 'Important': 'Important',
117
+ 'Insuffisant': 'Insuffisant',
118
+ 'Majeur': 'Majeur',
119
+ 'Modéré': 'Modéré',
120
+ 'Non précisé': 'Non précisé',
121
+ }
122
+ }
123
+ const smrByLang = smr as Record<string, Record<string, string>>;
124
+ return type === 'get' ? smrByLang[lang]?.[value] : getObj(smrByLang[lang], value);
125
+ };
126
+ export const heartMapAsmr = (value: string, lang: string, type: string) => {
127
+ function getObj(obj: any, value: any) {
128
+ for (const key in obj) {
129
+ if (obj[key] === value) {
130
+ return key;
131
+ }
132
+ }
133
+ }
134
+ const asmr = {
135
+ 'en': {
136
+ "Commentaires sans chiffrage de l'ASMR": "Comments with no ASMR number",
137
+ 'I (majeur)': 'I (major)',
138
+ 'II (important)': 'II (important)',
139
+ 'V (absence)': 'V (absence)',
140
+ 'IV (mineur)': 'IV (minor)',
141
+ 'III (modéré)': 'III (moderate)',
142
+ 'Non précisée': 'Not assessed',
143
+ 'Non applicable au dossier': 'Not applicable to topic',
144
+ 'Sans objet': 'Without object',
145
+ },
146
+ 'fr': {
147
+ "Commentaires sans chiffrage de l'ASMR": "Commentaires sans chiffrage de l'ASMR",
148
+ 'I (majeur)': 'I (majeur)',
149
+ 'II (important)': 'II (important)',
150
+ 'V (absence)': 'V (absence)',
151
+ 'IV (mineur)': 'IV (mineur)',
152
+ 'III (modéré)': 'III (modéré)',
153
+ 'Non précisée': 'Non précisée',
154
+ 'Non applicable au dossier': 'Non applicable au dossier',
155
+ 'Sans objet': 'Sans objet',
156
+ }
157
+ }
158
+ const asmrByLang = asmr as Record<string, Record<string, string>>;
159
+ return type === 'get' ? asmrByLang[lang]?.[value] : getObj(asmrByLang[lang], value);
160
+ };
161
+
162
+ /**
163
+ * Corrige le mojibake UTF-8 (ex: "sécurité" → "sécurité").
164
+ * Se produit quand du texte UTF-8 est décodé comme Latin-1 dans la data pipeline.
165
+ */
166
+ export const heartFixMojibake = (str: string | null | undefined): string => {
167
+ if (!str) return '';
168
+ // Détection rapide : si pas de caractères > 127 ou pas de pattern Ã, pas de mojibake
169
+ if (!/[\u00c0-\u00ff]/.test(str)) return str;
170
+ try {
171
+ const bytes = new Uint8Array([...str].map(c => c.charCodeAt(0)));
172
+ const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
173
+ // Vérifier que le décodage a bien amélioré le texte (moins de caractères hauts)
174
+ if (decoded !== str && !decoded.includes('\uFFFD')) {
175
+ return decoded;
176
+ }
177
+ } catch {
178
+ // Le décodage a échoué → ce n'était pas du mojibake
179
+ }
180
+ return str;
181
+ };
182
+
183
+ // Assainit une valeur destinée à une cellule XLSX :
184
+ // - retire les caractères de contrôle illégaux en XML (< 0x20 sauf tab/LF/CR),
185
+ // parfois présents dans les textes HAS extraits de PDF ;
186
+ // - tronque à 32 767 caractères, la limite dure d'Excel par cellule (au-delà,
187
+ // le fichier est considéré comme corrompu).
188
+ export const heartCleanXlsxText = <T>(value: T): T => {
189
+ if (typeof value !== "string") return value;
190
+ let out = "";
191
+ for (let i = 0; i < value.length; i++) {
192
+ const code = value.charCodeAt(i);
193
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13) continue;
194
+ out += value[i];
195
+ }
196
+ const MAX = 32767;
197
+ if (out.length > MAX) out = out.slice(0, MAX - 1) + "…";
198
+ return out as unknown as T;
199
+ };
200
+
201
+ /**
202
+ * Nom lisible d'un document a partir de son URL.
203
+ *
204
+ * Les fichiers HAS et Tekkare arrivent nommes pour la machine
205
+ * (« p_4231687__maladie_coeliaque_-_diagnostic.pdf », parfois avec une
206
+ * double extension « .docx_1.pdf ») : on ne garde que le titre.
207
+ */
208
+ export const heartDocLabel = (url: string) => {
209
+ if (!url) return '';
210
+ const file = decodeURIComponent(url.split('/').pop() ?? '');
211
+ const name = file
212
+ .replace(/(\.(pdf|docx?|xlsx?)(_\d+)?)+$/i, '')
213
+ // Prefixe technique de l'export HAS : l'identifiant du protocole.
214
+ .replace(/^p_\d+__/i, '')
215
+ .replace(/[_-]+/g, ' ')
216
+ .replace(/\s+/g, ' ')
217
+ .trim();
218
+ return name ? name.charAt(0).toUpperCase() + name.slice(1) : file;
219
+ };
220
+
221
+ /**
222
+ * Circuit de distribution d'une presentation tarifaire, en clair.
223
+ *
224
+ * `distribution_channel` est le seul champ de l'index prix qui dise ville ou
225
+ * hopital, et il ne prend que deux valeurs. Les drapeaux `is_outpatient` et
226
+ * `is_inpatient` portent le regime de prise en charge hospitalier (homologation
227
+ * en retrocession, T2A) et ne sont jamais vrais hors `UCD(Hospital)` : s'en
228
+ * servir pour le circuit mettait un tag « Ville » sur des produits purement
229
+ * hospitaliers.
230
+ */
231
+ export const heartPriceChannel = (raw: string | null | undefined, lang = "fr") => {
232
+ if (!raw) return null;
233
+ if (raw === "CIP(Retail)") {
234
+ return {
235
+ key: "retail",
236
+ // Litteral et non `string` : c'est l'union de variantes que RmTag attend,
237
+ // et une chaine large ne lui est pas assignable.
238
+ variant: "teal" as const,
239
+ label: lang === "en" ? "Retail (CIP)" : "Ville (CIP)",
240
+ };
241
+ }
242
+ if (raw === "UCD(Hospital)") {
243
+ return {
244
+ key: "hospital",
245
+ variant: "blue" as const,
246
+ label: lang === "en" ? "Hospital (UCD)" : "Hôpital (UCD)",
247
+ };
248
+ }
249
+ return { key: "other", variant: "gray" as const, label: raw };
250
+ };
251
+
252
+ /**
253
+ * Nom du code porte par une presentation : CIP en ville, UCD a l'hopital.
254
+ *
255
+ * L'index tarifaire range les deux dans `local_drug_id` ; seul
256
+ * `distribution_channel` dit lequel. Sans canal connu, on garde les deux.
257
+ */
258
+ export const heartCodeLabel = (raw: string | null | undefined) => {
259
+ if (raw === "CIP(Retail)") return "CIP";
260
+ if (raw === "UCD(Hospital)") return "UCD";
261
+ return "CIP / UCD";
262
+ };
@@ -0,0 +1,71 @@
1
+ import type { HeartTagColor } from "../types/heart/tag";
2
+ import { openSource } from "./styleAndFormat";
3
+ import { HEART_SOURCE_UUIDS } from "./sourceUuids";
4
+
5
+ export function heartFormatAdministrations(administrations: string): string {
6
+ if (!administrations) return "N/A";
7
+ const base = administrations?.split(';')
8
+
9
+ // join with , and last with '&'
10
+ // also first letter in maj and the rest in lower case for each administration
11
+ for (let i = 0; i < base.length; i++) {
12
+ // trim avant la capitale : « ; orale » commencait par une espace.
13
+ base[i] = base[i]!.trim().charAt(0).toUpperCase() + base[i]!.trim().slice(1).toLowerCase();
14
+ }
15
+
16
+ if (base.length > 1) {
17
+ return base.slice(0, -1).join(', ') + ' & ' + base.slice(-1);
18
+ }
19
+ return base.join(', ');
20
+ }
21
+
22
+ export function heartFirstLetterUpperCase(str: string): string {
23
+ if (!str) return "N/A";
24
+ return str.trim().charAt(0).toUpperCase() + str.trim().slice(1).toLowerCase();
25
+ }
26
+
27
+ export function heartGetStatusColor(status?: string | null): HeartTagColor {
28
+ // Statut absent sur certains CIS : sans garde la carte entiere plantait.
29
+ switch ((status ?? "").toLowerCase().trim()) {
30
+ case 'autorisation active':
31
+ case 'commercialisée':
32
+ case 'présentation active':
33
+ return 'green';
34
+ case 'autorisation abrogée':
35
+ case 'non commercialisée':
36
+ return 'amber';
37
+ case 'autorisation archivée':
38
+ return 'blue';
39
+ case "autorisation suspendue":
40
+ case "présentation abrogée":
41
+ return 'pink';
42
+ default:
43
+ return 'default';
44
+ }
45
+ }
46
+
47
+ export function heartGetBorderColor(status?: string | null): string | undefined {
48
+ switch ((status ?? "").toLowerCase().trim()) {
49
+ case 'présentation active':
50
+ return "#cff2d8ff"
51
+ case "présentation abrogée":
52
+ return "#eec9d8ff"
53
+ }}
54
+
55
+ // Retire le nom de la marque en préfixe d'un libellé CIS (redondant quand on est déjà
56
+ // dans le contexte de la marque). Match token par token en ignorant les séparateurs :
57
+ // le champ `brand` a des espaces là où le libellé CIS peut avoir des « / »
58
+ // (ex. "AMOXICILLINE ACIDE" vs "AMOXICILLINE/ACIDE").
59
+ export function heartStripBrandPrefix(label: string, brand: string): string {
60
+ if (!label || !brand) return label;
61
+ const tokens = brand.trim().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
62
+ if (!tokens.length) return label;
63
+ const SEP = "[^\\p{L}\\p{N}]+";
64
+ const re = new RegExp("^" + tokens.join(SEP) + SEP, "iu");
65
+ return label.replace(re, "").trim() || label;
66
+ }
67
+
68
+ /** Le panneau Sources de l'app, sur la fiche ANSM par defaut (cf. openSource). */
69
+ export function heartOpenSource(uuid: string = HEART_SOURCE_UUIDS.ANSM) {
70
+ openSource(uuid);
71
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * La date la plus recente d'un lot de valeurs, pour le pied des cartes.
3
+ *
4
+ * Le pied dit deja de quel jeu vient la carte et quand le jeu a ete recharge
5
+ * (Directus) ; cette date-ci dit tout autre chose : quand la donnee affichee a
6
+ * bouge pour la derniere fois (dernier prix applique, derniere autorisation,
7
+ * dernier avis). Les deux ne se confondent pas, cf. R-04 de la page
8
+ * `source-panel.md` du vault.
9
+ *
10
+ * Les listes arrivent telles que les index les rendent : des dates ISO, des
11
+ * `null`, et les marqueurs de vide des sources francaises (« - », « / »,
12
+ * « NA »). Tout ce qui ne se lit pas comme une date est ecarte plutot que de
13
+ * ressortir en « Invalid Date ».
14
+ */
15
+ export function heartLatestDate(values: unknown): string | null {
16
+ const list = Array.isArray(values) ? values : [values];
17
+ let best: string | null = null;
18
+ let bestTime = -Infinity;
19
+
20
+ for (const value of list) {
21
+ if (typeof value !== "string" && typeof value !== "number") continue;
22
+ const raw = String(value).trim();
23
+ if (!raw || raw === "-" || raw === "/" || raw.toUpperCase() === "NA") continue;
24
+ const time = new Date(raw).getTime();
25
+ if (Number.isNaN(time) || time <= bestTime) continue;
26
+ best = raw;
27
+ bestTime = time;
28
+ }
29
+
30
+ return best;
31
+ }