@tekkare/romulus 0.1.250 → 0.1.252
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/BarChart.vue +1 -0
- package/dist/runtime/components/ChoroplethMap.vue +4 -1
- package/dist/runtime/internal/geo/fr-regions.json +1 -1
- package/dist/runtime/utils/chart.d.ts +6 -3
- package/dist/runtime/utils/chart.js +15 -2
- package/layer/app/app.config.ts +6 -0
- package/layer/app/components/OipPubmedSummary.vue +223 -0
- package/layer/app/components/OipResourceDrawer.vue +331 -0
- package/layer/app/components/OipResourceDrawerLink.vue +40 -0
- package/layer/app/components/OipResourceField.vue +41 -0
- package/layer/app/components/OipResourceGenericCard.vue +54 -0
- package/layer/app/components/OipResourceSource.vue +50 -0
- package/layer/app/components/OipResourceTags.vue +65 -0
- package/layer/app/components/OipResourceView.vue +107 -0
- package/layer/app/components/oip-drugs/fiches/AssociationFiche.vue +775 -0
- package/layer/app/components/oip-drugs/fiches/BrandFiche.vue +1111 -0
- package/layer/app/components/oip-drugs/fiches/IndicationFiche.vue +182 -0
- package/layer/app/components/oip-drugs/fiches/JoFiche.vue +101 -0
- package/layer/app/components/oip-drugs/fiches/PndsFiche.vue +279 -0
- package/layer/app/components/oip-drugs/heart/JoCard.vue +314 -0
- package/layer/app/components/oip-drugs/heart/TextHighlight.vue +326 -0
- package/layer/app/components/oip-drugs/heart/ansm/BrandCard.vue +187 -0
- package/layer/app/components/oip-drugs/heart/ansm/CisDetail.vue +363 -0
- package/layer/app/components/oip-drugs/heart/ansm/CisTable.vue +167 -0
- package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyCard.vue +373 -0
- package/layer/app/components/oip-drugs/heart/associations/NationalAssemblyFiche.vue +372 -0
- package/layer/app/components/oip-drugs/heart/associations/TranscriptionCard.vue +55 -0
- package/layer/app/components/oip-drugs/heart/elastic/SourceContainerElastic.vue +103 -0
- package/layer/app/components/oip-drugs/heart/france/BrandGroupCard.vue +450 -0
- package/layer/app/components/oip-drugs/heart/mitm/Card.vue +199 -0
- package/layer/app/components/oip-drugs/heart/mitm/Source.vue +23 -0
- package/layer/app/components/oip-drugs/heart/mitm/details/Overview.vue +381 -0
- package/layer/app/components/oip-drugs/heart/put/Card.vue +212 -0
- package/layer/app/components/oip-drugs/heart/results/AccesCard.vue +696 -0
- package/layer/app/components/oip-drugs/heart/results/AlertCard.vue +342 -0
- package/layer/app/components/oip-drugs/heart/results/AsmrCursor.vue +229 -0
- package/layer/app/components/oip-drugs/heart/results/EvalCard.vue +370 -0
- package/layer/app/components/oip-drugs/heart/results/IndicCard.vue +743 -0
- package/layer/app/components/oip-drugs/heart/results/MatchExplain.vue +243 -0
- package/layer/app/components/oip-drugs/heart/results/PrefRows.vue +105 -0
- package/layer/app/components/oip-drugs/heart/results/details/HasHistory.vue +487 -0
- package/layer/app/components/oip-drugs/heart/results/details/InternationalCountryCard.vue +461 -0
- package/layer/app/components/oip-drugs/heart/results/details/InternationalOverview.vue +200 -0
- package/layer/app/components/oip-drugs/heart/results/details/InternationalPriceDrawer.vue +647 -0
- package/layer/app/components/oip-drugs/heart/results/details/Monograph.vue +128 -0
- package/layer/app/components/oip-drugs/heart/results/details/OpinionDrawer.vue +535 -0
- package/layer/app/components/oip-drugs/heart/results/details/PresentationDrawer.vue +738 -0
- package/layer/app/components/oip-drugs/heart/results/details/Price.vue +227 -0
- package/layer/app/components/oip-drugs/heart/results/details/PriceOverview.vue +1261 -0
- package/layer/app/components/oip-drugs/heart/results/details/RecordDrawer.vue +494 -0
- package/layer/app/components/oip-drugs/heart/results/details/RecordList.vue +426 -0
- package/layer/app/components/oip-drugs/heart/results/les/Card.vue +340 -0
- package/layer/app/components/oip-drugs/heart/results/les/Drawer.vue +67 -0
- package/layer/app/components/oip-drugs/heart/results/les/IndicationDetail.vue +149 -0
- package/layer/app/components/oip-drugs/heart/results/les/Source.vue +20 -0
- package/layer/app/components/oip-drugs/heart/results/les/SourceTag.vue +39 -0
- package/layer/app/components/oip-drugs/heart/searchPage/DynamicSource.vue +29 -0
- package/layer/app/components/oip-drugs/medicam/market/Chart.vue +121 -0
- package/layer/app/components/oip-drugs/outpatient/ProductEvolution.vue +236 -0
- package/layer/app/components/oip-drugs/resources/Ansm.vue +75 -0
- package/layer/app/components/oip-drugs/resources/Association.vue +30 -0
- package/layer/app/components/oip-drugs/resources/Brand.vue +65 -0
- package/layer/app/components/oip-drugs/resources/Contribution.vue +216 -0
- package/layer/app/components/oip-drugs/resources/Hospital.vue +151 -0
- package/layer/app/components/oip-drugs/resources/Indication.vue +54 -0
- package/layer/app/components/oip-drugs/resources/Jo.vue +57 -0
- package/layer/app/components/oip-drugs/resources/Medicam.vue +140 -0
- package/layer/app/components/oip-drugs/resources/Mitm.vue +84 -0
- package/layer/app/components/oip-drugs/resources/NationalAssembly.vue +93 -0
- package/layer/app/components/oip-drugs/resources/Outpatient.vue +97 -0
- package/layer/app/components/oip-drugs/resources/Pnds.vue +24 -0
- package/layer/app/components/oip-drugs/resources/Price.vue +106 -0
- package/layer/app/components/oip-drugs/resources/Put.vue +143 -0
- package/layer/app/components/oip-science/appel-a-projet/Cards.vue +162 -0
- package/layer/app/components/oip-science/clinical-trials/Cards.vue +382 -0
- package/layer/app/components/oip-science/clinical-trials/CardsGlobal.vue +273 -0
- package/layer/app/components/oip-science/clinical-trials/LinkedList.vue +366 -0
- package/layer/app/components/oip-science/clinical-trials/PairBadge.vue +25 -0
- package/layer/app/components/oip-science/clinical-trials/PairBody.vue +182 -0
- package/layer/app/components/oip-science/clinical-trials/SitesDrawer.vue +95 -0
- package/layer/app/components/oip-science/common/Form.vue +73 -0
- package/layer/app/components/oip-science/common/MatchAvgTag.vue +283 -0
- package/layer/app/components/oip-science/common/NoData.vue +16 -0
- package/layer/app/components/oip-science/common/ScoreBadge.vue +35 -0
- package/layer/app/components/oip-science/common/Source.vue +31 -0
- package/layer/app/components/oip-science/common/TableTags.vue +279 -0
- package/layer/app/components/oip-science/common/Timeline.vue +104 -0
- package/layer/app/components/oip-science/common/ToolTips.vue +89 -0
- package/layer/app/components/oip-science/cross/publicationPubmed.vue +484 -0
- package/layer/app/components/oip-science/crossData/CrossDataLocations.vue +71 -0
- package/layer/app/components/oip-science/ctgov/Investigators.vue +148 -0
- package/layer/app/components/oip-science/ctgov/Locations.vue +102 -0
- package/layer/app/components/oip-science/ctgov/Publications.vue +52 -0
- package/layer/app/components/oip-science/ctgov/Sponsors.vue +66 -0
- package/layer/app/components/oip-science/ctgov/StudyDescription.vue +1136 -0
- package/layer/app/components/oip-science/ctgov-centers/ClinicalTrials.vue +40 -0
- package/layer/app/components/oip-science/ctgov-centers/Publications.vue +50 -0
- package/layer/app/components/oip-science/ctgov-investigators/ClinicalTrials.vue +43 -0
- package/layer/app/components/oip-science/ctgov-investigators/Informations.vue +171 -0
- package/layer/app/components/oip-science/ctgov-investigators/Publications.vue +44 -0
- package/layer/app/components/oip-science/elastic/DateRangeFilter.vue +209 -0
- package/layer/app/components/oip-science/elastic/Filter.vue +719 -0
- package/layer/app/components/oip-science/elastic/MatchExplain.vue +152 -0
- package/layer/app/components/oip-science/establishments/Cards.vue +183 -0
- package/layer/app/components/oip-science/establishments/ClinicalTrials.vue +62 -0
- package/layer/app/components/oip-science/establishments/Hcps.vue +227 -0
- package/layer/app/components/oip-science/establishments/Informations.vue +263 -0
- package/layer/app/components/oip-science/establishments/LinkedList.vue +153 -0
- package/layer/app/components/oip-science/establishments/MatchedLabels.vue +194 -0
- package/layer/app/components/oip-science/establishments/PublicFinance.vue +62 -0
- package/layer/app/components/oip-science/euct/Investigators.vue +125 -0
- package/layer/app/components/oip-science/euct/Locations.vue +102 -0
- package/layer/app/components/oip-science/euct/Products.vue +101 -0
- package/layer/app/components/oip-science/euct/Publications.vue +89 -0
- package/layer/app/components/oip-science/euct/Sponsors.vue +86 -0
- package/layer/app/components/oip-science/euct/StudyDescription.vue +1075 -0
- package/layer/app/components/oip-science/euct-centers/ClinicalTrials.vue +40 -0
- package/layer/app/components/oip-science/euct-centers/Publications.vue +55 -0
- package/layer/app/components/oip-science/euct-investigators/ClinicalTrials.vue +41 -0
- package/layer/app/components/oip-science/euct-investigators/Informations.vue +171 -0
- package/layer/app/components/oip-science/euct-investigators/Publications.vue +51 -0
- package/layer/app/components/oip-science/hcps/AppelsAProjet.vue +62 -0
- package/layer/app/components/oip-science/hcps/Cards.vue +237 -0
- package/layer/app/components/oip-science/hcps/ClinicalTrials.vue +78 -0
- package/layer/app/components/oip-science/hcps/Informations.vue +296 -0
- package/layer/app/components/oip-science/hcps/LinkedList.vue +153 -0
- package/layer/app/components/oip-science/hcps/MatchedLabels.vue +207 -0
- package/layer/app/components/oip-science/hcps/Pubmed.vue +128 -0
- package/layer/app/components/oip-science/local/SliderTabs.vue +99 -0
- package/layer/app/components/oip-science/local/SliderTabsFull.vue +135 -0
- package/layer/app/components/oip-science/pubmed/Authors.vue +128 -0
- package/layer/app/components/oip-science/pubmed/Cards.vue +417 -0
- package/layer/app/components/oip-science/pubmed/ClinicalTrials.vue +71 -0
- package/layer/app/components/oip-science/resources/Phrc.vue +39 -0
- package/layer/app/composables/useOipChapterScroll.ts +43 -0
- package/layer/app/composables/useOipResourceHooks.ts +27 -0
- package/layer/app/composables/useOipResourceNav.ts +66 -0
- package/layer/app/composables/useOipResourceViews.ts +117 -0
- package/layer/app/composables/useOipResources.ts +83 -0
- package/layer/app/oip-drugs/PORTING.md +120 -0
- package/layer/app/oip-drugs/composables/useAssociationDetail.ts +220 -0
- package/layer/app/oip-drugs/composables/useAssociationExport.ts +63 -0
- package/layer/app/oip-drugs/composables/useAssociationLookup.ts +105 -0
- package/layer/app/oip-drugs/composables/useCardPrefOptions.ts +201 -0
- package/layer/app/oip-drugs/composables/useCurrency.ts +121 -0
- package/layer/app/oip-drugs/composables/useDirectusSources.ts +224 -0
- package/layer/app/oip-drugs/composables/useHeartBrandMatches.ts +112 -0
- package/layer/app/oip-drugs/composables/useHeartDetailSearches.ts +479 -0
- package/layer/app/oip-drugs/composables/useHeartIntlApprovals.ts +661 -0
- package/layer/app/oip-drugs/composables/useHeartLesIndication.ts +168 -0
- package/layer/app/oip-drugs/composables/useHeartMatchExplain.ts +231 -0
- package/layer/app/oip-drugs/composables/useHeartReports.ts +239 -0
- package/layer/app/oip-drugs/composables/useHeartSearchFields.ts +233 -0
- package/layer/app/oip-drugs/composables/useMergeByKey.ts +23 -0
- package/layer/app/oip-drugs/composables/useOipDrugsElastic.ts +621 -0
- package/layer/app/oip-drugs/composables/usePageEmpty.ts +58 -0
- package/layer/app/oip-drugs/composables/v-useSalesSource.ts +64 -0
- package/layer/app/oip-drugs/drugs.css +500 -0
- package/layer/app/oip-drugs/esIndices.ts +322 -0
- package/layer/app/oip-drugs/kinds.ts +264 -0
- package/layer/app/oip-drugs/locales/en.json +1571 -0
- package/layer/app/oip-drugs/locales/fr.json +1571 -0
- package/layer/app/oip-drugs/monographs/blincyto-has.vue +289 -0
- package/layer/app/oip-drugs/monographs/darzalex-has.vue +276 -0
- package/layer/app/oip-drugs/monographs/dupixent-has.vue +216 -0
- package/layer/app/oip-drugs/monographs/enhertu-has.vue +280 -0
- package/layer/app/oip-drugs/monographs/has-sein-2023.vue +1649 -0
- package/layer/app/oip-drugs/monographs/ibrance-has.vue +277 -0
- package/layer/app/oip-drugs/monographs/imfinzi-has.vue +293 -0
- package/layer/app/oip-drugs/monographs/keytruda-has.vue +334 -0
- package/layer/app/oip-drugs/monographs/kisqali-has.vue +201 -0
- package/layer/app/oip-drugs/monographs/opdivo-has.vue +231 -0
- package/layer/app/oip-drugs/monographs/ozempic-has.vue +186 -0
- package/layer/app/oip-drugs/monographs/tecentriq-has.vue +312 -0
- package/layer/app/oip-drugs/monographs/yescarta-has.vue +272 -0
- package/layer/app/oip-drugs/types/elastic.ts +113 -0
- package/layer/app/oip-drugs/types/es/agencies.ts +152 -0
- package/layer/app/oip-drugs/types/es/directory.ts +54 -0
- package/layer/app/oip-drugs/types/es/docByIndex.ts +151 -0
- package/layer/app/oip-drugs/types/es/openmedic.ts +67 -0
- package/layer/app/oip-drugs/types/es/outpatient.ts +71 -0
- package/layer/app/oip-drugs/types/es/portal.ts +130 -0
- package/layer/app/oip-drugs/types/es/retail.ts +107 -0
- package/layer/app/oip-drugs/types/es/statistics.ts +29 -0
- package/layer/app/oip-drugs/types/heart/CurrencyRate.ts +16 -0
- package/layer/app/oip-drugs/types/heart/ansm.ts +106 -0
- package/layer/app/oip-drugs/types/heart/tag.ts +29 -0
- package/layer/app/oip-drugs/utils/associationFields.ts +118 -0
- package/layer/app/oip-drugs/utils/contributions.ts +36 -0
- package/layer/app/oip-drugs/utils/countryMapping.ts +387 -0
- package/layer/app/oip-drugs/utils/esHighlight.ts +21 -0
- package/layer/app/oip-drugs/utils/exportSheets.ts +57 -0
- package/layer/app/oip-drugs/utils/heartIntlPrice.ts +228 -0
- package/layer/app/oip-drugs/utils/heartStyleAndFormat.ts +262 -0
- package/layer/app/oip-drugs/utils/heartStyleAnsm.ts +71 -0
- package/layer/app/oip-drugs/utils/latestDate.ts +31 -0
- package/layer/app/oip-drugs/utils/p-heartCardTags.ts +53 -0
- package/layer/app/oip-drugs/utils/priceExport.ts +269 -0
- package/layer/app/oip-drugs/utils/q-hasRatings.ts +43 -0
- package/layer/app/oip-drugs/utils/s-openCard.ts +20 -0
- package/layer/app/oip-drugs/utils/sourceUuids.ts +155 -0
- package/layer/app/oip-drugs/utils/styleAndFormat.ts +40 -0
- package/layer/app/oip-drugs/utils/v-salesSeries.ts +156 -0
- package/layer/app/oip-drugs/utils/v-scopeTags.ts +23 -0
- package/layer/app/oip-science/PORTING.md +93 -0
- package/layer/app/oip-science/assets/smartSelection.json +428 -0
- package/layer/app/oip-science/composables/useAuthorMatching.ts +342 -0
- package/layer/app/oip-science/composables/useCTCenter.ts +107 -0
- package/layer/app/oip-science/composables/useCTInvestigators.ts +107 -0
- package/layer/app/oip-science/composables/useEstablishmentCrossData.ts +139 -0
- package/layer/app/oip-science/composables/useEstablishmentMatching.ts +176 -0
- package/layer/app/oip-science/composables/useEuct.ts +418 -0
- package/layer/app/oip-science/composables/useEuctCenter.ts +107 -0
- package/layer/app/oip-science/composables/useEuctInvestigators.ts +107 -0
- package/layer/app/oip-science/composables/useGlobalClinicalTrials.ts +624 -0
- package/layer/app/oip-science/composables/useLinkedTrials.ts +83 -0
- package/layer/app/oip-science/composables/useOipElastic.ts +658 -0
- package/layer/app/oip-science/composables/useOipSearchExplain.ts +166 -0
- package/layer/app/oip-science/composables/usePubmed.ts +487 -0
- package/layer/app/oip-science/composables/useSearchFields.ts +153 -0
- package/layer/app/oip-science/indexConfig.json +794 -0
- package/layer/app/oip-science/locales/en.json +483 -0
- package/layer/app/oip-science/locales/fr.json +483 -0
- package/layer/app/oip-science/science.css +1182 -0
- package/layer/app/oip-science/utils/a-openCard.ts +23 -0
- package/layer/app/oip-science/utils/b-openCard.ts +35 -0
- package/layer/app/oip-science/utils/c-filterContext.ts +81 -0
- package/layer/app/oip-science/utils/f-types.ts +241 -0
- package/layer/app/oip-science/utils/frDiseaseTerms.ts +269 -0
- package/layer/app/oip-science/utils/matchMethod.ts +53 -0
- package/layer/app/oip-science/utils/sliderTabs.ts +13 -0
- package/layer/app/oip-science/utils/sources.ts +15 -0
- package/layer/app/oip-science/utils/styleAndFormat.ts +608 -0
- package/layer/app/oip-science/utils/toPresets.ts +7 -0
- package/layer/app/oip-science/utils/valueTable.ts +126 -0
- package/layer/app/plugins/oip-science-i18n.ts +17 -0
- package/layer/app/utils/oipFicheRoutes.ts +118 -0
- package/layer/app/utils/oipResources.ts +440 -0
- package/layer/nuxt.config.ts +22 -2
- package/layer/public/reports/blincyto-has.html +292 -0
- package/layer/public/reports/blincyto-has.pdf +0 -0
- package/layer/public/reports/darzalex-has.html +279 -0
- package/layer/public/reports/darzalex-has.pdf +0 -0
- package/layer/public/reports/dupixent-has.html +217 -0
- package/layer/public/reports/dupixent-has.pdf +0 -0
- package/layer/public/reports/enhertu-has.html +283 -0
- package/layer/public/reports/enhertu-has.pdf +0 -0
- package/layer/public/reports/has-sein-2023.html +1639 -0
- package/layer/public/reports/ibrance-has.html +280 -0
- package/layer/public/reports/ibrance-has.pdf +0 -0
- package/layer/public/reports/imfinzi-has.html +297 -0
- package/layer/public/reports/imfinzi-has.pdf +0 -0
- package/layer/public/reports/keytruda-has.html +337 -0
- package/layer/public/reports/keytruda-has.pdf +0 -0
- package/layer/public/reports/kisqali-has.html +202 -0
- package/layer/public/reports/kisqali-has.pdf +0 -0
- package/layer/public/reports/opdivo-has.html +232 -0
- package/layer/public/reports/opdivo-has.pdf +0 -0
- package/layer/public/reports/ozempic-has.html +188 -0
- package/layer/public/reports/ozempic-has.pdf +0 -0
- package/layer/public/reports/tecentriq-has.html +315 -0
- package/layer/public/reports/tecentriq-has.pdf +0 -0
- package/layer/public/reports/yescarta-has.html +275 -0
- package/layer/public/reports/yescarta-has.pdf +0 -0
- package/layer/server/api/oip/es.post.ts +99 -0
- package/layer/server/api/oip/pdf.get.ts +70 -0
- package/layer/server/api/oip/reports.get.ts +98 -0
- package/layer/server/api/oip/resource-types.get.ts +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Prix d'une marque, presentation par presentation.
|
|
3
|
+
|
|
4
|
+
Le tableau precedent alignait huit colonnes sur une ligne par presentation :
|
|
5
|
+
on y lisait des nombres, pas des prix. Ici l'entete resume la marque (combien
|
|
6
|
+
de presentations tarifees, la fourchette, la derniere mise a jour, la
|
|
7
|
+
tendance) et chaque presentation devient une carte : son PFHT en grand, sa
|
|
8
|
+
derniere variation, puis le prix public et l'ecart avec le PFHT.
|
|
9
|
+
-->
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { rmTooltipTrigger } from "@tekkare/romulus/utils/chart";
|
|
12
|
+
import { HEART_SOURCE_UUIDS } from "../../../../../oip-drugs/utils/sourceUuids";
|
|
13
|
+
import { heartCodeLabel, heartPriceChannel } from "../../../../../oip-drugs/utils/heartStyleAndFormat";
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
/** Les entrees de prix de la marque (une par presentation). */
|
|
17
|
+
product: any[];
|
|
18
|
+
/** Nom de la marque, pour la ligne de portee. */
|
|
19
|
+
brand?: string;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const { locale } = useI18n();
|
|
23
|
+
|
|
24
|
+
const money = (value: number | null | undefined) =>
|
|
25
|
+
typeof value === "number"
|
|
26
|
+
? `${value.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
|
|
27
|
+
: "-";
|
|
28
|
+
|
|
29
|
+
const percent = (value: number | null | undefined) =>
|
|
30
|
+
typeof value === "number"
|
|
31
|
+
? `${value > 0 ? "+" : ""}${value.toLocaleString("fr-FR", { maximumFractionDigits: 1 })} %`
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
/** Derniere variation du PFHT, en euros (price_type_id 1 = prix fabricant). */
|
|
35
|
+
function factoryVariation(entry: any): number | null {
|
|
36
|
+
const history = (entry?.changed_history || [])
|
|
37
|
+
// Le champ de l'index est `status_changed` : ecrit `status_change`, la
|
|
38
|
+
// comparaison portait sur `undefined` et laissait passer les lignes sans
|
|
39
|
+
// changement, qui masquaient la derniere vraie variation.
|
|
40
|
+
.filter((h: any) => h.price_type_id === 1 && h.status_changed !== "N")
|
|
41
|
+
.sort(
|
|
42
|
+
(a: any, b: any) =>
|
|
43
|
+
new Date(b.application_date).getTime() - new Date(a.application_date).getTime(),
|
|
44
|
+
);
|
|
45
|
+
return history[0]?.price_variation ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Le tarif applicable de chaque presentation.
|
|
50
|
+
*
|
|
51
|
+
* `reimbursement_history` n'est pas trie : prendre la premiere entree ferait
|
|
52
|
+
* remonter un ancien retrait sans prix a la place du tarif courant.
|
|
53
|
+
*/
|
|
54
|
+
/** La date du tarif le plus recent de la marque, pour les pieds de la carte. */
|
|
55
|
+
const lastPriceDates = computed(() =>
|
|
56
|
+
(props.product || []).flatMap((entry: any) =>
|
|
57
|
+
(entry?.reimbursement_history ?? []).map((h: any) => h?.application_date),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Date d'inscription la plus ancienne, tous circuits confondus.
|
|
63
|
+
*
|
|
64
|
+
* L'index ne remplit `commercialization_date` que sur les presentations de
|
|
65
|
+
* ville : aucun des 3 551 documents UCD ne la porte, et une marque
|
|
66
|
+
* hospitaliere comme OPDIVO n'affichait donc aucune date. Cote hopital, la
|
|
67
|
+
* mise a disposition se lit dans `homologation_history` (l'inscription au
|
|
68
|
+
* circuit : agrement collectivites, retrocession). Ce n'est pas la meme notion
|
|
69
|
+
* qu'une commercialisation : elle porte donc son propre libelle.
|
|
70
|
+
*/
|
|
71
|
+
function earliestListing(entry: any) {
|
|
72
|
+
const dates = (Array.isArray(entry?.homologation_history) ? entry.homologation_history : [])
|
|
73
|
+
.map((h: any) => h?.start_date)
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.sort();
|
|
76
|
+
return dates[0] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rows = computed(() =>
|
|
80
|
+
(props.product || [])
|
|
81
|
+
.map((entry: any) => {
|
|
82
|
+
const history = Array.isArray(entry?.reimbursement_history)
|
|
83
|
+
? [...entry.reimbursement_history].sort(
|
|
84
|
+
(a: any, b: any) =>
|
|
85
|
+
new Date(b.application_date).getTime() - new Date(a.application_date).getTime(),
|
|
86
|
+
)
|
|
87
|
+
: [];
|
|
88
|
+
const current = history[0] ?? {};
|
|
89
|
+
const factory = current.factory_price ?? null;
|
|
90
|
+
const variation = factoryVariation(entry);
|
|
91
|
+
const previous =
|
|
92
|
+
typeof factory === "number" && typeof variation === "number"
|
|
93
|
+
? factory - variation
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
key: `${entry?.local_drug_id ?? ""}-${entry?.label ?? ""}`,
|
|
98
|
+
// Le document complet : le tiroir y lit tout ce que la carte resume.
|
|
99
|
+
source: entry,
|
|
100
|
+
label: entry?.label ?? entry?.local_drug_id ?? "-",
|
|
101
|
+
code: entry?.local_drug_id ?? null,
|
|
102
|
+
// CIP en ville, UCD a l'hopital : le meme champ porte les deux.
|
|
103
|
+
codeLabel: heartCodeLabel(entry?.distribution_channel),
|
|
104
|
+
manufacturer: entry?.manufacturer ?? null,
|
|
105
|
+
// Ville ou hopital : seul `distribution_channel` le dit.
|
|
106
|
+
channel: heartPriceChannel(entry?.distribution_channel, locale.value),
|
|
107
|
+
// L'index ecrit "-" quand la date manque, et ne porte commercialisation
|
|
108
|
+
// ni date que sur les presentations de ville.
|
|
109
|
+
marketedOn:
|
|
110
|
+
entry?.commercialization_date && entry.commercialization_date !== "-"
|
|
111
|
+
? entry.commercialization_date
|
|
112
|
+
: null,
|
|
113
|
+
// Repli hopital : faute de commercialisation, la date d'inscription.
|
|
114
|
+
listedOn:
|
|
115
|
+
entry?.commercialization_date && entry.commercialization_date !== "-"
|
|
116
|
+
? null
|
|
117
|
+
: earliestListing(entry),
|
|
118
|
+
notMarketed: entry?.commercialization_status === "Non commercialisée",
|
|
119
|
+
date: current.application_date ?? null,
|
|
120
|
+
factory,
|
|
121
|
+
publicPrice: current.public_price ?? null,
|
|
122
|
+
// Taux de prise en charge, ecrit en chaine (« 0.65 ») et seulement en
|
|
123
|
+
// ville : aucune presentation UCD ne le porte, le rendu le passe.
|
|
124
|
+
reimbursementRate:
|
|
125
|
+
current.reimbursement_rate !== undefined &&
|
|
126
|
+
current.reimbursement_rate !== null &&
|
|
127
|
+
current.reimbursement_rate !== "" &&
|
|
128
|
+
!Number.isNaN(Number(current.reimbursement_rate))
|
|
129
|
+
? Number(current.reimbursement_rate) * 100
|
|
130
|
+
: null,
|
|
131
|
+
variation,
|
|
132
|
+
previous,
|
|
133
|
+
// Le pourcentage se deduit du prix precedent : l'index ne le porte pas.
|
|
134
|
+
variationPct:
|
|
135
|
+
typeof variation === "number" && previous
|
|
136
|
+
? (variation / previous) * 100
|
|
137
|
+
: null,
|
|
138
|
+
// Ce que la distribution ajoute au prix fabricant.
|
|
139
|
+
margin:
|
|
140
|
+
typeof current.public_price === "number" && typeof factory === "number"
|
|
141
|
+
? current.public_price - factory
|
|
142
|
+
: null,
|
|
143
|
+
marginPct:
|
|
144
|
+
typeof current.public_price === "number" && factory
|
|
145
|
+
? ((current.public_price - factory) / factory) * 100
|
|
146
|
+
: null,
|
|
147
|
+
};
|
|
148
|
+
})
|
|
149
|
+
.filter((row: any) => row.factory !== null || row.publicPrice !== null)
|
|
150
|
+
.sort(byDosage),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Dosage lu dans le libelle (« 150 MG/ML », « 30MG », « 0,5 G »), ramene en
|
|
155
|
+
* milligrammes. Les presentations d'une marque arrivaient dans l'ordre de
|
|
156
|
+
* l'index, 150 mg et 30 mg meles : on les range par dosage, puis ville avant
|
|
157
|
+
* hopital, puis par libelle, pour que les memes dosages se lisent ensemble.
|
|
158
|
+
*/
|
|
159
|
+
const DOSE_UNITS: Record<string, number> = { MCG: 0.001, µG: 0.001, UG: 0.001, MG: 1, G: 1000 };
|
|
160
|
+
function dosageOf(label: string): number {
|
|
161
|
+
const match = String(label ?? "").toUpperCase().match(/(\d+(?:[.,]\d+)?)\s*(MCG|µG|UG|MG|G)\b/);
|
|
162
|
+
if (!match) return Number.POSITIVE_INFINITY;
|
|
163
|
+
return parseFloat(match[1]!.replace(",", ".")) * (DOSE_UNITS[match[2]!] ?? 1);
|
|
164
|
+
}
|
|
165
|
+
function byDosage(a: any, b: any): number {
|
|
166
|
+
const dose = dosageOf(a.label) - dosageOf(b.label);
|
|
167
|
+
if (dose !== 0 && !Number.isNaN(dose)) return dose;
|
|
168
|
+
const channel = String(a.channel?.key ?? "").localeCompare(String(b.channel?.key ?? ""));
|
|
169
|
+
if (channel !== 0) return -channel;
|
|
170
|
+
return String(a.label).localeCompare(String(b.label));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Historique du PFHT, presentation par presentation.
|
|
175
|
+
*
|
|
176
|
+
* `reimbursement_history` porte un tarif par date d'application : c'est deja la
|
|
177
|
+
* serie, il suffit de la remettre dans l'ordre. Un prix tient jusqu'au tarif
|
|
178
|
+
* suivant, d'où le trace en marches plutot qu'en droites : une interpolation
|
|
179
|
+
* laisserait croire a une baisse continue entre deux arretes.
|
|
180
|
+
*/
|
|
181
|
+
const SERIES_COLORS = ["#2017ad", "#1dacfc", "#a17def", "#fd9147", "#22c55e"];
|
|
182
|
+
|
|
183
|
+
const series = computed(() =>
|
|
184
|
+
(props.product || [])
|
|
185
|
+
.map((entry: any, index: number) => {
|
|
186
|
+
const points = (Array.isArray(entry?.reimbursement_history) ? entry.reimbursement_history : [])
|
|
187
|
+
.filter((h: any) => typeof h?.factory_price === "number" && h?.application_date)
|
|
188
|
+
.map((h: any) => ({ date: new Date(h.application_date).getTime(), price: h.factory_price }))
|
|
189
|
+
.sort((a: any, b: any) => a.date - b.date);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
label: entry?.label ?? entry?.local_drug_id ?? "-",
|
|
193
|
+
color: SERIES_COLORS[index % SERIES_COLORS.length],
|
|
194
|
+
points,
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
.filter((line: any) => line.points.length > 0),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Tous les tarifs publies de la marque, une ligne par tarif.
|
|
202
|
+
*
|
|
203
|
+
* Le graphe dit la tendance, il ne donne aucun chiffre : pour lire « combien,
|
|
204
|
+
* quand, sur quelle presentation », il fallait ouvrir le tiroir d'une
|
|
205
|
+
* presentation apres l'autre et comparer de tete. Le tableau met les memes
|
|
206
|
+
* lignes que l'onglet « Historique tarifaire » du tiroir, mais pour toutes les
|
|
207
|
+
* presentations a la fois, sous le graphe qu'il detaille.
|
|
208
|
+
*
|
|
209
|
+
* L'evolution se calcule DANS une presentation : elle compare au tarif
|
|
210
|
+
* precedent de la meme presentation, jamais a la ligne du dessus, qui peut
|
|
211
|
+
* appartenir a une autre. `changed_history` n'est pas utilise ici pour la meme
|
|
212
|
+
* raison que dans le tiroir : il ne decoupe ni aux memes dates ni aux memes
|
|
213
|
+
* circuits que `reimbursement_history`, et donnerait un ecart qui ne relie
|
|
214
|
+
* aucun des deux PFHT affiches.
|
|
215
|
+
*/
|
|
216
|
+
/**
|
|
217
|
+
* L'index ecrit « nan » (et parfois « - ») la ou la valeur manque : ce sont
|
|
218
|
+
* des trous du fichier source remontes tels quels, pas des libelles. Ecrits
|
|
219
|
+
* dans une cellule, ils se lisent comme une donnee.
|
|
220
|
+
*/
|
|
221
|
+
function clean(value: unknown): string | null {
|
|
222
|
+
const text = String(value ?? "").trim();
|
|
223
|
+
if (!text || text === "-" || text.toLowerCase() === "nan") return null;
|
|
224
|
+
return text;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** La meme chose, prete a etre affichee : un tiret cadratin a la place du vide. */
|
|
228
|
+
function text(value: unknown): string {
|
|
229
|
+
return clean(value) ?? "-";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const historyRows = computed(() =>
|
|
233
|
+
(props.product || [])
|
|
234
|
+
.flatMap((entry: any) => {
|
|
235
|
+
const history = (
|
|
236
|
+
Array.isArray(entry?.reimbursement_history) ? [...entry.reimbursement_history] : []
|
|
237
|
+
).sort(
|
|
238
|
+
(a: any, b: any) =>
|
|
239
|
+
new Date(b?.application_date).getTime() - new Date(a?.application_date).getTime(),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return history.map((row: any, index: number) => {
|
|
243
|
+
const price = typeof row?.factory_price === "number" ? row.factory_price : null;
|
|
244
|
+
const previous =
|
|
245
|
+
history
|
|
246
|
+
.slice(index + 1)
|
|
247
|
+
.find((older: any) => typeof older?.factory_price === "number")?.factory_price ?? null;
|
|
248
|
+
const variation = price !== null && previous !== null ? price - previous : null;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
key: `${entry?.local_drug_id ?? ""}-${row?.application_date ?? ""}-${index}`,
|
|
252
|
+
presentation: entry?.label ?? entry?.local_drug_id ?? "-",
|
|
253
|
+
code: entry?.local_drug_id ?? null,
|
|
254
|
+
// Le tri de la colonne porte sur cette valeur : une date ISO se
|
|
255
|
+
// compare comme du texte, une date affichee ne se compare pas.
|
|
256
|
+
date: row?.application_date ?? "",
|
|
257
|
+
factory: price,
|
|
258
|
+
variation,
|
|
259
|
+
variationPct: variation !== null && previous ? (variation / previous) * 100 : null,
|
|
260
|
+
publicPrice: typeof row?.public_price === "number" ? row.public_price : null,
|
|
261
|
+
marge: typeof row?.marge === "number" ? row.marge : null,
|
|
262
|
+
channel: text(row?.distribution_type),
|
|
263
|
+
documentarySource: text(row?.documentary_source),
|
|
264
|
+
sourceDate: clean(row?.source_date),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
})
|
|
268
|
+
// Du plus recent au plus ancien, toutes presentations melangees : c'est
|
|
269
|
+
// l'ordre dans lequel on lit un historique.
|
|
270
|
+
.sort((a: any, b: any) => String(b.date).localeCompare(String(a.date))),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* La marge de retrocession n'existe que sur les tarifs hopital. Comme dans le
|
|
275
|
+
* tiroir, la colonne disparait quand aucune ligne n'en porte.
|
|
276
|
+
*/
|
|
277
|
+
const hasMarge = computed(() => historyRows.value.some((row: any) => row.marge !== null));
|
|
278
|
+
|
|
279
|
+
const historyColumns = computed(() => {
|
|
280
|
+
const en = locale.value === "en";
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
key: "presentation",
|
|
284
|
+
label: en ? "Presentation" : "Présentation",
|
|
285
|
+
sortable: true,
|
|
286
|
+
rowHeader: true,
|
|
287
|
+
minWidth: "220px",
|
|
288
|
+
},
|
|
289
|
+
{ key: "date", label: en ? "Applies from" : "Applicable au", sortable: true, width: "120px" },
|
|
290
|
+
{ key: "factory", label: "PFHT", numeric: true, sortable: true },
|
|
291
|
+
{ key: "variation", label: en ? "Change" : "Évolution", numeric: true, sortable: true },
|
|
292
|
+
{ key: "publicPrice", label: en ? "Public price" : "Prix public", numeric: true, sortable: true },
|
|
293
|
+
...(hasMarge.value
|
|
294
|
+
? [{ key: "marge", label: en ? "Margin" : "Marge", numeric: true, sortable: true }]
|
|
295
|
+
: []),
|
|
296
|
+
{ key: "channel", label: en ? "Channel" : "Circuit", sortable: true },
|
|
297
|
+
{ key: "documentarySource", label: en ? "Source" : "Source" },
|
|
298
|
+
];
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/** Geometrie du graphe : bornes, echelles et chemins en marches.
|
|
302
|
+
|
|
303
|
+
La largeur suit celle de la carte pour qu'une unite de viewBox vaille un
|
|
304
|
+
pixel : sinon le SVG est agrandi par la mise en page et les libelles des
|
|
305
|
+
axes, ecrits en unites de viewBox, arrivent deux fois trop gros. */
|
|
306
|
+
const chartWrap = ref<HTMLElement | null>(null);
|
|
307
|
+
const chartW = ref(720);
|
|
308
|
+
|
|
309
|
+
const CHART = computed(() => ({
|
|
310
|
+
w: chartW.value,
|
|
311
|
+
h: 240,
|
|
312
|
+
top: 16,
|
|
313
|
+
right: 16,
|
|
314
|
+
bottom: 28,
|
|
315
|
+
left: 56,
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
let chartObserver: ResizeObserver | null = null;
|
|
319
|
+
|
|
320
|
+
onMounted(() => {
|
|
321
|
+
if (!chartWrap.value) return;
|
|
322
|
+
chartObserver = new ResizeObserver(([entry]) => {
|
|
323
|
+
const width = entry?.contentRect?.width;
|
|
324
|
+
if (width) chartW.value = Math.round(width);
|
|
325
|
+
});
|
|
326
|
+
chartObserver.observe(chartWrap.value);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
onBeforeUnmount(() => chartObserver?.disconnect());
|
|
330
|
+
|
|
331
|
+
const chart = computed(() => {
|
|
332
|
+
const points = series.value.flatMap((line: any) => line.points);
|
|
333
|
+
if (points.length < 2) return null;
|
|
334
|
+
|
|
335
|
+
const dates = points.map((p: any) => p.date);
|
|
336
|
+
const prices = points.map((p: any) => p.price);
|
|
337
|
+
const minDate = Math.min(...dates);
|
|
338
|
+
// La derniere marche court jusqu'a aujourd'hui : le tarif est toujours en
|
|
339
|
+
// vigueur, la ligne ne doit pas s'arreter a sa date de prise d'effet.
|
|
340
|
+
const maxDate = Math.max(Math.max(...dates), Date.now());
|
|
341
|
+
const maxPrice = Math.max(...prices);
|
|
342
|
+
// L'echelle part de zero : tronquer la base amplifie visuellement des ecarts
|
|
343
|
+
// de quelques pour cent et fait passer une baisse de 6 % pour un effondrement.
|
|
344
|
+
const low = 0;
|
|
345
|
+
const high = maxPrice * 1.15;
|
|
346
|
+
|
|
347
|
+
const x = (date: number) =>
|
|
348
|
+
CHART.value.left +
|
|
349
|
+
((date - minDate) / (maxDate - minDate || 1)) * (CHART.value.w - CHART.value.left - CHART.value.right);
|
|
350
|
+
const y = (price: number) =>
|
|
351
|
+
CHART.value.top + (1 - (price - low) / (high - low || 1)) * (CHART.value.h - CHART.value.top - CHART.value.bottom);
|
|
352
|
+
|
|
353
|
+
const lines = series.value.map((line: any) => {
|
|
354
|
+
const steps: string[] = [];
|
|
355
|
+
line.points.forEach((point: any, index: number) => {
|
|
356
|
+
const px = x(point.date);
|
|
357
|
+
const py = y(point.price);
|
|
358
|
+
if (index === 0) steps.push(`M ${px} ${py}`);
|
|
359
|
+
else steps.push(`H ${px}`, `V ${py}`);
|
|
360
|
+
});
|
|
361
|
+
steps.push(`H ${x(maxDate)}`);
|
|
362
|
+
return {
|
|
363
|
+
...line,
|
|
364
|
+
path: steps.join(" "),
|
|
365
|
+
dots: line.points.map((point: any) => ({ ...point, cx: x(point.date), cy: y(point.price) })),
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Trois reperes de prix, et une annee tous les deux ans au plus dense.
|
|
370
|
+
const priceTicks = [low, (low + high) / 2, high].map((value) => ({ value, y: y(value) }));
|
|
371
|
+
const years: { label: string; x: number }[] = [];
|
|
372
|
+
const firstYear = new Date(minDate).getFullYear();
|
|
373
|
+
const lastYear = new Date(maxDate).getFullYear();
|
|
374
|
+
const stride = Math.max(1, Math.ceil((lastYear - firstYear) / 6));
|
|
375
|
+
for (let year = firstYear; year <= lastYear; year += stride) {
|
|
376
|
+
years.push({ label: String(year), x: x(new Date(`${year}-01-01`).getTime()) });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return { lines, priceTicks, years, minDate, maxDate };
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Survol du graphe : on lit la date sous le curseur et, pour chaque
|
|
384
|
+
* presentation, le dernier tarif entre en vigueur avant cette date, le prix
|
|
385
|
+
* qui s'appliquait ce jour-la, pas le point le plus proche.
|
|
386
|
+
*
|
|
387
|
+
* Sous dix courbes, l'infobulle ne garde que la courbe la plus proche du
|
|
388
|
+
* curseur : le lecteur qui pose le pointeur sur un tarif veut ce tarif, pas
|
|
389
|
+
* la liste de toute la gamme. Meme regle que les graphes Romulus
|
|
390
|
+
* (`rmTooltipTrigger`). Au-dela, les courbes se serrent au point qu'on ne
|
|
391
|
+
* vise plus rien : la colonne entiere reprend la main.
|
|
392
|
+
*/
|
|
393
|
+
const hover = ref<{
|
|
394
|
+
x: number;
|
|
395
|
+
date: number;
|
|
396
|
+
items: { label: string; color: string; price: number }[];
|
|
397
|
+
} | null>(null);
|
|
398
|
+
|
|
399
|
+
function onChartMove(event: MouseEvent) {
|
|
400
|
+
const box = (event.currentTarget as SVGElement).getBoundingClientRect();
|
|
401
|
+
if (!chart.value || !box.width) return;
|
|
402
|
+
|
|
403
|
+
// Le SVG est mis a l'echelle par la mise en page : on repasse en unites de
|
|
404
|
+
// viewBox avant de convertir en date.
|
|
405
|
+
const vx = ((event.clientX - box.left) / box.width) * CHART.value.w;
|
|
406
|
+
const ratio =
|
|
407
|
+
(vx - CHART.value.left) / (CHART.value.w - CHART.value.left - CHART.value.right);
|
|
408
|
+
const clamped = Math.min(1, Math.max(0, ratio));
|
|
409
|
+
const date = chart.value.minDate + clamped * (chart.value.maxDate - chart.value.minDate);
|
|
410
|
+
|
|
411
|
+
// Les points du trace portent deja leur ordonnee : de quoi mesurer la
|
|
412
|
+
// distance verticale au curseur sans rejouer l'echelle des prix.
|
|
413
|
+
const vy = ((event.clientY - box.top) / box.height) * CHART.value.h;
|
|
414
|
+
|
|
415
|
+
const items = chart.value.lines
|
|
416
|
+
.map((line: any) => {
|
|
417
|
+
const applicable = [...line.dots]
|
|
418
|
+
.filter((dot: any) => dot.date <= date)
|
|
419
|
+
.sort((a: any, b: any) => b.date - a.date)[0];
|
|
420
|
+
return applicable
|
|
421
|
+
? { label: line.label, color: line.color, price: applicable.price, y: applicable.cy }
|
|
422
|
+
: null;
|
|
423
|
+
})
|
|
424
|
+
.filter(Boolean) as { label: string; color: string; price: number; y: number }[];
|
|
425
|
+
|
|
426
|
+
const single =
|
|
427
|
+
items.length > 1 && rmTooltipTrigger(items.length) === "item"
|
|
428
|
+
? [items.reduce((best, item) => (Math.abs(item.y - vy) < Math.abs(best.y - vy) ? item : best))]
|
|
429
|
+
: items;
|
|
430
|
+
|
|
431
|
+
hover.value = items.length
|
|
432
|
+
? {
|
|
433
|
+
x: CHART.value.left + clamped * (CHART.value.w - CHART.value.left - CHART.value.right),
|
|
434
|
+
date,
|
|
435
|
+
items: single,
|
|
436
|
+
}
|
|
437
|
+
: null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const priced = computed(() => rows.value.filter((row: any) => row.factory !== null));
|
|
441
|
+
|
|
442
|
+
const range = computed(() => {
|
|
443
|
+
const values = priced.value.map((row: any) => row.factory as number);
|
|
444
|
+
if (!values.length) return null;
|
|
445
|
+
return { min: Math.min(...values), max: Math.max(...values) };
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const lastUpdate = computed(() => {
|
|
449
|
+
const dates = rows.value
|
|
450
|
+
.map((row: any) => row.date)
|
|
451
|
+
.filter(Boolean)
|
|
452
|
+
.sort((a: string, b: string) => new Date(b).getTime() - new Date(a).getTime());
|
|
453
|
+
return dates[0] ?? null;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
/** Tendance recente : le sens des dernieres variations de PFHT. */
|
|
457
|
+
/** Presentation ouverte dans le tiroir de detail. */
|
|
458
|
+
const drawerProduct = ref<any | null>(null);
|
|
459
|
+
|
|
460
|
+
const trend = computed(() => {
|
|
461
|
+
const variations = rows.value
|
|
462
|
+
.map((row: any) => row.variation)
|
|
463
|
+
.filter((value: any) => typeof value === "number" && value !== 0);
|
|
464
|
+
if (!variations.length) return { key: "flat", label: locale.value === "en" ? "Stable" : "Stable" };
|
|
465
|
+
const total = variations.reduce((sum: number, value: number) => sum + value, 0);
|
|
466
|
+
if (total < 0) return { key: "down", label: locale.value === "en" ? "Falling" : "En baisse" };
|
|
467
|
+
if (total > 0) return { key: "up", label: locale.value === "en" ? "Rising" : "En hausse" };
|
|
468
|
+
return { key: "flat", label: "Stable" };
|
|
469
|
+
});
|
|
470
|
+
</script>
|
|
471
|
+
|
|
472
|
+
<template>
|
|
473
|
+
<section class="pv">
|
|
474
|
+
<!-- Certaines marques n'ont aucun tarif publie : un bandeau de zeros suivi
|
|
475
|
+
de cartes vides donnait l'impression d'un chargement rate. On le dit
|
|
476
|
+
une fois, et on s'arrete la. -->
|
|
477
|
+
<template v-if="!rows.length">
|
|
478
|
+
<p class="pv_none">
|
|
479
|
+
<RmIcon name="Info" :size="16" />
|
|
480
|
+
{{
|
|
481
|
+
locale === "en"
|
|
482
|
+
? `No published tariff for the brand ${brand ?? ""}.`
|
|
483
|
+
: `Aucun tarif publié pour la marque ${brand ?? ""}.`
|
|
484
|
+
}}
|
|
485
|
+
</p>
|
|
486
|
+
<p class="pv_source">
|
|
487
|
+
<oip-drugs-heart-elastic-source-container-elastic
|
|
488
|
+
:uuid="HEART_SOURCE_UUIDS.PRICE_TRACKER"
|
|
489
|
+
:last-label="$t('oipDrugs.heart.sources.lastPrice')"
|
|
490
|
+
:last-date="lastPriceDates"
|
|
491
|
+
/>
|
|
492
|
+
</p>
|
|
493
|
+
</template>
|
|
494
|
+
|
|
495
|
+
<template v-else>
|
|
496
|
+
<p class="pv_scope">
|
|
497
|
+
<RmIcon name="Info" :size="14" />
|
|
498
|
+
{{
|
|
499
|
+
locale === "en"
|
|
500
|
+
? `Price tracking covers every presentation (CIP) of the brand ${brand ?? ""}.`
|
|
501
|
+
: `Le suivi des prix couvre toutes les présentations (CIP) de la marque ${brand ?? ""}.`
|
|
502
|
+
}}
|
|
503
|
+
</p>
|
|
504
|
+
|
|
505
|
+
<!-- Resume de la marque : ce qu'on veut savoir avant d'entrer dans le detail. -->
|
|
506
|
+
<div class="pv_kpis">
|
|
507
|
+
<div class="pv_kpi">
|
|
508
|
+
<p class="pv_kpi_value">{{ priced.length }}</p>
|
|
509
|
+
<p class="pv_kpi_label">
|
|
510
|
+
{{ locale === "en" ? "Priced presentations" : "Présentations tarifées" }}
|
|
511
|
+
</p>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div class="pv_kpi">
|
|
515
|
+
<p class="pv_kpi_value">
|
|
516
|
+
<template v-if="range">
|
|
517
|
+
{{ money(range.min) }}<span v-if="range.max !== range.min"> – {{ money(range.max) }}</span>
|
|
518
|
+
</template>
|
|
519
|
+
<template v-else>-</template>
|
|
520
|
+
</p>
|
|
521
|
+
<p class="pv_kpi_label">{{ locale === "en" ? "Ex-factory range" : "Fourchette PFHT" }}</p>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<div class="pv_kpi">
|
|
525
|
+
<p class="pv_kpi_value">
|
|
526
|
+
<RmDate v-if="lastUpdate" :date="lastUpdate" :lang="locale" />
|
|
527
|
+
<template v-else>-</template>
|
|
528
|
+
</p>
|
|
529
|
+
<p class="pv_kpi_label">
|
|
530
|
+
{{ locale === "en" ? "Latest tariff update" : "Dernière màj tarifaire" }}
|
|
531
|
+
</p>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div class="pv_kpi">
|
|
535
|
+
<p class="pv_kpi_value pv_trend" :class="`is_${trend.key}`">
|
|
536
|
+
<RmIcon
|
|
537
|
+
:name="trend.key === 'down' ? 'TrendDown' : trend.key === 'up' ? 'TrendUp' : 'Minus'"
|
|
538
|
+
:size="16"
|
|
539
|
+
/>
|
|
540
|
+
{{ trend.label }}
|
|
541
|
+
</p>
|
|
542
|
+
<p class="pv_kpi_label">{{ locale === "en" ? "Recent trend" : "Tendance récente" }}</p>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<div class="pv_panel">
|
|
547
|
+
<h3 class="pv_panel_title">
|
|
548
|
+
{{ locale === "en" ? "Price per presentation" : "Prix par présentation" }}
|
|
549
|
+
</h3>
|
|
550
|
+
<p class="pv_panel_sub">
|
|
551
|
+
{{
|
|
552
|
+
locale === "en"
|
|
553
|
+
? "Ex-factory (PFHT) and public price of the latest applicable tariff. Change, previous price and margin are derived from these amounts."
|
|
554
|
+
: "Prix fabricant HT (PFHT) et prix public du dernier tarif applicable. Variation, prix précédent et écart sont calculés à partir de ces montants."
|
|
555
|
+
}}
|
|
556
|
+
</p>
|
|
557
|
+
|
|
558
|
+
<div class="pv_grid">
|
|
559
|
+
<article
|
|
560
|
+
v-for="row in rows"
|
|
561
|
+
:key="row.key"
|
|
562
|
+
class="pv_card"
|
|
563
|
+
@click="drawerProduct = row.source"
|
|
564
|
+
>
|
|
565
|
+
<header class="pv_card_head">
|
|
566
|
+
<div class="pv_card_titleline">
|
|
567
|
+
<h4 class="pv_card_title">{{ row.label }}</h4>
|
|
568
|
+
<RmTag v-if="row.channel" :variant="row.channel.variant">
|
|
569
|
+
{{ row.channel.label }}
|
|
570
|
+
</RmTag>
|
|
571
|
+
</div>
|
|
572
|
+
<p v-if="row.date" class="pv_card_date">
|
|
573
|
+
<RmIcon name="Calendar" :size="14" />
|
|
574
|
+
{{ locale === "en" ? "Tariff of" : "Tarif au" }}
|
|
575
|
+
<RmDate :date="row.date" :lang="locale" />
|
|
576
|
+
</p>
|
|
577
|
+
</header>
|
|
578
|
+
|
|
579
|
+
<div class="pv_card_price">
|
|
580
|
+
<p class="pv_price">
|
|
581
|
+
{{ money(row.factory) }} <span class="pv_price_unit">PFHT</span>
|
|
582
|
+
</p>
|
|
583
|
+
|
|
584
|
+
<div v-if="typeof row.variation === 'number' && row.variation !== 0" class="pv_var">
|
|
585
|
+
<p class="pv_var_value" :class="row.variation < 0 ? 'is_down' : 'is_up'">
|
|
586
|
+
<RmIcon :name="row.variation < 0 ? 'TrendDown' : 'TrendUp'" :size="14" />
|
|
587
|
+
{{ row.variation > 0 ? "+" : "" }}{{ money(row.variation) }}
|
|
588
|
+
<span v-if="row.variationPct !== null">({{ percent(row.variationPct) }})</span>
|
|
589
|
+
</p>
|
|
590
|
+
<p v-if="row.previous !== null" class="pv_var_prev">
|
|
591
|
+
{{ locale === "en" ? "vs previous tariff" : "vs tarif précédent" }}
|
|
592
|
+
({{ money(row.previous) }})
|
|
593
|
+
</p>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<div class="pv_card_grid">
|
|
598
|
+
<div>
|
|
599
|
+
<p class="pv_field_label">
|
|
600
|
+
{{ locale === "en" ? "Public price incl. VAT" : "Prix public TTC" }}
|
|
601
|
+
</p>
|
|
602
|
+
<p class="pv_field_value">{{ money(row.publicPrice) }}</p>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
<div v-if="row.reimbursementRate !== null">
|
|
606
|
+
<p class="pv_field_label">
|
|
607
|
+
{{ locale === "en" ? "Reimbursement rate" : "Taux de remboursement" }}
|
|
608
|
+
</p>
|
|
609
|
+
<p class="pv_field_value">
|
|
610
|
+
{{ row.reimbursementRate.toLocaleString("fr-FR", { maximumFractionDigits: 0 }) }} %
|
|
611
|
+
</p>
|
|
612
|
+
</div>
|
|
613
|
+
|
|
614
|
+
<div v-if="row.margin !== null">
|
|
615
|
+
<p class="pv_field_label">
|
|
616
|
+
{{ locale === "en" ? "Ex-factory → public" : "Écart PFHT → public" }}
|
|
617
|
+
</p>
|
|
618
|
+
<p class="pv_field_value">
|
|
619
|
+
{{ row.margin > 0 ? "+" : "" }}{{ money(row.margin) }}
|
|
620
|
+
<span v-if="row.marginPct !== null" class="pv_field_muted">
|
|
621
|
+
({{ percent(row.marginPct) }})
|
|
622
|
+
</span>
|
|
623
|
+
</p>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<div v-if="row.manufacturer">
|
|
627
|
+
<p class="pv_field_label">{{ locale === "en" ? "Manufacturer" : "Fabricant" }}</p>
|
|
628
|
+
<p class="pv_field_value">{{ row.manufacturer }}</p>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div v-if="row.marketedOn">
|
|
632
|
+
<p class="pv_field_label">
|
|
633
|
+
{{ locale === "en" ? "Marketing date" : "Date de commercialisation" }}
|
|
634
|
+
</p>
|
|
635
|
+
<p class="pv_field_value">
|
|
636
|
+
<RmDate :date="row.marketedOn" :lang="locale" />
|
|
637
|
+
<span v-if="row.notMarketed" class="pv_field_muted">
|
|
638
|
+
({{ locale === "en" ? "no longer marketed" : "non commercialisée" }})
|
|
639
|
+
</span>
|
|
640
|
+
</p>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
<div v-else-if="row.listedOn">
|
|
644
|
+
<p class="pv_field_label">
|
|
645
|
+
{{ locale === "en" ? "Listed since" : "Inscrit depuis" }}
|
|
646
|
+
</p>
|
|
647
|
+
<p class="pv_field_value">
|
|
648
|
+
<RmDate :date="row.listedOn" :lang="locale" />
|
|
649
|
+
</p>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<div v-if="row.code">
|
|
653
|
+
<p class="pv_field_label">{{ row.codeLabel }}</p>
|
|
654
|
+
<p class="pv_field_value pv_code">{{ row.code }}</p>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</article>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
<p class="pv_source">
|
|
661
|
+
<oip-drugs-heart-elastic-source-container-elastic
|
|
662
|
+
:uuid="HEART_SOURCE_UUIDS.PRICE_TRACKER"
|
|
663
|
+
:last-label="$t('oipDrugs.heart.sources.lastPrice')"
|
|
664
|
+
:last-date="lastPriceDates"
|
|
665
|
+
/>
|
|
666
|
+
</p>
|
|
667
|
+
</div>
|
|
668
|
+
|
|
669
|
+
<div class="pv_panel">
|
|
670
|
+
<h3 class="pv_panel_title">
|
|
671
|
+
{{ locale === "en" ? "Tariff history (ex-factory)" : "Historique tarifaire (PFHT)" }}
|
|
672
|
+
</h3>
|
|
673
|
+
<p class="pv_panel_sub">
|
|
674
|
+
{{
|
|
675
|
+
locale === "en"
|
|
676
|
+
? "Ex-factory price over time, per presentation."
|
|
677
|
+
: "Évolution du prix fabricant HT dans le temps, par présentation."
|
|
678
|
+
}}
|
|
679
|
+
</p>
|
|
680
|
+
|
|
681
|
+
<template v-if="chart">
|
|
682
|
+
<div ref="chartWrap" class="pv_chart_wrap">
|
|
683
|
+
<svg
|
|
684
|
+
class="pv_chart"
|
|
685
|
+
:viewBox="`0 0 ${CHART.w} ${CHART.h}`"
|
|
686
|
+
role="img"
|
|
687
|
+
@mousemove="onChartMove"
|
|
688
|
+
@mouseleave="hover = null"
|
|
689
|
+
>
|
|
690
|
+
<!-- Reperes de prix : trois lignes suffisent a situer les marches. -->
|
|
691
|
+
<g class="pv_grid_lines">
|
|
692
|
+
<template v-for="tick in chart.priceTicks" :key="tick.value">
|
|
693
|
+
<line :x1="CHART.left" :x2="CHART.w - CHART.right" :y1="tick.y" :y2="tick.y" />
|
|
694
|
+
<text :x="CHART.left - 8" :y="tick.y + 4" text-anchor="end">
|
|
695
|
+
{{ Math.round(tick.value).toLocaleString("fr-FR") }} €
|
|
696
|
+
</text>
|
|
697
|
+
</template>
|
|
698
|
+
</g>
|
|
699
|
+
|
|
700
|
+
<g class="pv_axis">
|
|
701
|
+
<text
|
|
702
|
+
v-for="year in chart.years"
|
|
703
|
+
:key="year.label"
|
|
704
|
+
:x="year.x"
|
|
705
|
+
:y="CHART.h - 8"
|
|
706
|
+
text-anchor="middle"
|
|
707
|
+
>
|
|
708
|
+
{{ year.label }}
|
|
709
|
+
</text>
|
|
710
|
+
</g>
|
|
711
|
+
|
|
712
|
+
<g v-for="line in chart.lines" :key="line.label">
|
|
713
|
+
<path :d="line.path" :stroke="line.color" fill="none" stroke-width="2" />
|
|
714
|
+
<circle
|
|
715
|
+
v-for="dot in line.dots"
|
|
716
|
+
:key="dot.date"
|
|
717
|
+
:cx="dot.cx"
|
|
718
|
+
:cy="dot.cy"
|
|
719
|
+
r="3.5"
|
|
720
|
+
:fill="line.color"
|
|
721
|
+
/>
|
|
722
|
+
</g>
|
|
723
|
+
|
|
724
|
+
<line
|
|
725
|
+
v-if="hover"
|
|
726
|
+
class="pv_cursor"
|
|
727
|
+
:x1="hover.x"
|
|
728
|
+
:x2="hover.x"
|
|
729
|
+
:y1="CHART.top"
|
|
730
|
+
:y2="CHART.h - CHART.bottom"
|
|
731
|
+
/>
|
|
732
|
+
</svg>
|
|
733
|
+
|
|
734
|
+
<!-- Infobulle posee en pourcentage : le SVG est redimensionne par la
|
|
735
|
+
mise en page, ses unites ne sont pas des pixels d'ecran. -->
|
|
736
|
+
<div
|
|
737
|
+
v-if="hover"
|
|
738
|
+
class="pv_tip"
|
|
739
|
+
:class="{ is_left: hover.x > CHART.w * 0.6 }"
|
|
740
|
+
:style="{ left: `${(hover.x / CHART.w) * 100}%` }"
|
|
741
|
+
>
|
|
742
|
+
<p class="pv_tip_date">{{ new Date(hover.date).toLocaleDateString("fr-FR") }}</p>
|
|
743
|
+
<p v-for="item in hover.items" :key="item.label" class="pv_tip_row">
|
|
744
|
+
<span class="pv_legend_dot" :style="{ background: item.color }" />
|
|
745
|
+
<span class="pv_tip_label">{{ item.label }}</span>
|
|
746
|
+
<b>{{ money(item.price) }}</b>
|
|
747
|
+
</p>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
<ul class="pv_legend">
|
|
752
|
+
<li v-for="line in chart.lines" :key="line.label">
|
|
753
|
+
<span class="pv_legend_dot" :style="{ background: line.color }" />
|
|
754
|
+
{{ line.label }}
|
|
755
|
+
</li>
|
|
756
|
+
</ul>
|
|
757
|
+
</template>
|
|
758
|
+
|
|
759
|
+
<p v-else class="pv_empty">
|
|
760
|
+
{{
|
|
761
|
+
locale === "en"
|
|
762
|
+
? "Not enough tariff records to draw a trend for this brand."
|
|
763
|
+
: "Pas assez de tarifs successifs pour tracer une évolution sur cette marque."
|
|
764
|
+
}}
|
|
765
|
+
</p>
|
|
766
|
+
|
|
767
|
+
<!-- Le detail du graphe : les memes tarifs, en chiffres, toutes
|
|
768
|
+
presentations confondues. Le tiroir d'une presentation montre les
|
|
769
|
+
siens ; ici on compare les presentations entre elles sans l'ouvrir. -->
|
|
770
|
+
<template v-if="historyRows.length">
|
|
771
|
+
<h4 class="pv_table_title">
|
|
772
|
+
{{ locale === "en" ? "Every published tariff" : "Tous les tarifs publiés" }}
|
|
773
|
+
</h4>
|
|
774
|
+
<p class="pv_panel_sub">
|
|
775
|
+
{{
|
|
776
|
+
locale === "en"
|
|
777
|
+
? "One row per published tariff, all presentations together. The change compares with the previous tariff of the same presentation."
|
|
778
|
+
: "Une ligne par tarif publié, toutes présentations confondues. L'évolution compare au tarif précédent de la même présentation."
|
|
779
|
+
}}
|
|
780
|
+
</p>
|
|
781
|
+
|
|
782
|
+
<RmTable
|
|
783
|
+
:columns="historyColumns"
|
|
784
|
+
:rows="historyRows"
|
|
785
|
+
:caption="locale === 'en' ? 'Tariff history, all presentations' : 'Historique tarifaire, toutes présentations'"
|
|
786
|
+
row-key="key"
|
|
787
|
+
compact
|
|
788
|
+
search="auto"
|
|
789
|
+
:search-placeholder="
|
|
790
|
+
locale === 'en'
|
|
791
|
+
? 'Search a presentation, a channel...'
|
|
792
|
+
: 'Rechercher une présentation, un circuit...'
|
|
793
|
+
"
|
|
794
|
+
:page-size="25"
|
|
795
|
+
max-height="520px"
|
|
796
|
+
:empty-text="locale === 'en' ? 'No published tariff' : 'Aucun tarif publié'"
|
|
797
|
+
>
|
|
798
|
+
<template #presentation="{ row }">
|
|
799
|
+
<span class="pv_table_name">{{ row.presentation }}</span>
|
|
800
|
+
<span v-if="row.code" class="pv_table_code">{{ row.code }}</span>
|
|
801
|
+
</template>
|
|
802
|
+
|
|
803
|
+
<template #date="{ value }">
|
|
804
|
+
<RmDate v-if="value" :date="value" :lang="locale" />
|
|
805
|
+
<template v-else>-</template>
|
|
806
|
+
</template>
|
|
807
|
+
|
|
808
|
+
<template #factory="{ value }">{{ money(value) }}</template>
|
|
809
|
+
<template #publicPrice="{ value }">{{ money(value) }}</template>
|
|
810
|
+
<template #marge="{ value }">{{ money(value) }}</template>
|
|
811
|
+
|
|
812
|
+
<!-- Un tarif reconduit au meme prix (le cas courant) laisse la
|
|
813
|
+
cellule vide plutot que d'ecrire « 0,00 € (0 %) » sur toute la
|
|
814
|
+
hauteur du tableau : les cartes du dessus font deja ce choix. -->
|
|
815
|
+
<template #variation="{ row }">
|
|
816
|
+
<template v-if="!row.variation">-</template>
|
|
817
|
+
<span
|
|
818
|
+
v-else
|
|
819
|
+
class="pv_table_var"
|
|
820
|
+
:class="row.variation > 0 ? 'is_up' : 'is_down'"
|
|
821
|
+
>
|
|
822
|
+
{{ row.variation > 0 ? "+" : "" }}{{ money(row.variation) }}
|
|
823
|
+
<span v-if="row.variationPct !== null" class="pv_field_muted">
|
|
824
|
+
({{ percent(row.variationPct) }})
|
|
825
|
+
</span>
|
|
826
|
+
</span>
|
|
827
|
+
</template>
|
|
828
|
+
|
|
829
|
+
<template #documentarySource="{ row }">
|
|
830
|
+
{{ row.documentarySource }}
|
|
831
|
+
<span v-if="row.sourceDate" class="pv_field_muted">
|
|
832
|
+
· <RmDate :date="row.sourceDate" :lang="locale" />
|
|
833
|
+
</span>
|
|
834
|
+
</template>
|
|
835
|
+
</RmTable>
|
|
836
|
+
</template>
|
|
837
|
+
|
|
838
|
+
<p class="pv_source">
|
|
839
|
+
<oip-drugs-heart-elastic-source-container-elastic
|
|
840
|
+
:uuid="HEART_SOURCE_UUIDS.PRICE_TRACKER"
|
|
841
|
+
:last-label="$t('oipDrugs.heart.sources.lastPrice')"
|
|
842
|
+
:last-date="lastPriceDates"
|
|
843
|
+
/>
|
|
844
|
+
</p>
|
|
845
|
+
</div>
|
|
846
|
+
</template>
|
|
847
|
+
|
|
848
|
+
<!-- Tout ce que l'index sait de la presentation choisie. -->
|
|
849
|
+
<oip-drugs-heart-results-details-presentation-drawer
|
|
850
|
+
:product="drawerProduct"
|
|
851
|
+
@close="drawerProduct = null"
|
|
852
|
+
/>
|
|
853
|
+
</section>
|
|
854
|
+
</template>
|
|
855
|
+
|
|
856
|
+
<style scoped>
|
|
857
|
+
.pv {
|
|
858
|
+
display: flex;
|
|
859
|
+
flex-direction: column;
|
|
860
|
+
gap: 16px;
|
|
861
|
+
box-sizing: border-box;
|
|
862
|
+
width: 100%;
|
|
863
|
+
padding-inline: 32px;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.pv_scope {
|
|
867
|
+
display: flex;
|
|
868
|
+
align-items: center;
|
|
869
|
+
gap: 6px;
|
|
870
|
+
color: var(--medium);
|
|
871
|
+
font-size: 13px;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* Titre du tableau de detail : un cran sous le titre de panneau, pour dire
|
|
875
|
+
qu'il detaille le graphe au-dessus plutot que d'ouvrir un autre sujet. */
|
|
876
|
+
.pv_table_title {
|
|
877
|
+
margin-top: 8px;
|
|
878
|
+
color: var(--dark);
|
|
879
|
+
font-size: 14px;
|
|
880
|
+
font-weight: 600;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/* Meme code couleur que la variation des cartes : une baisse de prix se lit
|
|
884
|
+
en vert, une hausse en rouge. */
|
|
885
|
+
.pv_table_var.is_down {
|
|
886
|
+
color: var(--system-success-hover);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.pv_table_var.is_up {
|
|
890
|
+
color: var(--system-alert-hover);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.pv_table_name {
|
|
894
|
+
font-weight: 500;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* Le code tombe sous le libelle : deux presentations d'une meme marque ne se
|
|
898
|
+
distinguent parfois que par lui. */
|
|
899
|
+
.pv_table_code {
|
|
900
|
+
display: block;
|
|
901
|
+
color: var(--medium);
|
|
902
|
+
font-size: 11px;
|
|
903
|
+
font-variant-numeric: tabular-nums;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/* Bandeau de synthese : quatre mesures, un filet degrade en tete. */
|
|
907
|
+
.pv_kpis {
|
|
908
|
+
display: grid;
|
|
909
|
+
/* Quatre colonnes de meme largeur, puis deux, puis une : avec `auto-fit`, la
|
|
910
|
+
quatrieme mesure tombait seule sur une deuxieme ligne, pleine largeur, et
|
|
911
|
+
le bandeau paraissait casse. */
|
|
912
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
913
|
+
border: 1px solid var(--light);
|
|
914
|
+
border-radius: 12px;
|
|
915
|
+
background: var(--white);
|
|
916
|
+
overflow: hidden;
|
|
917
|
+
box-shadow: var(--small-shadow);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.pv_kpis::before {
|
|
921
|
+
content: "";
|
|
922
|
+
grid-column: 1 / -1;
|
|
923
|
+
height: 3px;
|
|
924
|
+
background: linear-gradient(to right, var(--primary), color-mix(in srgb, var(--primary) 35%, var(--white)));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.pv_kpi {
|
|
928
|
+
display: flex;
|
|
929
|
+
flex-direction: column;
|
|
930
|
+
align-items: center;
|
|
931
|
+
gap: 6px;
|
|
932
|
+
padding: 20px 16px;
|
|
933
|
+
border-left: 1px solid var(--light);
|
|
934
|
+
text-align: center;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.pv_kpi:first-of-type {
|
|
938
|
+
border-left: none;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
@media (max-width: 1100px) {
|
|
942
|
+
.pv_kpis {
|
|
943
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.pv_kpi:nth-of-type(odd) {
|
|
947
|
+
border-left: none;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
@media (max-width: 640px) {
|
|
952
|
+
.pv_kpis {
|
|
953
|
+
grid-template-columns: 1fr;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.pv_kpi {
|
|
957
|
+
border-left: none;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.pv_none {
|
|
962
|
+
display: flex;
|
|
963
|
+
align-items: center;
|
|
964
|
+
gap: 8px;
|
|
965
|
+
padding: 16px;
|
|
966
|
+
border: 1px solid var(--light);
|
|
967
|
+
border-radius: 12px;
|
|
968
|
+
background: var(--white);
|
|
969
|
+
color: var(--dark);
|
|
970
|
+
font-size: 14px;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.pv_kpi_value {
|
|
974
|
+
color: var(--primary);
|
|
975
|
+
font-size: 19px;
|
|
976
|
+
font-weight: 800;
|
|
977
|
+
white-space: nowrap;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.pv_kpi_label {
|
|
981
|
+
color: var(--medium);
|
|
982
|
+
font-size: 11px;
|
|
983
|
+
font-weight: 600;
|
|
984
|
+
letter-spacing: 0.04em;
|
|
985
|
+
text-transform: uppercase;
|
|
986
|
+
white-space: nowrap;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.pv_trend {
|
|
990
|
+
display: inline-flex;
|
|
991
|
+
align-items: center;
|
|
992
|
+
gap: 6px;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.pv_trend.is_down {
|
|
996
|
+
color: var(--system-success-hover);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.pv_trend.is_up {
|
|
1000
|
+
color: var(--system-alert-hover);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.pv_trend.is_flat {
|
|
1004
|
+
color: var(--dark);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.pv_panel {
|
|
1008
|
+
display: flex;
|
|
1009
|
+
flex-direction: column;
|
|
1010
|
+
gap: 6px;
|
|
1011
|
+
padding: 20px;
|
|
1012
|
+
border: 1px solid var(--light);
|
|
1013
|
+
border-radius: 12px;
|
|
1014
|
+
background: var(--white);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.pv_panel_title {
|
|
1018
|
+
font-size: 17px;
|
|
1019
|
+
font-weight: 800;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.pv_panel_sub {
|
|
1023
|
+
color: var(--medium);
|
|
1024
|
+
font-size: 14px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.pv_grid {
|
|
1028
|
+
display: grid;
|
|
1029
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
1030
|
+
gap: 16px;
|
|
1031
|
+
margin-top: 12px;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/* La carte ouvre le detail complet de la presentation. */
|
|
1035
|
+
.pv_card {
|
|
1036
|
+
cursor: pointer;
|
|
1037
|
+
transition: border-color 0.15s linear, box-shadow 0.15s linear;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.pv_card:hover {
|
|
1041
|
+
border-color: color-mix(in srgb, var(--primary) 35%, var(--light));
|
|
1042
|
+
box-shadow: var(--medium-shadow);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.pv_card {
|
|
1046
|
+
display: flex;
|
|
1047
|
+
flex-direction: column;
|
|
1048
|
+
gap: 12px;
|
|
1049
|
+
padding: 16px;
|
|
1050
|
+
border: 1px solid var(--light);
|
|
1051
|
+
border-radius: 12px;
|
|
1052
|
+
background: var(--demi-fond);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.pv_card_titleline {
|
|
1056
|
+
display: flex;
|
|
1057
|
+
align-items: flex-start;
|
|
1058
|
+
justify-content: space-between;
|
|
1059
|
+
gap: 8px;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Le tag garde sa largeur : un libelle long ne doit pas l'ecraser. */
|
|
1063
|
+
.pv_card_titleline :deep(.rm-tag) {
|
|
1064
|
+
flex: 0 0 auto;
|
|
1065
|
+
white-space: nowrap;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.pv_card_title {
|
|
1069
|
+
font-size: 15px;
|
|
1070
|
+
font-weight: 800;
|
|
1071
|
+
text-transform: uppercase;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.pv_card_date {
|
|
1075
|
+
display: flex;
|
|
1076
|
+
align-items: center;
|
|
1077
|
+
gap: 6px;
|
|
1078
|
+
margin-top: 4px;
|
|
1079
|
+
color: var(--medium);
|
|
1080
|
+
font-size: 13px;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.pv_card_price {
|
|
1084
|
+
display: flex;
|
|
1085
|
+
align-items: flex-start;
|
|
1086
|
+
justify-content: space-between;
|
|
1087
|
+
flex-wrap: wrap;
|
|
1088
|
+
gap: 12px;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.pv_price {
|
|
1092
|
+
color: var(--primary);
|
|
1093
|
+
font-size: 26px;
|
|
1094
|
+
font-weight: 800;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.pv_price_unit {
|
|
1098
|
+
margin-left: 4px;
|
|
1099
|
+
color: var(--medium);
|
|
1100
|
+
font-size: 13px;
|
|
1101
|
+
font-weight: 600;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
.pv_var {
|
|
1105
|
+
text-align: right;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.pv_var_value {
|
|
1109
|
+
display: inline-flex;
|
|
1110
|
+
align-items: center;
|
|
1111
|
+
gap: 6px;
|
|
1112
|
+
font-size: 14px;
|
|
1113
|
+
font-weight: 700;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.pv_var_value.is_down {
|
|
1117
|
+
color: var(--system-success-hover);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.pv_var_value.is_up {
|
|
1121
|
+
color: var(--system-alert-hover);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.pv_var_prev {
|
|
1125
|
+
color: var(--medium);
|
|
1126
|
+
font-size: 13px;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.pv_card_grid {
|
|
1130
|
+
display: grid;
|
|
1131
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
1132
|
+
gap: 12px 16px;
|
|
1133
|
+
padding-top: 12px;
|
|
1134
|
+
border-top: 1px solid var(--light);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.pv_field_label {
|
|
1138
|
+
color: var(--medium);
|
|
1139
|
+
font-size: 12px;
|
|
1140
|
+
font-weight: 600;
|
|
1141
|
+
letter-spacing: 0.04em;
|
|
1142
|
+
text-transform: uppercase;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.pv_field_value {
|
|
1146
|
+
margin-top: 2px;
|
|
1147
|
+
font-size: 15px;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.pv_field_muted {
|
|
1151
|
+
color: var(--medium);
|
|
1152
|
+
font-size: 13px;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.pv_code {
|
|
1156
|
+
font-family: "Courier New", monospace;
|
|
1157
|
+
font-size: 14px;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.pv_source {
|
|
1161
|
+
margin-top: 12px;
|
|
1162
|
+
font-size: 13px;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.pv_chart_wrap {
|
|
1166
|
+
position: relative;
|
|
1167
|
+
margin-top: 12px;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.pv_chart {
|
|
1171
|
+
display: block;
|
|
1172
|
+
width: 100%;
|
|
1173
|
+
height: auto;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.pv_cursor {
|
|
1177
|
+
stroke: var(--medium);
|
|
1178
|
+
stroke-width: 1;
|
|
1179
|
+
stroke-dasharray: 4 4;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.pv_tip {
|
|
1183
|
+
position: absolute;
|
|
1184
|
+
top: 8px;
|
|
1185
|
+
z-index: 2;
|
|
1186
|
+
min-width: 220px;
|
|
1187
|
+
padding: 10px 12px;
|
|
1188
|
+
border-radius: 8px;
|
|
1189
|
+
background: var(--darkest);
|
|
1190
|
+
color: var(--white);
|
|
1191
|
+
font-size: 13px;
|
|
1192
|
+
pointer-events: none;
|
|
1193
|
+
transform: translateX(12px);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/* Au-dela des deux tiers du graphe, l'infobulle bascule a gauche du curseur
|
|
1197
|
+
plutot que de sortir de la carte. */
|
|
1198
|
+
.pv_tip.is_left {
|
|
1199
|
+
transform: translateX(calc(-100% - 12px));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.pv_tip_date {
|
|
1203
|
+
margin-bottom: 6px;
|
|
1204
|
+
color: var(--light);
|
|
1205
|
+
font-size: 12px;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.pv_tip_row {
|
|
1209
|
+
display: flex;
|
|
1210
|
+
align-items: center;
|
|
1211
|
+
gap: 8px;
|
|
1212
|
+
padding: 2px 0;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.pv_tip_label {
|
|
1216
|
+
flex: 1 1 auto;
|
|
1217
|
+
overflow: hidden;
|
|
1218
|
+
text-overflow: ellipsis;
|
|
1219
|
+
white-space: nowrap;
|
|
1220
|
+
max-width: 220px;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.pv_grid_lines line {
|
|
1224
|
+
stroke: var(--light);
|
|
1225
|
+
stroke-width: 1;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.pv_grid_lines text,
|
|
1229
|
+
.pv_axis text {
|
|
1230
|
+
fill: var(--medium);
|
|
1231
|
+
font-size: 12px;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
.pv_legend {
|
|
1235
|
+
display: flex;
|
|
1236
|
+
flex-wrap: wrap;
|
|
1237
|
+
gap: 8px 20px;
|
|
1238
|
+
margin-top: 8px;
|
|
1239
|
+
list-style: none;
|
|
1240
|
+
color: var(--medium);
|
|
1241
|
+
font-size: 12px;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.pv_legend li {
|
|
1245
|
+
display: flex;
|
|
1246
|
+
align-items: center;
|
|
1247
|
+
gap: 8px;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.pv_legend_dot {
|
|
1251
|
+
width: 10px;
|
|
1252
|
+
height: 10px;
|
|
1253
|
+
border-radius: 999px;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.pv_empty {
|
|
1257
|
+
margin-top: 12px;
|
|
1258
|
+
color: var(--medium);
|
|
1259
|
+
font-size: 14px;
|
|
1260
|
+
}
|
|
1261
|
+
</style>
|