@tekkare/romulus 0.1.251 → 0.1.253

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 (267) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/utils/projects.d.ts +8 -0
  3. package/dist/runtime/utils/projects.js +8 -5
  4. package/layer/app/app.config.ts +6 -0
  5. package/layer/app/components/OipPubmedSummary.vue +223 -0
  6. package/layer/app/components/OipResourceDrawer.vue +331 -0
  7. package/layer/app/components/OipResourceDrawerLink.vue +40 -0
  8. package/layer/app/components/OipResourceField.vue +41 -0
  9. package/layer/app/components/OipResourceGenericCard.vue +54 -0
  10. package/layer/app/components/OipResourceSource.vue +50 -0
  11. package/layer/app/components/OipResourceTags.vue +65 -0
  12. package/layer/app/components/OipResourceView.vue +107 -0
  13. package/layer/app/components/oip-drugs/fiches/AssociationFiche.vue +775 -0
  14. package/layer/app/components/oip-drugs/fiches/BrandFiche.vue +1111 -0
  15. package/layer/app/components/oip-drugs/fiches/IndicationFiche.vue +182 -0
  16. package/layer/app/components/oip-drugs/fiches/JoFiche.vue +101 -0
  17. package/layer/app/components/oip-drugs/fiches/PndsFiche.vue +279 -0
  18. package/layer/app/components/oip-drugs/heart/JoCard.vue +314 -0
  19. package/layer/app/components/oip-drugs/heart/TextHighlight.vue +326 -0
  20. package/layer/app/components/oip-drugs/heart/ansm/BrandCard.vue +187 -0
  21. package/layer/app/components/oip-drugs/heart/ansm/CisDetail.vue +363 -0
  22. package/layer/app/components/oip-drugs/heart/ansm/CisTable.vue +167 -0
  23. package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyCard.vue +373 -0
  24. package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyFiche.vue +372 -0
  25. package/layer/app/components/oip-drugs/heart/associations/TranscriptionCard.vue +55 -0
  26. package/layer/app/components/oip-drugs/heart/elastic/SourceContainerElastic.vue +103 -0
  27. package/layer/app/components/oip-drugs/heart/france/BrandGroupCard.vue +450 -0
  28. package/layer/app/components/oip-drugs/heart/mitm/Card.vue +199 -0
  29. package/layer/app/components/oip-drugs/heart/mitm/Source.vue +23 -0
  30. package/layer/app/components/oip-drugs/heart/mitm/details/Overview.vue +381 -0
  31. package/layer/app/components/oip-drugs/heart/put/Card.vue +212 -0
  32. package/layer/app/components/oip-drugs/heart/results/AccesCard.vue +696 -0
  33. package/layer/app/components/oip-drugs/heart/results/AlertCard.vue +342 -0
  34. package/layer/app/components/oip-drugs/heart/results/AsmrCursor.vue +229 -0
  35. package/layer/app/components/oip-drugs/heart/results/EvalCard.vue +370 -0
  36. package/layer/app/components/oip-drugs/heart/results/IndicCard.vue +743 -0
  37. package/layer/app/components/oip-drugs/heart/results/MatchExplain.vue +243 -0
  38. package/layer/app/components/oip-drugs/heart/results/PrefRows.vue +105 -0
  39. package/layer/app/components/oip-drugs/heart/results/details/HasHistory.vue +487 -0
  40. package/layer/app/components/oip-drugs/heart/results/details/InternationalCountryCard.vue +461 -0
  41. package/layer/app/components/oip-drugs/heart/results/details/InternationalOverview.vue +200 -0
  42. package/layer/app/components/oip-drugs/heart/results/details/InternationalPriceDrawer.vue +647 -0
  43. package/layer/app/components/oip-drugs/heart/results/details/Monograph.vue +128 -0
  44. package/layer/app/components/oip-drugs/heart/results/details/OpinionDrawer.vue +535 -0
  45. package/layer/app/components/oip-drugs/heart/results/details/PresentationDrawer.vue +738 -0
  46. package/layer/app/components/oip-drugs/heart/results/details/Price.vue +227 -0
  47. package/layer/app/components/oip-drugs/heart/results/details/PriceOverview.vue +1261 -0
  48. package/layer/app/components/oip-drugs/heart/results/details/RecordDrawer.vue +494 -0
  49. package/layer/app/components/oip-drugs/heart/results/details/RecordList.vue +426 -0
  50. package/layer/app/components/oip-drugs/heart/results/les/Card.vue +340 -0
  51. package/layer/app/components/oip-drugs/heart/results/les/Drawer.vue +67 -0
  52. package/layer/app/components/oip-drugs/heart/results/les/IndicationDetail.vue +149 -0
  53. package/layer/app/components/oip-drugs/heart/results/les/Source.vue +20 -0
  54. package/layer/app/components/oip-drugs/heart/results/les/SourceTag.vue +39 -0
  55. package/layer/app/components/oip-drugs/heart/searchPage/DynamicSource.vue +29 -0
  56. package/layer/app/components/oip-drugs/medicam/market/Chart.vue +121 -0
  57. package/layer/app/components/oip-drugs/outpatient/ProductEvolution.vue +236 -0
  58. package/layer/app/components/oip-drugs/resources/Ansm.vue +75 -0
  59. package/layer/app/components/oip-drugs/resources/Association.vue +30 -0
  60. package/layer/app/components/oip-drugs/resources/Brand.vue +65 -0
  61. package/layer/app/components/oip-drugs/resources/Contribution.vue +216 -0
  62. package/layer/app/components/oip-drugs/resources/Hospital.vue +151 -0
  63. package/layer/app/components/oip-drugs/resources/Indication.vue +54 -0
  64. package/layer/app/components/oip-drugs/resources/Jo.vue +57 -0
  65. package/layer/app/components/oip-drugs/resources/Medicam.vue +140 -0
  66. package/layer/app/components/oip-drugs/resources/Mitm.vue +84 -0
  67. package/layer/app/components/oip-drugs/resources/NationalAssembly.vue +93 -0
  68. package/layer/app/components/oip-drugs/resources/Outpatient.vue +97 -0
  69. package/layer/app/components/oip-drugs/resources/Pnds.vue +24 -0
  70. package/layer/app/components/oip-drugs/resources/Price.vue +106 -0
  71. package/layer/app/components/oip-drugs/resources/Put.vue +143 -0
  72. package/layer/app/components/oip-science/appel-a-projet/Cards.vue +162 -0
  73. package/layer/app/components/oip-science/clinical-trials/Cards.vue +382 -0
  74. package/layer/app/components/oip-science/clinical-trials/CardsGlobal.vue +273 -0
  75. package/layer/app/components/oip-science/clinical-trials/LinkedList.vue +366 -0
  76. package/layer/app/components/oip-science/clinical-trials/PairBadge.vue +25 -0
  77. package/layer/app/components/oip-science/clinical-trials/PairBody.vue +182 -0
  78. package/layer/app/components/oip-science/clinical-trials/SitesDrawer.vue +95 -0
  79. package/layer/app/components/oip-science/common/Form.vue +73 -0
  80. package/layer/app/components/oip-science/common/MatchAvgTag.vue +283 -0
  81. package/layer/app/components/oip-science/common/NoData.vue +16 -0
  82. package/layer/app/components/oip-science/common/ScoreBadge.vue +35 -0
  83. package/layer/app/components/oip-science/common/Source.vue +31 -0
  84. package/layer/app/components/oip-science/common/TableTags.vue +279 -0
  85. package/layer/app/components/oip-science/common/Timeline.vue +104 -0
  86. package/layer/app/components/oip-science/common/ToolTips.vue +89 -0
  87. package/layer/app/components/oip-science/cross/publicationPubmed.vue +484 -0
  88. package/layer/app/components/oip-science/crossData/CrossDataLocations.vue +71 -0
  89. package/layer/app/components/oip-science/ctgov/Investigators.vue +148 -0
  90. package/layer/app/components/oip-science/ctgov/Locations.vue +102 -0
  91. package/layer/app/components/oip-science/ctgov/Publications.vue +52 -0
  92. package/layer/app/components/oip-science/ctgov/Sponsors.vue +66 -0
  93. package/layer/app/components/oip-science/ctgov/StudyDescription.vue +1136 -0
  94. package/layer/app/components/oip-science/ctgov-centers/ClinicalTrials.vue +40 -0
  95. package/layer/app/components/oip-science/ctgov-centers/Publications.vue +50 -0
  96. package/layer/app/components/oip-science/ctgov-investigators/ClinicalTrials.vue +43 -0
  97. package/layer/app/components/oip-science/ctgov-investigators/Informations.vue +171 -0
  98. package/layer/app/components/oip-science/ctgov-investigators/Publications.vue +44 -0
  99. package/layer/app/components/oip-science/elastic/DateRangeFilter.vue +209 -0
  100. package/layer/app/components/oip-science/elastic/Filter.vue +719 -0
  101. package/layer/app/components/oip-science/elastic/MatchExplain.vue +152 -0
  102. package/layer/app/components/oip-science/establishments/Cards.vue +183 -0
  103. package/layer/app/components/oip-science/establishments/ClinicalTrials.vue +62 -0
  104. package/layer/app/components/oip-science/establishments/Hcps.vue +227 -0
  105. package/layer/app/components/oip-science/establishments/Informations.vue +263 -0
  106. package/layer/app/components/oip-science/establishments/LinkedList.vue +153 -0
  107. package/layer/app/components/oip-science/establishments/MatchedLabels.vue +194 -0
  108. package/layer/app/components/oip-science/establishments/PublicFinance.vue +62 -0
  109. package/layer/app/components/oip-science/euct/Investigators.vue +125 -0
  110. package/layer/app/components/oip-science/euct/Locations.vue +102 -0
  111. package/layer/app/components/oip-science/euct/Products.vue +101 -0
  112. package/layer/app/components/oip-science/euct/Publications.vue +89 -0
  113. package/layer/app/components/oip-science/euct/Sponsors.vue +86 -0
  114. package/layer/app/components/oip-science/euct/StudyDescription.vue +1075 -0
  115. package/layer/app/components/oip-science/euct-centers/ClinicalTrials.vue +40 -0
  116. package/layer/app/components/oip-science/euct-centers/Publications.vue +55 -0
  117. package/layer/app/components/oip-science/euct-investigators/ClinicalTrials.vue +41 -0
  118. package/layer/app/components/oip-science/euct-investigators/Informations.vue +171 -0
  119. package/layer/app/components/oip-science/euct-investigators/Publications.vue +51 -0
  120. package/layer/app/components/oip-science/hcps/AppelsAProjet.vue +62 -0
  121. package/layer/app/components/oip-science/hcps/Cards.vue +237 -0
  122. package/layer/app/components/oip-science/hcps/ClinicalTrials.vue +78 -0
  123. package/layer/app/components/oip-science/hcps/Informations.vue +296 -0
  124. package/layer/app/components/oip-science/hcps/LinkedList.vue +153 -0
  125. package/layer/app/components/oip-science/hcps/MatchedLabels.vue +207 -0
  126. package/layer/app/components/oip-science/hcps/Pubmed.vue +128 -0
  127. package/layer/app/components/oip-science/local/SliderTabs.vue +99 -0
  128. package/layer/app/components/oip-science/local/SliderTabsFull.vue +135 -0
  129. package/layer/app/components/oip-science/pubmed/Authors.vue +128 -0
  130. package/layer/app/components/oip-science/pubmed/Cards.vue +417 -0
  131. package/layer/app/components/oip-science/pubmed/ClinicalTrials.vue +71 -0
  132. package/layer/app/components/oip-science/resources/Phrc.vue +39 -0
  133. package/layer/app/composables/useOipChapterScroll.ts +43 -0
  134. package/layer/app/composables/useOipResourceHooks.ts +27 -0
  135. package/layer/app/composables/useOipResourceNav.ts +66 -0
  136. package/layer/app/composables/useOipResourceViews.ts +117 -0
  137. package/layer/app/composables/useOipResources.ts +83 -0
  138. package/layer/app/oip-drugs/PORTING.md +120 -0
  139. package/layer/app/oip-drugs/composables/useAssociationDetail.ts +220 -0
  140. package/layer/app/oip-drugs/composables/useAssociationExport.ts +63 -0
  141. package/layer/app/oip-drugs/composables/useAssociationLookup.ts +105 -0
  142. package/layer/app/oip-drugs/composables/useCardPrefOptions.ts +201 -0
  143. package/layer/app/oip-drugs/composables/useCurrency.ts +121 -0
  144. package/layer/app/oip-drugs/composables/useDirectusSources.ts +224 -0
  145. package/layer/app/oip-drugs/composables/useHeartBrandMatches.ts +112 -0
  146. package/layer/app/oip-drugs/composables/useHeartDetailSearches.ts +479 -0
  147. package/layer/app/oip-drugs/composables/useHeartIntlApprovals.ts +661 -0
  148. package/layer/app/oip-drugs/composables/useHeartLesIndication.ts +168 -0
  149. package/layer/app/oip-drugs/composables/useHeartMatchExplain.ts +231 -0
  150. package/layer/app/oip-drugs/composables/useHeartReports.ts +239 -0
  151. package/layer/app/oip-drugs/composables/useHeartSearchFields.ts +233 -0
  152. package/layer/app/oip-drugs/composables/useMergeByKey.ts +23 -0
  153. package/layer/app/oip-drugs/composables/useOipDrugsElastic.ts +621 -0
  154. package/layer/app/oip-drugs/composables/usePageEmpty.ts +58 -0
  155. package/layer/app/oip-drugs/composables/v-useSalesSource.ts +64 -0
  156. package/layer/app/oip-drugs/drugs.css +500 -0
  157. package/layer/app/oip-drugs/esIndices.ts +322 -0
  158. package/layer/app/oip-drugs/kinds.ts +264 -0
  159. package/layer/app/oip-drugs/locales/en.json +1571 -0
  160. package/layer/app/oip-drugs/locales/fr.json +1571 -0
  161. package/layer/app/oip-drugs/monographs/blincyto-has.vue +289 -0
  162. package/layer/app/oip-drugs/monographs/darzalex-has.vue +276 -0
  163. package/layer/app/oip-drugs/monographs/dupixent-has.vue +216 -0
  164. package/layer/app/oip-drugs/monographs/enhertu-has.vue +280 -0
  165. package/layer/app/oip-drugs/monographs/has-sein-2023.vue +1649 -0
  166. package/layer/app/oip-drugs/monographs/ibrance-has.vue +277 -0
  167. package/layer/app/oip-drugs/monographs/imfinzi-has.vue +293 -0
  168. package/layer/app/oip-drugs/monographs/keytruda-has.vue +334 -0
  169. package/layer/app/oip-drugs/monographs/kisqali-has.vue +201 -0
  170. package/layer/app/oip-drugs/monographs/opdivo-has.vue +231 -0
  171. package/layer/app/oip-drugs/monographs/ozempic-has.vue +186 -0
  172. package/layer/app/oip-drugs/monographs/tecentriq-has.vue +312 -0
  173. package/layer/app/oip-drugs/monographs/yescarta-has.vue +272 -0
  174. package/layer/app/oip-drugs/types/elastic.ts +113 -0
  175. package/layer/app/oip-drugs/types/es/agencies.ts +152 -0
  176. package/layer/app/oip-drugs/types/es/directory.ts +54 -0
  177. package/layer/app/oip-drugs/types/es/docByIndex.ts +151 -0
  178. package/layer/app/oip-drugs/types/es/openmedic.ts +67 -0
  179. package/layer/app/oip-drugs/types/es/outpatient.ts +71 -0
  180. package/layer/app/oip-drugs/types/es/portal.ts +130 -0
  181. package/layer/app/oip-drugs/types/es/retail.ts +107 -0
  182. package/layer/app/oip-drugs/types/es/statistics.ts +29 -0
  183. package/layer/app/oip-drugs/types/heart/CurrencyRate.ts +16 -0
  184. package/layer/app/oip-drugs/types/heart/ansm.ts +106 -0
  185. package/layer/app/oip-drugs/types/heart/tag.ts +29 -0
  186. package/layer/app/oip-drugs/utils/associationFields.ts +118 -0
  187. package/layer/app/oip-drugs/utils/contributions.ts +36 -0
  188. package/layer/app/oip-drugs/utils/countryMapping.ts +387 -0
  189. package/layer/app/oip-drugs/utils/esHighlight.ts +21 -0
  190. package/layer/app/oip-drugs/utils/exportSheets.ts +57 -0
  191. package/layer/app/oip-drugs/utils/heartIntlPrice.ts +228 -0
  192. package/layer/app/oip-drugs/utils/heartStyleAndFormat.ts +262 -0
  193. package/layer/app/oip-drugs/utils/heartStyleAnsm.ts +71 -0
  194. package/layer/app/oip-drugs/utils/latestDate.ts +31 -0
  195. package/layer/app/oip-drugs/utils/p-heartCardTags.ts +53 -0
  196. package/layer/app/oip-drugs/utils/priceExport.ts +269 -0
  197. package/layer/app/oip-drugs/utils/q-hasRatings.ts +43 -0
  198. package/layer/app/oip-drugs/utils/s-openCard.ts +20 -0
  199. package/layer/app/oip-drugs/utils/sourceUuids.ts +155 -0
  200. package/layer/app/oip-drugs/utils/styleAndFormat.ts +40 -0
  201. package/layer/app/oip-drugs/utils/v-salesSeries.ts +156 -0
  202. package/layer/app/oip-drugs/utils/v-scopeTags.ts +23 -0
  203. package/layer/app/oip-science/PORTING.md +93 -0
  204. package/layer/app/oip-science/assets/smartSelection.json +428 -0
  205. package/layer/app/oip-science/composables/useAuthorMatching.ts +342 -0
  206. package/layer/app/oip-science/composables/useCTCenter.ts +107 -0
  207. package/layer/app/oip-science/composables/useCTInvestigators.ts +107 -0
  208. package/layer/app/oip-science/composables/useEstablishmentCrossData.ts +139 -0
  209. package/layer/app/oip-science/composables/useEstablishmentMatching.ts +176 -0
  210. package/layer/app/oip-science/composables/useEuct.ts +418 -0
  211. package/layer/app/oip-science/composables/useEuctCenter.ts +107 -0
  212. package/layer/app/oip-science/composables/useEuctInvestigators.ts +107 -0
  213. package/layer/app/oip-science/composables/useGlobalClinicalTrials.ts +624 -0
  214. package/layer/app/oip-science/composables/useLinkedTrials.ts +83 -0
  215. package/layer/app/oip-science/composables/useOipElastic.ts +658 -0
  216. package/layer/app/oip-science/composables/useOipSearchExplain.ts +166 -0
  217. package/layer/app/oip-science/composables/usePubmed.ts +487 -0
  218. package/layer/app/oip-science/composables/useSearchFields.ts +153 -0
  219. package/layer/app/oip-science/indexConfig.json +794 -0
  220. package/layer/app/oip-science/locales/en.json +483 -0
  221. package/layer/app/oip-science/locales/fr.json +483 -0
  222. package/layer/app/oip-science/science.css +1182 -0
  223. package/layer/app/oip-science/utils/a-openCard.ts +23 -0
  224. package/layer/app/oip-science/utils/b-openCard.ts +35 -0
  225. package/layer/app/oip-science/utils/c-filterContext.ts +81 -0
  226. package/layer/app/oip-science/utils/f-types.ts +241 -0
  227. package/layer/app/oip-science/utils/frDiseaseTerms.ts +269 -0
  228. package/layer/app/oip-science/utils/matchMethod.ts +53 -0
  229. package/layer/app/oip-science/utils/sliderTabs.ts +13 -0
  230. package/layer/app/oip-science/utils/sources.ts +15 -0
  231. package/layer/app/oip-science/utils/styleAndFormat.ts +608 -0
  232. package/layer/app/oip-science/utils/toPresets.ts +7 -0
  233. package/layer/app/oip-science/utils/valueTable.ts +126 -0
  234. package/layer/app/plugins/oip-science-i18n.ts +17 -0
  235. package/layer/app/utils/oipFicheRoutes.ts +118 -0
  236. package/layer/app/utils/oipResources.ts +440 -0
  237. package/layer/nuxt.config.ts +22 -2
  238. package/layer/public/reports/blincyto-has.html +292 -0
  239. package/layer/public/reports/blincyto-has.pdf +0 -0
  240. package/layer/public/reports/darzalex-has.html +279 -0
  241. package/layer/public/reports/darzalex-has.pdf +0 -0
  242. package/layer/public/reports/dupixent-has.html +217 -0
  243. package/layer/public/reports/dupixent-has.pdf +0 -0
  244. package/layer/public/reports/enhertu-has.html +283 -0
  245. package/layer/public/reports/enhertu-has.pdf +0 -0
  246. package/layer/public/reports/has-sein-2023.html +1639 -0
  247. package/layer/public/reports/ibrance-has.html +280 -0
  248. package/layer/public/reports/ibrance-has.pdf +0 -0
  249. package/layer/public/reports/imfinzi-has.html +297 -0
  250. package/layer/public/reports/imfinzi-has.pdf +0 -0
  251. package/layer/public/reports/keytruda-has.html +337 -0
  252. package/layer/public/reports/keytruda-has.pdf +0 -0
  253. package/layer/public/reports/kisqali-has.html +202 -0
  254. package/layer/public/reports/kisqali-has.pdf +0 -0
  255. package/layer/public/reports/opdivo-has.html +232 -0
  256. package/layer/public/reports/opdivo-has.pdf +0 -0
  257. package/layer/public/reports/ozempic-has.html +188 -0
  258. package/layer/public/reports/ozempic-has.pdf +0 -0
  259. package/layer/public/reports/tecentriq-has.html +315 -0
  260. package/layer/public/reports/tecentriq-has.pdf +0 -0
  261. package/layer/public/reports/yescarta-has.html +275 -0
  262. package/layer/public/reports/yescarta-has.pdf +0 -0
  263. package/layer/server/api/oip/es.post.ts +99 -0
  264. package/layer/server/api/oip/pdf.get.ts +70 -0
  265. package/layer/server/api/oip/reports.get.ts +98 -0
  266. package/layer/server/api/oip/resource-types.get.ts +22 -0
  267. package/package.json +1 -1
