@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,647 @@
1
+ <!--
2
+ Tout ce que le suivi mondial sait d'une presentation etrangere.
3
+
4
+ La carte pays ne montre que le prix en vigueur : c'est ce qu'on lit d'un coup
5
+ d'oeil. Le reste est ici : les autres types de prix publies sur la meme
6
+ presentation, l'historique complet des changements avec leur ecart, et
7
+ l'identite du produit tel que le pays l'enregistre.
8
+
9
+ Meme chrome que le tiroir des presentations francaises : le lecteur passe de
10
+ l'onglet Prix a l'onglet Regard international sans changer d'habitude.
11
+ -->
12
+ <script setup lang="ts">
13
+ import type { HeartIntlPriceRow } from "../../../../../oip-drugs/utils/heartIntlPrice";
14
+
15
+ const props = defineProps<{
16
+ /** La presentation ouverte, ou null quand le tiroir est ferme. */
17
+ row: HeartIntlPriceRow | null;
18
+ /** Nom du pays dans la langue lue ; l'index l'ecrit toujours en anglais. */
19
+ countryLabel?: string;
20
+ }>();
21
+
22
+ const emit = defineEmits<{ (event: "close"): void }>();
23
+
24
+ const { locale } = useI18n();
25
+ const isEn = computed(() => locale.value === "en");
26
+
27
+ /** Les montants gardent leur devise : un prix local n'est pas un prix en euros. */
28
+ function money(value: number | null | undefined, currency = "EUR") {
29
+ if (value === null || value === undefined) return "-";
30
+ // `narrowSymbol` : en francais, GBP s'ecrit sinon « £GB » et USD « $US ».
31
+ return new Intl.NumberFormat(isEn.value ? "en-US" : "fr-FR", {
32
+ style: "currency",
33
+ currency,
34
+ currencyDisplay: "narrowSymbol",
35
+ maximumFractionDigits: 2,
36
+ }).format(Number(value));
37
+ }
38
+
39
+ /** Le prix converti ne s'affiche que hors zone euro. */
40
+ function inEuro(value: number | null | undefined, currency: string) {
41
+ if (currency === "EUR" || value === null || value === undefined) return "";
42
+ return money(value, "EUR");
43
+ }
44
+
45
+ function date(raw: string | number | null | undefined) {
46
+ if (raw === null || raw === undefined || raw === "") return "-";
47
+ const value = new Date(raw);
48
+ if (Number.isNaN(value.getTime())) return String(raw);
49
+ return value.toLocaleDateString(isEn.value ? "en-GB" : "fr-FR", {
50
+ day: "numeric",
51
+ month: "short",
52
+ year: "numeric",
53
+ });
54
+ }
55
+
56
+ function signedPercent(value: number | null) {
57
+ if (value === null) return "";
58
+ const formatted = value.toLocaleString(isEn.value ? "en-US" : "fr-FR", {
59
+ maximumFractionDigits: 1,
60
+ });
61
+ return `${value > 0 ? "+" : ""}${formatted} %`;
62
+ }
63
+
64
+ const currency = computed(() => props.row?.currency ?? "EUR");
65
+
66
+ /**
67
+ * L'historique, ligne par ligne, avec l'ecart au prix precedent.
68
+ *
69
+ * `price_variation` est rempli par la source et vaut 0 sur les lignes de
70
+ * reconduction : l'ecart affiche se recalcule donc a partir du prix precedent
71
+ * du meme type, la seule comparaison qui ait un sens.
72
+ */
73
+ const historyRows = computed(() => {
74
+ const history = props.row?.history ?? [];
75
+ return history.map((entry, index) => {
76
+ const previous = history
77
+ .slice(index + 1)
78
+ .find((older) => older.type === entry.type && typeof older.local === "number");
79
+ const variation = previous ? entry.local - previous.local : null;
80
+ return {
81
+ entry,
82
+ variation,
83
+ variationPct: variation !== null && previous?.local ? (variation / previous.local) * 100 : null,
84
+ };
85
+ });
86
+ });
87
+
88
+ /**
89
+ * Seuls les mouvements sont montres.
90
+ *
91
+ * L'index republie le meme tarif a chaque collecte (jusqu'a 540 lignes pour
92
+ * une seule presentation) : garder les reconductions noyait les quelques
93
+ * dates ou le prix a reellement bouge. Le prix en vigueur, lui, se lit sur
94
+ * l'onglet d'a cote.
95
+ */
96
+ const visibleHistory = computed(() =>
97
+ historyRows.value.filter((item) => item.variation === null || item.variation !== 0),
98
+ );
99
+
100
+ /** Le nombre de lignes posees d'un coup ; le reste arrive a la demande. */
101
+ const PAGE = 40;
102
+ const shown = ref(PAGE);
103
+ const pagedHistory = computed(() => visibleHistory.value.slice(0, shown.value));
104
+
105
+ /** Premiere mise en vigueur connue, tous types confondus. */
106
+ const firstSeen = computed(() => {
107
+ const dates = (props.row?.history ?? [])
108
+ .map((entry) => entry.date)
109
+ .filter(Boolean)
110
+ .sort();
111
+ return dates[0] ?? null;
112
+ });
113
+
114
+ const tabs = computed(() =>
115
+ [
116
+ { key: "prices", label: isEn.value ? "Current prices" : "Prix en vigueur", filled: true },
117
+ {
118
+ key: "history",
119
+ label: isEn.value ? "Price history" : "Historique des prix",
120
+ filled: (props.row?.history?.length ?? 0) > 0,
121
+ },
122
+ { key: "identity", label: isEn.value ? "Identity" : "Identité", filled: true },
123
+ ].filter((tab) => tab.filled),
124
+ );
125
+
126
+ const active = ref(0);
127
+ const activeKey = computed(() => tabs.value[active.value]?.key ?? "prices");
128
+
129
+ // Une autre presentation s'ouvre : on repart de son premier onglet, et la
130
+ // liste d'historique reprend a sa premiere page.
131
+ watch(
132
+ () => props.row,
133
+ () => {
134
+ active.value = 0;
135
+ shown.value = PAGE;
136
+ },
137
+ );
138
+
139
+ function onKeydown(event: KeyboardEvent) {
140
+ if (event.key === "Escape") emit("close");
141
+ }
142
+
143
+ onMounted(() => document.addEventListener("keydown", onKeydown));
144
+ onBeforeUnmount(() => document.removeEventListener("keydown", onKeydown));
145
+ </script>
146
+
147
+ <template>
148
+ <Teleport to="body">
149
+ <Transition name="drawer">
150
+ <div v-if="row" class="ipd oip-drugs" @click.self="emit('close')">
151
+ <aside class="ipd_panel">
152
+ <header class="ipd_head">
153
+ <p class="ipd_type">
154
+ <RmFlag :code="row.iso" :height="14" :title="countryLabel || row.country" />
155
+ {{ countryLabel || row.country }}
156
+ </p>
157
+ <button
158
+ type="button"
159
+ class="ipd_close"
160
+ :aria-label="isEn ? 'Close' : 'Fermer'"
161
+ @click="emit('close')"
162
+ >
163
+ <RmIcon name="X" :size="16" />
164
+ </button>
165
+ </header>
166
+
167
+ <div class="ipd_body">
168
+ <h2 class="ipd_title">{{ row.product }}</h2>
169
+ <p class="ipd_sub">
170
+ <span v-if="row.localId">{{ isEn ? "Code" : "Code local" }} {{ row.localId }}</span>
171
+ <span v-if="row.manufacturer"> · {{ row.manufacturer }}</span>
172
+ <span v-if="row.category"> · {{ row.category }}</span>
173
+ </p>
174
+
175
+ <!-- Le prix de reference, celui que porte la carte du pays. -->
176
+ <div class="ipd_hero">
177
+ <p class="ipd_hero_price">
178
+ {{ money(row.priceLocal, currency) }}
179
+ <span class="ipd_hero_type">{{ row.typeLabel }}</span>
180
+ </p>
181
+ <p v-if="inEuro(row.priceEur, currency)" class="ipd_hero_eur">
182
+ {{ inEuro(row.priceEur, currency) }}
183
+ <span class="ipd_muted">{{ isEn ? "converted" : "converti" }}</span>
184
+ </p>
185
+ <p class="ipd_hero_date">
186
+ <RmIcon name="Calendar" :size="14" />
187
+ {{ isEn ? "In force since" : "En vigueur depuis" }} {{ date(row.date) }}
188
+ </p>
189
+ </div>
190
+
191
+ <div v-if="tabs.length > 1" class="ipd_tabs">
192
+ <button
193
+ v-for="(tab, index) in tabs"
194
+ :key="tab.key"
195
+ type="button"
196
+ class="ipd_tab"
197
+ :class="{ is_active: index === active }"
198
+ @click="active = index"
199
+ >
200
+ {{ tab.label }}
201
+ </button>
202
+ </div>
203
+
204
+ <!-- Tous les types de prix publies sur cette presentation. -->
205
+ <div v-if="activeKey === 'prices'" class="ipd_scroll">
206
+ <table class="ipd_table">
207
+ <thead>
208
+ <tr>
209
+ <th>{{ isEn ? "Price type" : "Type de prix" }}</th>
210
+ <th>{{ isEn ? "Local price" : "Prix local" }}</th>
211
+ <th v-if="currency !== 'EUR'">{{ isEn ? "In euros" : "En euros" }}</th>
212
+ <th>{{ isEn ? "In force since" : "En vigueur depuis" }}</th>
213
+ </tr>
214
+ </thead>
215
+ <tbody>
216
+ <tr v-for="type in row.types" :key="type.type">
217
+ <td>{{ type.label }}</td>
218
+ <td class="ipd_num">{{ money(type.local, currency) }}</td>
219
+ <td v-if="currency !== 'EUR'" class="ipd_num">
220
+ {{ inEuro(type.eur, currency) || "-" }}
221
+ </td>
222
+ <td>{{ date(type.date) }}</td>
223
+ </tr>
224
+ </tbody>
225
+ </table>
226
+ <p class="ipd_note">
227
+ {{
228
+ isEn
229
+ ? "Each country publishes its own price types. The card shows the first available in this order: ex-factory, wholesale, public, reimbursement."
230
+ : "Chaque pays publie ses propres types de prix. La carte montre le premier disponible dans cet ordre : fabricant, grossiste, public, remboursement."
231
+ }}
232
+ </p>
233
+ </div>
234
+
235
+ <!-- L'historique complet, un mouvement par ligne. -->
236
+ <template v-else-if="activeKey === 'history'">
237
+ <p class="ipd_filter">
238
+ {{ visibleHistory.length }}
239
+ {{
240
+ isEn
241
+ ? visibleHistory.length > 1
242
+ ? "price movements"
243
+ : "price movement"
244
+ : "mouvement" + (visibleHistory.length > 1 ? "s" : "") + " de prix"
245
+ }}
246
+ <span class="ipd_muted">
247
+ {{ isEn ? "out of" : "sur" }} {{ row.history.length }}
248
+ {{ isEn ? "records" : "relevés" }}
249
+ </span>
250
+ </p>
251
+
252
+ <div class="ipd_scroll">
253
+ <table class="ipd_table">
254
+ <thead>
255
+ <tr>
256
+ <th>{{ isEn ? "Applies from" : "Applicable au" }}</th>
257
+ <th>{{ isEn ? "Price type" : "Type de prix" }}</th>
258
+ <th>{{ isEn ? "Price" : "Prix" }}</th>
259
+ <th>{{ isEn ? "Change" : "Évolution" }}</th>
260
+ <th v-if="currency !== 'EUR'">{{ isEn ? "In euros" : "En euros" }}</th>
261
+ <th>{{ isEn ? "Collected on" : "Relevé le" }}</th>
262
+ </tr>
263
+ </thead>
264
+ <tbody>
265
+ <tr v-for="(item, index) in pagedHistory" :key="index">
266
+ <td>{{ date(item.entry.date) }}</td>
267
+ <td>{{ item.entry.label }}</td>
268
+ <td class="ipd_num">{{ money(item.entry.local, currency) }}</td>
269
+ <td
270
+ class="ipd_num"
271
+ :class="
272
+ item.variation && item.variation > 0
273
+ ? 'is_up'
274
+ : item.variation && item.variation < 0
275
+ ? 'is_down'
276
+ : ''
277
+ "
278
+ >
279
+ <template v-if="item.variation === null">
280
+ {{ isEn ? "First price" : "Premier prix" }}
281
+ </template>
282
+ <template v-else-if="item.variation === 0">-</template>
283
+ <template v-else>
284
+ {{ item.variation > 0 ? "+" : "" }}{{ money(item.variation, currency) }}
285
+ <span v-if="item.variationPct !== null" class="ipd_muted">
286
+ ({{ signedPercent(item.variationPct) }})
287
+ </span>
288
+ </template>
289
+ </td>
290
+ <td v-if="currency !== 'EUR'" class="ipd_num">
291
+ {{ inEuro(item.entry.eur, currency) || "-" }}
292
+ </td>
293
+ <td>{{ date(item.entry.sourceUpdate) }}</td>
294
+ </tr>
295
+ </tbody>
296
+ </table>
297
+ </div>
298
+
299
+ <button
300
+ v-if="visibleHistory.length > shown"
301
+ type="button"
302
+ class="ipd_more"
303
+ @click="shown += PAGE"
304
+ >
305
+ {{ isEn ? "Load more" : "Voir plus" }}
306
+ ({{ visibleHistory.length - shown }})
307
+ </button>
308
+ </template>
309
+
310
+ <!-- Ce que le pays enregistre du produit. -->
311
+ <dl v-else class="ipd_grid">
312
+ <div>
313
+ <dt>{{ isEn ? "Country" : "Pays" }}</dt>
314
+ <dd>{{ countryLabel || row.country }}</dd>
315
+ </div>
316
+ <div v-if="row.brandLocal">
317
+ <dt>{{ isEn ? "Brand" : "Marque" }}</dt>
318
+ <dd>{{ row.brandLocal }}</dd>
319
+ </div>
320
+ <div v-if="row.molecule">
321
+ <dt>{{ isEn ? "Molecule" : "Molécule" }}</dt>
322
+ <dd>{{ row.molecule }}</dd>
323
+ </div>
324
+ <div v-if="row.atc">
325
+ <dt>ATC</dt>
326
+ <dd>{{ row.atc }}</dd>
327
+ </div>
328
+ <div v-if="row.manufacturer">
329
+ <dt>{{ isEn ? "Manufacturer" : "Fabricant" }}</dt>
330
+ <dd>{{ row.manufacturer }}</dd>
331
+ </div>
332
+ <div v-if="row.localId">
333
+ <dt>{{ isEn ? "Local code" : "Code local" }}</dt>
334
+ <dd>{{ row.localId }}</dd>
335
+ </div>
336
+ <div v-if="row.category">
337
+ <dt>{{ isEn ? "Channel" : "Circuit" }}</dt>
338
+ <dd>{{ row.category }}</dd>
339
+ </div>
340
+ <div>
341
+ <dt>{{ isEn ? "Currency" : "Devise" }}</dt>
342
+ <dd>{{ row.currency }}</dd>
343
+ </div>
344
+ <div v-if="row.marketedOn">
345
+ <dt>{{ isEn ? "Marketed since" : "Commercialisé depuis" }}</dt>
346
+ <dd>{{ date(row.marketedOn) }}</dd>
347
+ </div>
348
+ <div v-if="firstSeen">
349
+ <dt>{{ isEn ? "First price recorded" : "Premier prix relevé" }}</dt>
350
+ <dd>{{ date(firstSeen) }}</dd>
351
+ </div>
352
+ <div v-if="row.lastPriceUpdate">
353
+ <dt>{{ isEn ? "Latest price change" : "Dernier changement de prix" }}</dt>
354
+ <dd>{{ date(row.lastPriceUpdate) }}</dd>
355
+ </div>
356
+ <div v-if="row.lastSourceUpdate">
357
+ <dt>{{ isEn ? "Source collected on" : "Source relevée le" }}</dt>
358
+ <dd>{{ date(row.lastSourceUpdate) }}</dd>
359
+ </div>
360
+ </dl>
361
+ </div>
362
+
363
+ <p v-if="row.sourceLabel" class="ipd_foot">
364
+ <RmIcon name="Info" :size="14" />
365
+ {{ isEn ? "Source" : "Source" }} : {{ row.sourceLabel }}
366
+ </p>
367
+ </aside>
368
+ </div>
369
+ </Transition>
370
+ </Teleport>
371
+ </template>
372
+
373
+ <style scoped>
374
+ .ipd {
375
+ position: fixed;
376
+ inset: var(--rm-shell-inset, 8px);
377
+ border-radius: var(--rm-shell-radius, 12px);
378
+ overflow: hidden;
379
+ z-index: 300;
380
+ display: flex;
381
+ justify-content: flex-end;
382
+ background: rgba(30, 41, 59, 0.35);
383
+ -webkit-backdrop-filter: blur(2px);
384
+ backdrop-filter: blur(2px);
385
+ }
386
+
387
+ .ipd_panel {
388
+ display: flex;
389
+ flex-direction: column;
390
+ width: min(860px, 100%);
391
+ height: 100%;
392
+ background: var(--white);
393
+ box-shadow: var(--overlay-shadow);
394
+ }
395
+
396
+ .ipd_head {
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: space-between;
400
+ gap: 16px;
401
+ padding: 16px 20px;
402
+ border-bottom: 1px solid var(--light);
403
+ }
404
+
405
+ .ipd_type {
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 8px;
409
+ color: var(--primary);
410
+ font-size: 15px;
411
+ font-weight: 700;
412
+ }
413
+
414
+ .ipd_close {
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ width: 32px;
419
+ height: 32px;
420
+ border: 1px solid var(--light);
421
+ border-radius: 8px;
422
+ background: var(--white);
423
+ color: var(--dark);
424
+ cursor: pointer;
425
+ }
426
+
427
+ .ipd_close:hover {
428
+ background: var(--lightest);
429
+ }
430
+
431
+ .ipd_body {
432
+ flex: 1 1 auto;
433
+ overflow-y: auto;
434
+ display: flex;
435
+ flex-direction: column;
436
+ gap: 12px;
437
+ padding: 20px;
438
+ }
439
+
440
+ .ipd_title {
441
+ font-size: 18px;
442
+ font-weight: 800;
443
+ line-height: 1.3;
444
+ }
445
+
446
+ .ipd_sub {
447
+ color: var(--medium);
448
+ font-size: 13px;
449
+ }
450
+
451
+ .ipd_hero {
452
+ display: flex;
453
+ flex-wrap: wrap;
454
+ align-items: baseline;
455
+ gap: 8px 20px;
456
+ padding: 14px 16px;
457
+ border: 1px solid var(--light);
458
+ border-radius: 12px;
459
+ background: var(--lightest, #f7f7fb);
460
+ }
461
+
462
+ .ipd_hero_price {
463
+ font-family: var(--font-serif, inherit);
464
+ font-size: 28px;
465
+ font-weight: 700;
466
+ color: var(--primary);
467
+ }
468
+
469
+ .ipd_hero_type {
470
+ font-family: inherit;
471
+ font-size: 12px;
472
+ font-weight: 600;
473
+ color: var(--medium);
474
+ }
475
+
476
+ .ipd_hero_eur {
477
+ font-size: 14px;
478
+ font-weight: 700;
479
+ color: var(--darkest);
480
+ }
481
+
482
+ .ipd_hero_date {
483
+ display: inline-flex;
484
+ align-items: center;
485
+ gap: 6px;
486
+ margin-left: auto;
487
+ font-size: 12px;
488
+ color: var(--medium);
489
+ }
490
+
491
+ .ipd_tabs {
492
+ display: flex;
493
+ flex-wrap: wrap;
494
+ gap: 4px;
495
+ border-bottom: 1px solid var(--light);
496
+ }
497
+
498
+ .ipd_tab {
499
+ padding: 8px 12px;
500
+ border: none;
501
+ border-bottom: 2px solid transparent;
502
+ background: none;
503
+ color: var(--medium);
504
+ font-family: inherit;
505
+ font-size: 14px;
506
+ font-weight: 600;
507
+ cursor: pointer;
508
+ }
509
+
510
+ .ipd_tab.is_active {
511
+ border-bottom-color: var(--primary);
512
+ background: color-mix(in srgb, var(--primary) 6%, transparent);
513
+ color: var(--primary);
514
+ }
515
+
516
+ .ipd_filter {
517
+ display: flex;
518
+ align-items: center;
519
+ gap: 10px;
520
+ font-size: 13px;
521
+ color: var(--dark);
522
+ }
523
+
524
+ .ipd_scroll {
525
+ overflow-x: auto;
526
+ }
527
+
528
+ .ipd_table {
529
+ width: 100%;
530
+ border-collapse: collapse;
531
+ font-size: 13px;
532
+ }
533
+
534
+ .ipd_table th {
535
+ padding: 8px 10px;
536
+ border-bottom: 1px solid var(--light);
537
+ color: var(--medium);
538
+ font-size: 11px;
539
+ font-weight: 700;
540
+ letter-spacing: 0.04em;
541
+ text-align: left;
542
+ text-transform: uppercase;
543
+ white-space: nowrap;
544
+ }
545
+
546
+ .ipd_table td {
547
+ padding: 8px 10px;
548
+ border-bottom: 1px solid var(--lightest);
549
+ color: var(--dark);
550
+ vertical-align: top;
551
+ white-space: nowrap;
552
+ }
553
+
554
+ .ipd_num {
555
+ font-variant-numeric: tabular-nums;
556
+ }
557
+
558
+ .ipd_table .is_up {
559
+ color: var(--system-alert-hover);
560
+ font-weight: 700;
561
+ }
562
+
563
+ .ipd_table .is_down {
564
+ color: var(--system-success-hover);
565
+ font-weight: 700;
566
+ }
567
+
568
+ .ipd_muted {
569
+ color: var(--medium);
570
+ font-weight: 400;
571
+ }
572
+
573
+ .ipd_note {
574
+ margin-top: 10px;
575
+ font-size: 11px;
576
+ color: var(--medium);
577
+ }
578
+
579
+ .ipd_more {
580
+ align-self: flex-start;
581
+ border: 1px solid var(--light);
582
+ border-radius: 8px;
583
+ padding: 6px 12px;
584
+ background: var(--white);
585
+ color: var(--primary);
586
+ font-family: inherit;
587
+ font-size: 12px;
588
+ font-weight: 600;
589
+ cursor: pointer;
590
+ }
591
+
592
+ .ipd_more:hover {
593
+ background: var(--lightest);
594
+ }
595
+
596
+ .ipd_grid {
597
+ display: grid;
598
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
599
+ gap: 14px;
600
+ margin: 0;
601
+ }
602
+
603
+ .ipd_grid dt {
604
+ color: var(--medium);
605
+ font-size: 11px;
606
+ font-weight: 700;
607
+ letter-spacing: 0.04em;
608
+ text-transform: uppercase;
609
+ }
610
+
611
+ .ipd_grid dd {
612
+ margin: 2px 0 0;
613
+ color: var(--dark);
614
+ font-size: 14px;
615
+ }
616
+
617
+ .ipd_foot {
618
+ display: flex;
619
+ align-items: center;
620
+ gap: 6px;
621
+ padding: 14px 20px;
622
+ border-top: 1px solid var(--light);
623
+ background: var(--demi-fond);
624
+ color: var(--medium);
625
+ font-size: 13px;
626
+ }
627
+
628
+ .drawer-enter-active,
629
+ .drawer-leave-active {
630
+ transition: opacity 0.2s ease;
631
+ }
632
+
633
+ .drawer-enter-active .ipd_panel,
634
+ .drawer-leave-active .ipd_panel {
635
+ transition: transform 0.2s ease;
636
+ }
637
+
638
+ .drawer-enter-from,
639
+ .drawer-leave-to {
640
+ opacity: 0;
641
+ }
642
+
643
+ .drawer-enter-from .ipd_panel,
644
+ .drawer-leave-to .ipd_panel {
645
+ transform: translateX(24px);
646
+ }
647
+ </style>