@@ -0,0 +1,1111 @@
1
+ <!--
2
+ Fiche marque HAS (portage de pages/hubr/heart/details/[id].vue de
3
+ fr_drugs_app).
4
+
5
+ Ce que la page lisait dans sa route arrive en props (`brand`, `tab`, `cis`,
6
+ `code`, `indication`) ; l'onglet choisi part par `update:tab` au lieu
7
+ d'ecrire `?tab=`. L'export et le bouton Projet restent dans la page de
8
+ fr_drugs_app, qui les pose dans le slot `actions` (le slot recoit les donnees
9
+ de la fiche) ; dans le tiroir d'un projet, rien n'y est passe.
10
+ -->
11
+ <template>
12
+ <div class="oip-drugs arg_page_layout">
13
+ <RmPageHeader
14
+ :have-return="haveReturn"
15
+ :tabs="headerTabs"
16
+ :tabs-label="locale === 'en' ? 'Record sections' : 'Sections de la fiche'"
17
+ @tab="selectTab"
18
+ >
19
+ <template #title>
20
+ <span class="oip-drugs-brand__title">{{ title }}</span>
21
+ <slot name="title-link" />
22
+ </template>
23
+ <template v-if="$slots.actions" #actions>
24
+ <slot
25
+ name="actions"
26
+ :product="resultsQuery"
27
+ :brand-id="brandId"
28
+ :brand="title"
29
+ :tab="currentTabKey"
30
+ :tab-label="currentTabLabel"
31
+ :tabs="availableTabs"
32
+ :prices="resultsQueryPrice"
33
+ :intl-prices="resultsQueryIntlPrice"
34
+ :intl-records="intlRecords"
35
+ :ansm="resultsQueryAnsm"
36
+ :mitm="resultsQueryMitm"
37
+ :put="resultsQueryPut"
38
+ :les="resultsQueryLes"
39
+ />
40
+ </template>
41
+ </RmPageHeader>
42
+
43
+ <RmLoadingState v-if="showLoading" class="oip-drugs-brand__state" :lines="6" />
44
+ <RmEmptyState
45
+ v-else-if="showEmpty"
46
+ class="oip-drugs-brand__state"
47
+ icon="Pill"
48
+ :title="locale === 'en' ? 'No record for this brand' : 'Aucune fiche pour cette marque'"
49
+ :text="brandMatch"
50
+ />
51
+
52
+ <div
53
+ v-else-if="chosenTab === monographTabIndex && monographId"
54
+ class="arg_page_layout mono-bleed"
55
+ >
56
+ <OipDrugsHeartResultsDetailsMonograph :id="monographId!" :pdf="monographPdf" />
57
+ </div>
58
+ <div v-else-if="chosenTab === overviewTabIndex" class="arg_page_layout">
59
+ <!-- Historique complet en cartes, groupees par annee ; le detail d'un
60
+ avis s'ouvre dans un tiroir plutot que sous la liste. -->
61
+ <OipDrugsHeartResultsDetailsHasHistory
62
+ v-if="resultsQuery !== null"
63
+ :product="resultsQuery"
64
+ :matched-codes="brandMatches.matchedCodes.value"
65
+ :search-terms="searchTerms"
66
+ @select="drawerOpinion = $event"
67
+ />
68
+
69
+ <!-- Le tiroir a son propre etat : il ne s'ouvre que sur un avis choisi
70
+ dans la liste, jamais des l'arrivee sur l'onglet. -->
71
+ <OipDrugsHeartResultsDetailsOpinionDrawer
72
+ :opinion="drawerOpinion"
73
+ :checked-at="hasCheckedAt"
74
+ :search-terms="searchTerms"
75
+ @close="drawerOpinion = null"
76
+ />
77
+ </div>
78
+ <div v-else-if="chosenTab === alertTabIndex && alertTabIndex > 0" class="arg_page_layout">
79
+ <OipDrugsHeartResultsDetailsRecordList
80
+ :title="locale === 'en' ? 'Safety alerts' : 'Alertes de sécurité'"
81
+ :subtitle="locale === 'en' ? 'alerts recorded for the brand' : 'alertes recensées pour la marque'"
82
+ :items="alertRecords"
83
+ :empty-label="locale === 'en' ? 'No safety alerts for this product.' : 'Aucune alerte de sécurité pour ce produit.'"
84
+ @select="drawerRecord = $event"
85
+ />
86
+ </div>
87
+ <div v-else-if="chosenTab === priceTabIndex && priceTabIndex > 0" class="arg_page_layout">
88
+ <OipDrugsHeartResultsDetailsPriceOverview
89
+ :product="resultsQueryPrice"
90
+ :brand="title"
91
+ />
92
+ </div>
93
+ <div
94
+ v-else-if="chosenTab === internationalTabIndex && internationalTabIndex > 0"
95
+ class="arg_page_layout"
96
+ >
97
+ <OipDrugsHeartResultsDetailsInternationalOverview
98
+ :docs="resultsQueryIntlPrice"
99
+ :records="intlRecords"
100
+ :loading="intlLoading"
101
+ :brand="title"
102
+ />
103
+ </div>
104
+ <div v-else-if="chosenTab === ansmTabIndex && ansmTabIndex > 0" class="arg_page_layout">
105
+ <div v-if="resultsQueryAnsm.length > 1" class="cis-switch">
106
+ <span class="oip_filter_label cis-switch-label">
107
+ {{ locale === 'en' ? 'Specialty (CIS):' : 'Spécialité (CIS) :' }}
108
+ </span>
109
+ <button
110
+ v-for="(h, i) in resultsQueryAnsm"
111
+ :key="h._id"
112
+ type="button"
113
+ class="cis-chip"
114
+ :class="{ active: i === selectedAnsmIndex }"
115
+ @click="selectedAnsmIndex = i"
116
+ >
117
+ {{ heartStripBrandPrefix(h._source.cis_label, brandMatch) }}
118
+ </button>
119
+ </div>
120
+ <OipDrugsHeartAnsmCisDetail
121
+ v-if="selectedAnsmCis"
122
+ :cis="selectedAnsmCis"
123
+ :sku-count="resultsQueryAnsm.length"
124
+ :avis-count="overviewHistory.length"
125
+ />
126
+ </div>
127
+ <div v-else-if="chosenTab === mitmTabIndex && mitmTabIndex > 0" class="arg_page_layout">
128
+ <div v-if="resultsQueryMitm.length > 1" class="cis-switch">
129
+ <span class="oip_filter_label cis-switch-label">
130
+ {{ locale === 'en' ? 'Specialty (CIS):' : 'Spécialité (CIS) :' }}
131
+ </span>
132
+ <button
133
+ v-for="(m, i) in resultsQueryMitm"
134
+ :key="m._id"
135
+ type="button"
136
+ class="cis-chip"
137
+ :class="{ active: i === selectedMitmIndex }"
138
+ @click="selectedMitmIndex = i"
139
+ >
140
+ {{ heartStripBrandPrefix(m._source.cis_label, brandMatch) }}
141
+ </button>
142
+ </div>
143
+ <OipDrugsHeartMitmDetailsOverview
144
+ v-if="selectedMitmCis"
145
+ :cis="selectedMitmCis"
146
+ @select="drawerRecord = $event"
147
+ />
148
+ </div>
149
+ <div v-else-if="chosenTab === putTabIndex && putTabIndex > 0" class="arg_page_layout">
150
+ <OipDrugsHeartResultsDetailsRecordList
151
+ :title="locale === 'en' ? 'Temporary use protocols' : 'Protocoles d’utilisation thérapeutique'"
152
+ :subtitle="locale === 'en' ? 'protocols recorded for the brand' : 'protocoles recensés pour la marque'"
153
+ :items="putRecords"
154
+ :empty-label="locale === 'en' ? 'No protocol for this product.' : 'Aucun protocole pour ce produit.'"
155
+ @select="drawerRecord = $event"
156
+ />
157
+ </div>
158
+ <div v-else-if="chosenTab === lesTabIndex && lesTabIndex > 0" class="arg_page_layout">
159
+ <OipDrugsHeartResultsDetailsRecordList
160
+ :title="locale === 'en' ? 'Liste en sus indications' : 'Indications en liste en sus'"
161
+ :subtitle="locale === 'en' ? 'indications recorded for the brand' : 'indications recensées pour la marque'"
162
+ :items="lesRecords"
163
+ :empty-label="locale === 'en' ? 'No liste en sus indication for this product.' : 'Aucune indication en liste en sus pour ce produit.'"
164
+ @select="drawerLes = $event"
165
+ />
166
+ </div>
167
+ <div
168
+ v-else-if="chosenTab === contributionsTabIndex && contributionsTabIndex > 0"
169
+ class="arg_page_layout"
170
+ >
171
+ <OipDrugsHeartResultsDetailsRecordList
172
+ :title="locale === 'en' ? 'Patient contributions' : 'Contributions patients'"
173
+ :subtitle="locale === 'en' ? 'contributions recorded for the brand' : 'contributions recensées pour la marque'"
174
+ :items="contributionRecords"
175
+ :empty-label="locale === 'en' ? 'No patient contribution.' : 'Aucune contribution patient.'"
176
+ @select="drawerRecord = $event"
177
+ />
178
+ </div>
179
+
180
+ <!-- Tiroir commun aux onglets Alertes, PUT et Contributions : un seul est
181
+ visible a la fois, il suit l'enregistrement choisi. -->
182
+ <OipDrugsHeartResultsDetailsRecordDrawer
183
+ :record="drawerRecord"
184
+ :source-label="drawerRecord?.sourceLabel"
185
+ :checked-at="hasCheckedAt"
186
+ @close="drawerRecord = null"
187
+ />
188
+
189
+ <!-- Tiroir de l'onglet LES : le detail de l'indication, tel que le montre
190
+ la page indication/[id]. -->
191
+ <OipDrugsHeartResultsLesDrawer
192
+ :code="drawerLes?.code ?? null"
193
+ :title="drawerLes?.title"
194
+ :date="drawerLes?.date"
195
+ @close="drawerLes = null"
196
+ />
197
+ </div>
198
+ </template>
199
+
200
+ <script setup lang="ts">
201
+ import { ES_INDEX } from "../../../oip-drugs/esIndices";
202
+ import useOipDrugsElastic from "../../../oip-drugs/composables/useOipDrugsElastic";
203
+ import useDirectusSources from "../../../oip-drugs/composables/useDirectusSources";
204
+ import { useAssociationLookup } from "../../../oip-drugs/composables/useAssociationLookup";
205
+ import useHeartIntlApprovals from "../../../oip-drugs/composables/useHeartIntlApprovals";
206
+ import useHeartBrandMatches from "../../../oip-drugs/composables/useHeartBrandMatches";
207
+ import { HEART_LES_INDEX } from "../../../oip-drugs/composables/useHeartLesIndication";
208
+ import { HEART_SOURCE_UUIDS } from "../../../oip-drugs/utils/sourceUuids";
209
+ import { HEART_INTL_PRICE_COUNTRIES } from "../../../oip-drugs/utils/heartIntlPrice";
210
+ import { heartFixMojibake } from "../../../oip-drugs/utils/heartStyleAndFormat";
211
+ import { heartStripBrandPrefix } from "../../../oip-drugs/utils/heartStyleAnsm";
212
+ import { drugsAssociationPath } from "../../../oip-drugs/kinds";
213
+ import { OIP_RESOURCE_NAV } from "../../../composables/useOipResourceNav";
214
+
215
+ const props = withDefaults(
216
+ defineProps<{
217
+ /** La marque (brand_group HAS), ex `route.params.id`. */
218
+ brand: string;
219
+ /** Cle d'onglet demandee (ex `?tab=`), alias francais acceptes. */
220
+ tab?: string;
221
+ /** CIS preselectionne dans ANSM (`_id`) ou MITM (`cis_code`), ex `?cis=`. */
222
+ cis?: string;
223
+ /** Code HAS (ex `?code=`). Garde pour le contrat : rien ne le relisait. */
224
+ code?: string;
225
+ /** Indication LES a ouvrir dans son tiroir (avec `tab=les`), ex `?indication=`. */
226
+ indication?: string;
227
+ lang?: "fr" | "en";
228
+ /** Termes de recherche a surligner. */
229
+ highlight?: string[];
230
+ /** Fleche de retour du bandeau (page de fr_drugs_app). */
231
+ haveReturn?: boolean;
232
+ }>(),
233
+ {
234
+ tab: "",
235
+ cis: "",
236
+ code: "",
237
+ indication: "",
238
+ lang: undefined,
239
+ highlight: () => [],
240
+ haveReturn: false,
241
+ },
242
+ );
243
+
244
+ const emit = defineEmits<{ "update:tab": [key: string] }>();
245
+
246
+ const { queryCustomElastic } = useOipDrugsElastic();
247
+ const { locale: i18nLocale, t } = useI18n();
248
+ const locale = computed<"fr" | "en">(() => props.lang ?? (i18nLocale.value === "en" ? "en" : "fr"));
249
+
250
+ const resultsQuery = ref(null as any);
251
+ /** `_id` ES de la marque, ce que le bouton « Projet » range. */
252
+ const brandId = ref<string | null>(null);
253
+ const resultsQueryAnsm = ref<Array<{ _id: string; _source: any }>>([]);
254
+ const resultsQueryMitm = ref<Array<{ _id: string; _source: any }>>([]);
255
+ const resultsQueryPut = ref<Array<{ _id: string; _source: any }>>([]);
256
+ const resultsQueryLes = ref<any[]>([]);
257
+ const indexQuery = ES_INDEX.BRAND_HAS;
258
+ const isLoadingQuery = ref(true);
259
+ /** Les sections secondaires (prix, ANSM, MITM...) sont-elles encore en route ? */
260
+ const isLoadingSecondary = ref(true);
261
+
262
+ // La marque telle que le lien la porte (cf. drugsBrandPath). Les liens
263
+ // partages avant heartBrandPath ecrivaient « / » en « + » : `lanchQuery` se
264
+ // replie sur cette ecriture si la HAS ne connait pas la marque telle quelle.
265
+ const brandMatch = ref(String(props.brand ?? ""));
266
+
267
+ const title = computed(() => resultsQuery.value?.brand_group || brandMatch.value);
268
+
269
+ const hasAlerts = computed(() => {
270
+ return resultsQuery.value?.brand_history?.some(
271
+ (h: any) => h.document_type === 'alert' && h.alert_details?.length > 0
272
+ ) ?? false;
273
+ });
274
+
275
+ const alertHistory = computed(() => {
276
+ if (!resultsQuery.value?.brand_history) return [];
277
+ return resultsQuery.value.brand_history
278
+ .filter((h: any) => h.document_type === 'alert' && h.alert_details?.length > 0)
279
+ .sort((a: any, b: any) =>
280
+ new Date(b.security_end_date).getTime() - new Date(a.security_end_date).getTime()
281
+ );
282
+ });
283
+
284
+ const overviewHistory = computed(() => {
285
+ if (!resultsQuery.value?.brand_history) return [];
286
+ return resultsQuery.value.brand_history.filter(
287
+ (h: any) => h.document_type !== 'alert'
288
+ );
289
+ });
290
+
291
+ const hasContributions = computed(
292
+ () => (resultsQuery.value?.patient_contribution?.length ?? 0) > 0
293
+ );
294
+
295
+ // Monographie : si le produit dispose d'une note d'intelligence HAS, on ajoute
296
+ // un onglet "Monographie" EN PREMIER.
297
+ const MONOGRAPH_BRANDS = [
298
+ 'blincyto', 'darzalex', 'dupixent', 'enhertu', 'ibrance', 'imfinzi', 'keytruda',
299
+ 'kisqali', 'opdivo', 'ozempic', 'tecentriq', 'yescarta',
300
+ ];
301
+ const monographId = computed(() => {
302
+ const bg = (resultsQuery.value?.brand_group || '').toLowerCase();
303
+ const brand = MONOGRAPH_BRANDS.find((b) => bg.includes(b));
304
+ return brand ? `${brand}-has` : null;
305
+ });
306
+ const hasMonograph = computed(() => monographId.value !== null);
307
+ const monographPdf = computed(() =>
308
+ monographId.value ? `/reports/${monographId.value}.pdf` : null
309
+ );
310
+
311
+ // Prix France, prix internationaux et registres etrangers : declares avant la
312
+ // liste des onglets, qui les lit.
313
+ const resultsQueryPrice = ref<any[]>([]);
314
+ const resultsQueryIntlPrice = ref<any[]>([]);
315
+ const { records: intlRecords, isLoading: intlLoading, load: loadIntlApprovals } =
316
+ useHeartIntlApprovals();
317
+ const intlDecisionCount = computed(() =>
318
+ Object.values(intlRecords.value as Record<string, any[]>).reduce((sum, list) => sum + list.length, 0),
319
+ );
320
+
321
+ // Onglets disponibles (l'ordre = l'ordre d'affichage). La MARQUE est le pivot :
322
+ // HAS n'apparait que s'il existe un avis, ANSM que s'il existe des CIS, etc.
323
+ const availableTabs = computed(() => {
324
+ const en = locale.value === 'en';
325
+ const tabs: { key: string; label: string }[] = [];
326
+ if (hasMonograph.value) tabs.push({ key: 'monograph', label: en ? 'Monograph' : 'Monographie' });
327
+ if (resultsQuery.value) tabs.push({ key: 'overview', label: 'HAS' });
328
+ if (hasContributions.value) tabs.push({ key: 'contributions', label: en ? 'Patient Contribution' : 'Contribution Patient' });
329
+ if (resultsQueryPrice.value.length > 0) tabs.push({ key: 'price', label: t('oipDrugs.heart.details.tabs.price') });
330
+ if (resultsQueryIntlPrice.value.length > 0 || intlDecisionCount.value > 0) tabs.push({ key: 'international', label: en ? 'International view' : 'Regard international' });
331
+ if (resultsQueryAnsm.value.length > 0) tabs.push({ key: 'ansm', label: 'ANSM' });
332
+ if (resultsQueryMitm.value.length > 0) tabs.push({ key: 'mitm', label: 'MITM' });
333
+ if (resultsQueryPut.value.length > 0) tabs.push({ key: 'put', label: 'PUT' });
334
+ if (resultsQueryLes.value.length > 0) tabs.push({ key: 'les', label: 'LES' });
335
+ // Les alertes ferment la liste : elles completent la fiche, elles ne la
336
+ // presentent pas.
337
+ if (hasAlerts.value) tabs.push({ key: 'alerts', label: en ? 'Safety Alerts' : 'Alertes' });
338
+ return tabs;
339
+ });
340
+
341
+ const chosenTab = ref(1);
342
+
343
+ // Onglets du bandeau : RmPageHeader travaille sur des cles, la fiche garde son
344
+ // indice a partir de 1 pour choisir le bloc rendu.
345
+ const headerTabs = computed(() =>
346
+ availableTabs.value.map((tb, index) => ({
347
+ key: tb.key,
348
+ label: tb.label,
349
+ active: chosenTab.value === index + 1,
350
+ })),
351
+ );
352
+
353
+ function selectTab(key: string) {
354
+ const index = availableTabs.value.findIndex((tb) => tb.key === key);
355
+ if (index >= 0) chosenTab.value = index + 1;
356
+ }
357
+
358
+ /** Ecritures francaises acceptees dans `tab`, pour les liens partages. */
359
+ const TAB_ALIASES: Record<string, string> = {
360
+ prix: 'price',
361
+ alertes: 'alerts',
362
+ monographie: 'monograph',
363
+ contributions_patients: 'contributions',
364
+ avis: 'overview',
365
+ has: 'overview',
366
+ liste_en_sus: 'les',
367
+ regard_international: 'international',
368
+ prix_internationaux: 'international',
369
+ };
370
+
371
+ const normalizeTab = (rawKey: string) => TAB_ALIASES[String(rawKey).toLowerCase()] ?? rawKey;
372
+
373
+ // Les onglets sont numerotes a partir de 1 (le 1er onglet = 1) ; 0 = onglet absent.
374
+ const tabIndexOf = (rawKey: string) => {
375
+ const key = normalizeTab(rawKey);
376
+ const i = availableTabs.value.findIndex((tb) => tb.key === key);
377
+ return i === -1 ? 0 : i + 1;
378
+ };
379
+
380
+ const monographTabIndex = computed(() => tabIndexOf('monograph'));
381
+ const overviewTabIndex = computed(() => tabIndexOf('overview'));
382
+ const contributionsTabIndex = computed(() => tabIndexOf('contributions'));
383
+ const alertTabIndex = computed(() => tabIndexOf('alerts'));
384
+ const priceTabIndex = computed(() => tabIndexOf('price'));
385
+ const internationalTabIndex = computed(() => tabIndexOf('international'));
386
+ const ansmTabIndex = computed(() => tabIndexOf('ansm'));
387
+ const mitmTabIndex = computed(() => tabIndexOf('mitm'));
388
+ const putTabIndex = computed(() => tabIndexOf('put'));
389
+ const lesTabIndex = computed(() => tabIndexOf('les'));
390
+
391
+ /** L'onglet ouvert, par sa cle et son libelle. */
392
+ const currentTabKey = computed(() => availableTabs.value[chosenTab.value - 1]?.key ?? null);
393
+ const currentTabLabel = computed(() => availableTabs.value[chosenTab.value - 1]?.label ?? "");
394
+
395
+ /** Chargement : tant que la HAS n'a pas repondu, ou qu'aucun onglet n'est encore la. */
396
+ const showLoading = computed(
397
+ () => isLoadingQuery.value || (isLoadingSecondary.value && availableTabs.value.length === 0),
398
+ );
399
+ /** Aucune section pour cette marque, une fois tout charge. */
400
+ const showEmpty = computed(() => !showLoading.value && availableTabs.value.length === 0);
401
+
402
+ // CIS selectionne dans l'onglet ANSM (selecteur de specialites de la marque).
403
+ const selectedAnsmIndex = ref(0);
404
+ const selectedAnsmCis = computed(
405
+ () => resultsQueryAnsm.value[selectedAnsmIndex.value]?._source ?? null
406
+ );
407
+
408
+ // CIS selectionne dans l'onglet MITM.
409
+ const selectedMitmIndex = ref(0);
410
+ const selectedMitmCis = computed(
411
+ () => resultsQueryMitm.value[selectedMitmIndex.value]?._source ?? null
412
+ );
413
+
414
+ /** Avis ouvert dans le tiroir de l'onglet HAS. */
415
+ const drawerOpinion = ref<any>(null);
416
+
417
+ /**
418
+ * Libelles et dates de source des tiroirs : Directus, comme le panneau
419
+ * « Sources » du rail.
420
+ */
421
+ const { load: loadSourceMeta, metaByUuid, verifiedRaw } = useDirectusSources();
422
+ loadSourceMeta(Object.values(HEART_SOURCE_UUIDS));
423
+ const sourceName = (uuid: string) => metaByUuid(uuid, locale.value).name;
424
+
425
+ /** Date de derniere verification de la source HAS, montree en pied de tiroir. */
426
+ const hasCheckedAt = computed(() => verifiedRaw(HEART_SOURCE_UUIDS.HAS));
427
+
428
+ /** Enregistrement ouvert dans le tiroir des onglets Alertes, PUT et Contributions. */
429
+ const drawerRecord = ref<any>(null);
430
+
431
+ /** Indication LES ouverte dans son tiroir (code, titre, date de la liste). */
432
+ const drawerLes = ref<any>(null);
433
+
434
+ /** Premiere valeur remplie ; a defaut de traduction on garde le francais. */
435
+ function pickText(...values: any[]) {
436
+ for (const value of values) {
437
+ if (typeof value === "string" && value.trim()) return heartFixMojibake(value.trim());
438
+ }
439
+ return "";
440
+ }
441
+
442
+ /** Une alerte par code CIS, comme le detail d'alerte le faisait deja. */
443
+ const alertRecords = computed(() =>
444
+ alertHistory.value.flatMap((entry: any, entryIndex: number) => {
445
+ const codes = [...new Set((entry.alert_details ?? []).map((a: any) => a.cis_code))];
446
+ return codes.map((code) => {
447
+ const details = (entry.alert_details ?? []).filter((a: any) => a.cis_code === code);
448
+ const first = details[0] ?? {};
449
+ const message = pickText(
450
+ locale.value === "en" ? first.display_message_en : first.display_message,
451
+ first.display_message,
452
+ first.display_message_en
453
+ );
454
+ const brands = [...new Set(details.map((a: any) => a.brand).filter(Boolean))].join(", ");
455
+ return {
456
+ // Deux alertes d'un meme avis peuvent viser le meme CIS : l'indice
457
+ // de l'entree garde la cle unique.
458
+ key: `${entry.has_code || entry.opinion_id || ""}-${entryIndex}-${code}`,
459
+ typeLabel: locale.value === "en" ? "Safety alert" : "Alerte de sécurité",
460
+ icon: "Warning",
461
+ date: entry.security_end_date || null,
462
+ ref: code ? `CIS ${code}` : null,
463
+ url: first.url_link || null,
464
+ title: message || (locale.value === "en" ? "Safety alert" : "Alerte de sécurité"),
465
+ excerpt: brands || null,
466
+ sourceLabel: sourceName(HEART_SOURCE_UUIDS.ANSM),
467
+ sections: [
468
+ { key: "message", label: "Message", body: message },
469
+ {
470
+ key: "brands",
471
+ label: locale.value === "en" ? "Brands concerned" : "Spécialités concernées",
472
+ body: brands,
473
+ },
474
+ ],
475
+ };
476
+ });
477
+ })
478
+ );
479
+
480
+ /** Pre-AMM ou post-AMM, accents et langue mis de cote. */
481
+ function aapTone(label: string) {
482
+ const value = (label || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
483
+ if (value.includes("pre")) return "pre" as const;
484
+ if (value.includes("post")) return "post" as const;
485
+ return "neutral" as const;
486
+ }
487
+
488
+ /** Statut d'un P.U.T : « en cours » se lit comme une bonne nouvelle. */
489
+ function putTone(status: string): "good" | "neutral" {
490
+ const value = (status || "").toLowerCase();
491
+ if (value.includes("en cours") || value.includes("ongoing")) return "good";
492
+ return "neutral";
493
+ }
494
+
495
+ const putRecords = computed(() =>
496
+ (resultsQueryPut.value ?? []).map((hit: any) => {
497
+ const put = hit._source ?? {};
498
+ const english = locale.value === "en";
499
+ const status = pickText(english ? put.status_en : put.status_fr, put.status_fr);
500
+ const indication = pickText(
501
+ english ? put.indication_en : put.indication_fr,
502
+ put.indication_fr
503
+ );
504
+ const objective = pickText(
505
+ english ? put.objective_en : put.objective_fr,
506
+ put.objective_fr
507
+ );
508
+ const efficacy = pickText(
509
+ english ? put.efficacy_precision_en : put.efficacy_precision_fr,
510
+ put.efficacy_precision_fr
511
+ );
512
+ const periodicity = pickText(
513
+ english ? put.put_periodicity_en : put.put_periodicity_fr,
514
+ put.put_periodicity_fr
515
+ );
516
+ const aapType = pickText(english ? put.aap_type_en : put.aap_type_fr, put.aap_type_fr);
517
+ const area = pickText(
518
+ english ? put.therapeutic_area_en : put.therapeutic_area_fr,
519
+ put.therapeutic_area_fr
520
+ );
521
+ const links = [...(put.tekkare_doc_links ?? []), ...(put.tekkare_pdf_links ?? [])];
522
+
523
+ return {
524
+ key: hit._id,
525
+ typeLabel: "P.U.T",
526
+ icon: "ClipboardText",
527
+ date: put.authorization_date || null,
528
+ badge: aapType || null,
529
+ badgeTone: aapTone(aapType),
530
+ url: links[0] || null,
531
+ title: pickText(put.specialty_dci) || "P.U.T",
532
+ excerpt: objective || indication || null,
533
+ status: status || null,
534
+ statusTone: putTone(status),
535
+ sourceLabel: sourceName(HEART_SOURCE_UUIDS.HAS),
536
+ sections: [
537
+ { key: "objective", label: t("oipDrugs.heart.put.card.objectives"), body: objective },
538
+ { key: "indication", label: t("oipDrugs.heart.put.card.indication"), body: indication },
539
+ { key: "efficacy", label: t("oipDrugs.heart.put.card.efficacy"), body: efficacy },
540
+ {
541
+ key: "context",
542
+ label: english ? "Framework" : "Cadre",
543
+ body: [
544
+ periodicity && `${t("oipDrugs.heart.put.card.periodicity")} : ${periodicity}`,
545
+ aapType && `${t("oipDrugs.heart.put.card.aapType")} : ${aapType}`,
546
+ area && `${english ? "Therapeutic area" : "Aire thérapeutique"} : ${area}`,
547
+ ]
548
+ .filter(Boolean)
549
+ .join("\n"),
550
+ },
551
+ ],
552
+ };
553
+ })
554
+ );
555
+
556
+ /**
557
+ * Indications en liste en sus de la marque : une entree par code d'indication,
558
+ * les lignes produit rassemblees dessous. La date est la plus recente
559
+ * inscription ; le tiroir montre ensuite le detail complet du code.
560
+ */
561
+ const lesRecords = computed(() => {
562
+ const byCode = new Map<string, any[]>();
563
+ for (const row of resultsQueryLes.value) {
564
+ if (!row?.indication) continue;
565
+ if (!byCode.has(row.indication)) byCode.set(row.indication, []);
566
+ byCode.get(row.indication)!.push(row);
567
+ }
568
+ return [...byCode.entries()]
569
+ .map(([code, rows]) => {
570
+ const label: string = rows[0].indication_label_fr || "";
571
+ const dates = rows.map((r) => r.start_date).filter(Boolean).sort();
572
+ const products = [...new Set(rows.map((r) => r.french_label).filter(Boolean))];
573
+ // « OUI » seul ne dit rien : on garde la question qu'il resout.
574
+ const flagged = rows.find((r) => r.statut);
575
+ const status = flagged
576
+ ? [flagged.statut_title, flagged.statut].filter(Boolean).join(" : ")
577
+ : null;
578
+ return {
579
+ key: code,
580
+ code,
581
+ typeLabel: "Liste en sus",
582
+ icon: "ListChecks",
583
+ date: dates[dates.length - 1] ?? null,
584
+ ref: code,
585
+ badge: rows[0].source || null,
586
+ title: label ? label.charAt(0) + label.slice(1).toLowerCase() : `Indication ${code}`,
587
+ excerpt: products.join(", ") || null,
588
+ status,
589
+ sourceLabel: sourceName(HEART_SOURCE_UUIDS.LES),
590
+ };
591
+ })
592
+ .sort((a, b) => new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime());
593
+ });
594
+
595
+ /** Rubriques d'une contribution, dans l'ordre de la fiche d'origine. */
596
+ const CONTRIBUTION_FIELDS = [
597
+ { field: "drug_indication", label: "indication" },
598
+ { field: "synthesis", label: "synthesis" },
599
+ { field: "impact", label: "impact" },
600
+ { field: "method", label: "method" },
601
+ { field: "experience_drug", label: "experienceDrug" },
602
+ { field: "experience_current_treatment", label: "experienceCurrent" },
603
+ { field: "additional_info", label: "additional" },
604
+ ];
605
+
606
+ /**
607
+ * La fiche de l'association qui signe chaque contribution. Le renvoi
608
+ * n'apparait qu'une fois l'association reconnue (cf. useAssociationLookup).
609
+ */
610
+ const { load: loadAssociations, idFor: associationIdFor } = useAssociationLookup();
611
+
612
+ watch(
613
+ () => resultsQuery.value?.patient_contribution,
614
+ (contributions: any[] | undefined) => {
615
+ if (!contributions?.length) return;
616
+ loadAssociations(
617
+ contributions.flatMap((contribution: any) => [
618
+ contribution?.association_label_regroup,
619
+ contribution?.association_label,
620
+ ]),
621
+ );
622
+ },
623
+ { immediate: true },
624
+ );
625
+
626
+ const contributionRecords = computed(() =>
627
+ [...(resultsQuery.value?.patient_contribution ?? [])]
628
+ .sort(
629
+ (a: any, b: any) =>
630
+ new Date(b.contribution_date).getTime() - new Date(a.contribution_date).getTime()
631
+ )
632
+ .map((contribution: any, index: number) => {
633
+ const regroup = contribution.association_label_regroup || contribution.association_label;
634
+ const original =
635
+ contribution.association_label &&
636
+ contribution.association_label_regroup &&
637
+ contribution.association_label !== contribution.association_label_regroup
638
+ ? contribution.association_label
639
+ : null;
640
+
641
+ const associationId = associationIdFor(regroup, contribution.association_label);
642
+
643
+ return {
644
+ key: contribution.has_code ? `${contribution.has_code}-${index}` : String(index),
645
+ typeLabel: locale.value === "en" ? "Patient contribution" : "Contribution patient",
646
+ // `to` : chemin de fr_drugs_app, suivi par useOipResourceNav dans le tiroir.
647
+ link: associationId
648
+ ? {
649
+ label: t("oipDrugs.heart.details.patientContrib.seeAssociation"),
650
+ to: drugsAssociationPath(associationId),
651
+ icon: "UsersThree",
652
+ }
653
+ : null,
654
+ // Sept rubriques : en onglets elles debordaient sur trois lignes.
655
+ layout: "accordion" as const,
656
+ icon: "Users",
657
+ date: contribution.contribution_date || null,
658
+ ref: contribution.request_nature || null,
659
+ url: contribution.contribution_url || null,
660
+ title: regroup || "-",
661
+ excerpt: pickText(contribution.synthesis) || original,
662
+ sourceLabel: sourceName(HEART_SOURCE_UUIDS.HAS),
663
+ sections: CONTRIBUTION_FIELDS.map((section) => ({
664
+ key: section.field,
665
+ label: t(`oipDrugs.heart.details.patientContrib.${section.label}`),
666
+ body: pickText(contribution[section.field]),
667
+ })),
668
+ };
669
+ })
670
+ );
671
+
672
+ // Pour eviter les bugs lors de la navigation : une reponse d'une marque
673
+ // precedente (ou d'une fiche demontee) ne s'ecrit pas.
674
+ const isMounted = ref(false);
675
+ let generation = 0;
676
+ const live = (gen: number) => isMounted.value && gen === generation;
677
+
678
+ watch(
679
+ () => chosenTab.value,
680
+ (val) => {
681
+ // Changer d'onglet ferme le tiroir : sinon le detail d'une alerte reste
682
+ // ouvert par-dessus l'onglet suivant.
683
+ drawerRecord.value = null;
684
+ // L'onglet ouvert remonte a l'appelant (ex `?tab=` de la page).
685
+ const key = availableTabs.value[val - 1]?.key;
686
+ if (isMounted.value && key && normalizeTab(props.tab) !== key) {
687
+ emit("update:tab", key);
688
+ }
689
+ }
690
+ );
691
+
692
+ // L'appelant change l'onglet (retour arriere de la page, lien interne...).
693
+ watch(
694
+ () => props.tab,
695
+ (key) => {
696
+ if (!key || isLoadingQuery.value) return;
697
+ const index = tabIndexOf(key);
698
+ if (index > 0 && index !== chosenTab.value) chosenTab.value = index;
699
+ }
700
+ );
701
+
702
+ // `and` sur tous les mots, et l'ecriture exacte remontee en tete : avec un
703
+ // seul resultat, « KEYTRUDA » ne doit pas ramener une marque voisine.
704
+ const customQuery = (brand: string) => ({
705
+ from: 0,
706
+ query: {
707
+ bool: {
708
+ must: [{ match: { brand_group: { query: brand, operator: "and" } } }],
709
+ should: [{ term: { "brand_group.keyword": brand } }],
710
+ },
711
+ },
712
+ size: 1,
713
+ });
714
+
715
+ const lanchQuery = async (gen: number) => {
716
+ if (!live(gen)) return;
717
+
718
+ isLoadingQuery.value = true;
719
+ // Reponse entiere (`allAnswer`) plutot que `onlyOne` : le bouton « Projet »
720
+ // range la fiche par son `_id`, que `onlyOne` ne rend pas.
721
+ const fetchBrand = (brand: string) =>
722
+ queryCustomElastic({
723
+ index: indexQuery,
724
+ customQuery: customQuery(brand),
725
+ allAnswer: true,
726
+ })
727
+ .then((r: any) => r?.hits?.hits?.[0] ?? null)
728
+ .catch((error) => {
729
+ console.error('Error fetching data:', error);
730
+ return null;
731
+ });
732
+
733
+ let hit = await fetchBrand(brandMatch.value);
734
+ // Repli pour les liens partages avant heartBrandPath (« / » ecrit « + »).
735
+ if (!hit?._source?.brand_group && brandMatch.value.includes(" + ")) {
736
+ const legacy = brandMatch.value.replace(" + ", " / ");
737
+ hit = await fetchBrand(legacy);
738
+ if (hit?._source?.brand_group && live(gen)) brandMatch.value = legacy;
739
+ }
740
+ // Sur erreur ES le composable rend un objet vide : sans `brand_group`, ce
741
+ // n'est pas une fiche, on ne l'affiche pas.
742
+ if (hit?._source?.brand_group && live(gen)) {
743
+ resultsQuery.value = hit._source;
744
+ brandId.value = hit._id;
745
+ }
746
+ if (live(gen)) {
747
+ isLoadingQuery.value = false;
748
+ }
749
+ };
750
+
751
+ // Les requetes secondaires lisent `brandMatch` au lancement, pas a la
752
+ // declaration : le repli de `lanchQuery` a pu le changer entre-temps.
753
+ const customQueryPrices = () => ({
754
+ from: 0,
755
+ _source: [],
756
+ query: {
757
+ bool: {
758
+ must: [{ term: { "brand.keyword": brandMatch.value } }],
759
+ },
760
+ },
761
+ size: 100,
762
+ track_total_hits: true,
763
+ });
764
+
765
+ const launchQueryPrices = async (gen: number) => {
766
+ if (!live(gen)) return;
767
+ await queryCustomElastic({
768
+ index: ES_INDEX.DRUG_CARDS,
769
+ customQuery: customQueryPrices(),
770
+ }).then((res) => {
771
+ if (res && res.results && res.results.length > 0 && live(gen)) {
772
+ resultsQueryPrice.value = res.results;
773
+ }
774
+ }).catch((error) => {
775
+ console.error('Error fetching prices:', error);
776
+ });
777
+ };
778
+
779
+ // Suivi mondial des prix : ce que les autres pays publient pour la marque.
780
+ // Le prefixe rattrape les declinaisons de gamme (« KEYTRUDA QLEX »).
781
+ const launchQueryIntlPrice = async (gen: number) => {
782
+ if (!live(gen)) return;
783
+ const res = await queryCustomElastic({
784
+ index: ES_INDEX.WW_PRICE_TRACKER,
785
+ customQuery: {
786
+ from: 0,
787
+ query: {
788
+ bool: {
789
+ should: [
790
+ { term: { "brand.keyword": brandMatch.value } },
791
+ { prefix: { "brand.keyword": `${brandMatch.value} ` } },
792
+ ],
793
+ minimum_should_match: 1,
794
+ // Le perimetre des pays est filtre ici, pas seulement a l'affichage.
795
+ filter: [
796
+ { terms: { "country.country_iso_2.keyword": HEART_INTL_PRICE_COUNTRIES } },
797
+ ],
798
+ },
799
+ },
800
+ size: 200,
801
+ track_total_hits: true,
802
+ } as any,
803
+ }).catch((error) => {
804
+ console.error("Error fetching international prices:", error);
805
+ return null;
806
+ });
807
+ if (res?.results && live(gen)) {
808
+ resultsQueryIntlPrice.value = res.results;
809
+ }
810
+ };
811
+
812
+ // ANSM (autorisation) : toutes les specialites CIS de la marque.
813
+ const customQueryAnsm = () => ({
814
+ from: 0,
815
+ query: { bool: { must: [{ term: { "brand.keyword": brandMatch.value } }] } },
816
+ size: 200,
817
+ sort: [{ authorization_date: { order: "desc", missing: "_last" } }],
818
+ track_total_hits: true,
819
+ });
820
+ const launchQueryAnsm = async (gen: number) => {
821
+ if (!live(gen)) return;
822
+ const res = await queryCustomElastic({
823
+ index: ES_INDEX.CIS_ANSM,
824
+ customQuery: customQueryAnsm() as any,
825
+ allAnswer: true,
826
+ }).catch((error) => {
827
+ console.error("Error fetching ANSM:", error);
828
+ return null;
829
+ });
830
+ if (res?.hits?.hits && live(gen)) {
831
+ resultsQueryAnsm.value = res.hits.hits;
832
+ }
833
+ };
834
+
835
+ // MITM (liste des medicaments d'interet therapeutique majeur) de la marque.
836
+ const customQueryMitm = () => ({
837
+ from: 0,
838
+ query: { bool: { must: [{ term: { "brand.keyword": brandMatch.value } }] } },
839
+ size: 200,
840
+ track_total_hits: true,
841
+ });
842
+ const launchQueryMitm = async (gen: number) => {
843
+ if (!live(gen)) return;
844
+ const res = await queryCustomElastic({
845
+ index: ES_INDEX.MITM_BDPM,
846
+ customQuery: customQueryMitm(),
847
+ allAnswer: true,
848
+ }).catch((error) => {
849
+ console.error("Error fetching MITM:", error);
850
+ return null;
851
+ });
852
+ if (res?.hits?.hits && live(gen)) {
853
+ // Deduplication par cis_code (plusieurs snapshots du meme CIS) : on garde
854
+ // le plus recent via @timestamp.
855
+ const byCis = new Map<string, any>();
856
+ for (const h of res.hits.hits) {
857
+ const key = h._source?.cis_code ?? h._id;
858
+ const existing = byCis.get(key);
859
+ if (
860
+ !existing ||
861
+ new Date(h._source?.["@timestamp"] || 0).getTime() >
862
+ new Date(existing._source?.["@timestamp"] || 0).getTime()
863
+ ) {
864
+ byCis.set(key, h);
865
+ }
866
+ }
867
+ resultsQueryMitm.value = [...byCis.values()];
868
+ }
869
+ };
870
+
871
+ // PUT (protocoles d'utilisation therapeutique / acces precoce) de la marque.
872
+ const customQueryPut = () => ({
873
+ from: 0,
874
+ query: { bool: { must: [{ term: { "brand.keyword": brandMatch.value } }] } },
875
+ size: 200,
876
+ sort: [{ authorization_date: { order: "desc", missing: "_last" } }],
877
+ track_total_hits: true,
878
+ });
879
+ const launchQueryPut = async (gen: number) => {
880
+ if (!live(gen)) return;
881
+ const res = await queryCustomElastic({
882
+ index: ES_INDEX.PUT_HAS,
883
+ customQuery: customQueryPut() as any,
884
+ allAnswer: true,
885
+ }).catch((error) => {
886
+ console.error("Error fetching PUT:", error);
887
+ return null;
888
+ });
889
+ if (res?.hits?.hits && live(gen)) {
890
+ resultsQueryPut.value = res.hits.hits;
891
+ }
892
+ };
893
+
894
+ // LES (liste en sus) : nom exact d'abord, puis nom contenu, les libelles LES
895
+ // pouvant porter un dosage ou un regroupement que la marque HAS n'a pas.
896
+ const launchQueryLes = async (gen: number) => {
897
+ if (!live(gen)) return;
898
+ const run = (clause: any) =>
899
+ queryCustomElastic({
900
+ index: HEART_LES_INDEX,
901
+ customQuery: { from: 0, query: { bool: { must: [clause] } }, size: 200, track_total_hits: true },
902
+ cluster: "pubmed",
903
+ }).catch((error) => {
904
+ console.error("Error fetching LES:", error);
905
+ return null;
906
+ });
907
+ let res = await run({ term: { "brand.keyword": brandMatch.value } });
908
+ if (!res?.results?.length) res = await run({ wildcard: { "brand.keyword": `*${brandMatch.value}*` } });
909
+ if (res?.results && live(gen)) {
910
+ resultsQueryLes.value = res.results;
911
+ }
912
+ };
913
+
914
+ /** Ce que la recherche a matche sur cette marque, pour marquer les avis. */
915
+ const brandMatches = useHeartBrandMatches();
916
+
917
+ /** Termes surlignes : ceux du moteur pour cette marque, sinon ceux de l'appelant. */
918
+ const searchTerms = computed<string[]>(() =>
919
+ brandMatches.terms.value?.length ? brandMatches.terms.value : props.highlight ?? [],
920
+ );
921
+
922
+ /**
923
+ * Onglet demande par la prop `tab` (ansm, alerts, price, overview...). Rend
924
+ * faux tant que la section n'est pas chargee : l'appelant reessaie plus tard.
925
+ */
926
+ function openRequestedTab() {
927
+ const requestedIdx = props.tab ? tabIndexOf(props.tab) : 0;
928
+ if (requestedIdx === 0) return false;
929
+ chosenTab.value = requestedIdx;
930
+ // tab=les + indication=<code> : le tiroir de l'indication s'ouvre d'emblee.
931
+ if (normalizeTab(props.tab) === 'les' && props.indication) {
932
+ const record = lesRecords.value.find((r) => r.code === props.indication);
933
+ if (record) drawerLes.value = record;
934
+ }
935
+ return true;
936
+ }
937
+
938
+ /** Remet la fiche a zero, pour une autre marque. */
939
+ function reset() {
940
+ resultsQuery.value = null;
941
+ brandId.value = null;
942
+ resultsQueryPrice.value = [];
943
+ resultsQueryIntlPrice.value = [];
944
+ resultsQueryAnsm.value = [];
945
+ resultsQueryMitm.value = [];
946
+ resultsQueryPut.value = [];
947
+ resultsQueryLes.value = [];
948
+ selectedAnsmIndex.value = 0;
949
+ selectedMitmIndex.value = 0;
950
+ drawerOpinion.value = null;
951
+ drawerRecord.value = null;
952
+ drawerLes.value = null;
953
+ chosenTab.value = 1;
954
+ }
955
+
956
+ async function loadAll() {
957
+ const gen = ++generation;
958
+ isLoadingSecondary.value = true;
959
+ await lanchQuery(gen);
960
+ if (!live(gen)) return;
961
+
962
+ // L'onglet se pose des que la HAS a repondu. Par defaut l'historique HAS
963
+ // (jamais la monographie), a defaut le premier onglet disponible.
964
+ const tabOpened = openRequestedTab();
965
+ if (!tabOpened) chosenTab.value = overviewTabIndex.value > 0 ? overviewTabIndex.value : 1;
966
+
967
+ const brand = resultsQuery.value?.brand_group || brandMatch.value;
968
+ await Promise.all([
969
+ launchQueryPrices(gen),
970
+ launchQueryIntlPrice(gen),
971
+ loadIntlApprovals(brand),
972
+ launchQueryAnsm(gen),
973
+ launchQueryMitm(gen),
974
+ launchQueryPut(gen),
975
+ launchQueryLes(gen),
976
+ brandMatches.load(brand, indexQuery),
977
+ ]);
978
+ if (!live(gen)) return;
979
+ isLoadingSecondary.value = false;
980
+
981
+ // Preselection du CIS (clic sur un dosage precis dans une carte ANSM : cis=<_id>)
982
+ if (props.cis) {
983
+ const i = resultsQueryAnsm.value.findIndex((h) => h._id === props.cis);
984
+ if (i !== -1) selectedAnsmIndex.value = i;
985
+ // MITM : la carte passe le cis_code (pas le _id du doc)
986
+ const j = resultsQueryMitm.value.findIndex(
987
+ (m) => String(m._source?.cis_code) === String(props.cis)
988
+ );
989
+ if (j !== -1) selectedMitmIndex.value = j;
990
+ }
991
+
992
+ // L'onglet demande dependait d'une section secondaire (prix, ANSM, LES...).
993
+ if (!tabOpened) openRequestedTab();
994
+ }
995
+
996
+ onMounted(() => {
997
+ isMounted.value = true;
998
+ loadAll();
999
+ });
1000
+
1001
+ // Une autre marque dans la meme instance (tiroir reutilise) : on recharge.
1002
+ watch(
1003
+ () => props.brand,
1004
+ (brand) => {
1005
+ if (!isMounted.value) return;
1006
+ brandMatch.value = String(brand ?? "");
1007
+ reset();
1008
+ loadAll();
1009
+ }
1010
+ );
1011
+
1012
+ // Le tiroir d'un projet fournit sa navigation : la fiche y est embarquee.
1013
+ const embedded = inject(OIP_RESOURCE_NAV, null) !== null;
1014
+
1015
+ onBeforeUnmount(() => {
1016
+ isMounted.value = false;
1017
+ if (import.meta.client && !embedded) {
1018
+ document.body.classList.remove("is-monograph-fullbleed");
1019
+ }
1020
+ });
1021
+
1022
+ // Onglet Monographie : on neutralise la largeur max du conteneur de page
1023
+ // (.oip_container) pour que le document occupe toute la zone de contenu.
1024
+ // Dans le tiroir d'un projet, la classe elargirait la page dessous.
1025
+ watchEffect(() => {
1026
+ if (import.meta.client && !embedded) {
1027
+ document.body.classList.toggle(
1028
+ "is-monograph-fullbleed",
1029
+ monographTabIndex.value > 0 && chosenTab.value === monographTabIndex.value
1030
+ );
1031
+ }
1032
+ });
1033
+
1034
+ // Le reste (libelle d'onglet, marque, sections secondaires) sert a la page de
1035
+ // fr_drugs_app : elle declare l'export du cadre depuis son script, ou le slot
1036
+ // `actions` n'est pas lisible. Memes donnees que les props de ce slot.
1037
+ defineExpose({
1038
+ product: resultsQuery,
1039
+ brandId,
1040
+ currentTabKey,
1041
+ availableTabs,
1042
+ currentTabLabel,
1043
+ brand: title,
1044
+ prices: resultsQueryPrice,
1045
+ intlPrices: resultsQueryIntlPrice,
1046
+ intlRecords,
1047
+ ansm: resultsQueryAnsm,
1048
+ mitm: resultsQueryMitm,
1049
+ put: resultsQueryPut,
1050
+ les: resultsQueryLes,
1051
+ });
1052
+ </script>
1053
+
1054
+ <style scoped>
1055
+ /* Le titre suit la typo du bandeau (RmPageHeader) ; le lien du tiroir
1056
+ (`title-link`) passe dessous. */
1057
+ .oip-drugs-brand__title {
1058
+ display: block;
1059
+ }
1060
+
1061
+ .oip-drugs-brand__state {
1062
+ box-sizing: border-box;
1063
+ width: 100%;
1064
+ padding: 24px 32px;
1065
+ }
1066
+
1067
+ /* Selecteur de specialite (CIS) de la marque, dans l'onglet Autorisation. */
1068
+ .cis-switch {
1069
+ display: flex;
1070
+ flex-wrap: wrap;
1071
+ align-items: center;
1072
+ justify-content: flex-start;
1073
+ align-self: flex-start;
1074
+ width: 100%;
1075
+ gap: 8px;
1076
+ margin-block: 16px;
1077
+ }
1078
+ .cis-switch-label {
1079
+ margin-right: 4px;
1080
+ }
1081
+ .cis-chip {
1082
+ border: 1px solid var(--light, #ececf2);
1083
+ background: var(--white);
1084
+ color: var(--dark);
1085
+ border-radius: 999px;
1086
+ padding: 6px 14px;
1087
+ font-size: 13px;
1088
+ font-weight: 600;
1089
+ cursor: pointer;
1090
+ transition: all 0.15s;
1091
+ }
1092
+ .cis-chip:hover {
1093
+ border-color: var(--primary);
1094
+ color: var(--primary);
1095
+ }
1096
+ .cis-chip.active {
1097
+ background: var(--primary);
1098
+ border-color: var(--primary);
1099
+ color: var(--white);
1100
+ }
1101
+ </style>
1102
+
1103
+ <style>
1104
+ /* Onglet Monographie : neutralise la largeur max + le padding du conteneur
1105
+ de page (full-bleed). Classe posee sur <body> par la fiche, hors tiroir. */
1106
+ body.is-monograph-fullbleed .oip_container {
1107
+ max-width: none !important;
1108
+ margin: 0 !important;
1109
+ padding: 0 !important;
1110
+ }
1111
+ </style>