@claudebernard/therapeutic-alternative-element 2.1.2 → 2.2.0

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.
@@ -5,9 +5,10 @@ import { TemplateResult } from 'lit-html';
5
5
 
6
6
  /**
7
7
  * Composant pour afficher les alternatives thérapeutiques d'un médicament donné.
8
- * Permet à l'utilisateur d'affiner sa recherche via des critères d'inclusion, d'exclusion et des
9
- * filtres rapides, et affiche les résultats correspondants — toute la recherche/agrégation reste
10
- * côté serveur, ce composant ne fait qu'envoyer les critères et afficher ce qui revient.
8
+ * Permet à l'utilisateur d'affiner sa recherche via un pool de base (scope), des critères
9
+ * complémentaires (include), des critères d'exclusion et des filtres rapides, et affiche les
10
+ * résultats correspondants — toute la recherche/agrégation reste côté serveur, ce composant ne
11
+ * fait qu'envoyer les critères et afficher ce qui revient.
11
12
  * @element bcb-therapeutic-alternative
12
13
  */
13
14
  export declare class TherapeuticAlternative extends LitElement {
@@ -21,17 +22,21 @@ export declare class TherapeuticAlternative extends LitElement {
21
22
  private totalElements;
22
23
  private lastRequestHadNoSelectedCriteria;
23
24
  private currentPage;
25
+ private scope;
24
26
  private include;
25
27
  private exclude;
26
28
  private quickFilters;
29
+ private scopeFacets;
27
30
  private includeFacets;
28
31
  private excludeFacets;
29
32
  private truncatedGroups;
30
33
  private expandFacetGroupKey;
34
+ private scopeLabels;
31
35
  private includeLabels;
32
36
  private excludeLabels;
33
- private openInclusionGroup;
37
+ private openScopeGroup;
34
38
  private openExclusionGroup;
39
+ private openIncludeGroup;
35
40
  private checklistSearchTerm;
36
41
  private sectionCollapsed;
37
42
  private apiSearchError;
@@ -40,7 +45,7 @@ export declare class TherapeuticAlternative extends LitElement {
40
45
  private apiReplaceError;
41
46
  private apiReplaceLoading;
42
47
  private missingPrescriptionError;
43
- private hasAppliedInitialInclude;
48
+ private hasAppliedInitialScope;
44
49
  private refetchTimer;
45
50
  private requestSeq;
46
51
  private get currentMedication();
@@ -53,15 +58,19 @@ export declare class TherapeuticAlternative extends LitElement {
53
58
  private hasNoSelectedCriteria;
54
59
  private fetchAlternatives;
55
60
  private applyResponse;
61
+ private static buildScopeFacets;
56
62
  private static buildIncludeFacets;
57
63
  private static buildExcludeFacets;
58
- private static deriveInitialInclude;
59
- private toggleInclude;
64
+ private static deriveInitialScope;
65
+ private toggleScope;
60
66
  private toggleExclude;
67
+ private toggleInclude;
61
68
  private toggleQuickFilter;
62
69
  private exclusionTruncatedGroupKey;
63
- private toggleInclusionPanel;
70
+ private includeTruncatedGroupKey;
71
+ private toggleScopePanel;
64
72
  private toggleExclusionPanel;
73
+ private toggleIncludePanel;
65
74
  private requestMoreFacetValues;
66
75
  private toggleSection;
67
76
  private cancelAlternative;
@@ -71,8 +80,9 @@ export declare class TherapeuticAlternative extends LitElement {
71
80
  private renderChecklistSearch;
72
81
  private renderSelectedChips;
73
82
  private renderPill;
74
- private renderInclusionSection;
83
+ private renderScopeSection;
75
84
  private renderExclusionSection;
85
+ private renderIncludeSection;
76
86
  private renderQuickFiltersSection;
77
87
  private renderSectionHeader;
78
88
  private matchReasonLabels;
@@ -2297,10 +2297,9 @@ ProductCard = __decorateClass$1([
2297
2297
  ], ProductCard);
2298
2298
  const REFETCH_DEBOUNCE_MS = 300;
2299
2299
  const CHECKLIST_SEARCH_THRESHOLD = 30;
2300
- const INCLUSION_GROUPS = [
2300
+ const SCOPE_GROUPS = [
2301
2301
  { key: "indications", idsField: "indicationIds", label: "Indications" },
2302
- { key: "activeSubstances", idsField: "activeSubstanceIds", label: "Principes actifs" },
2303
- { key: "atcClasses", idsField: "atcClassIds", label: "Classe thérapeutique" }
2302
+ { key: "atcClasses", idsField: "atcClassIds", label: "Classe ATC" }
2304
2303
  ];
2305
2304
  const EXCLUSION_GROUPS = [
2306
2305
  { key: "activeSubstances", idsField: "activeSubstanceIds", label: "Principes actifs" },
@@ -2315,6 +2314,12 @@ const QUICK_FILTERS = [
2315
2314
  { key: "sameDosage", label: "Même dosage" },
2316
2315
  { key: "sameRouteOfAdministration", label: "Même voie d'administration" }
2317
2316
  ];
2317
+ const INCLUDE_GROUPS = [
2318
+ { key: "activeSubstances", idsField: "activeSubstanceIds", label: "Principes actifs" },
2319
+ { key: "galenicForms", idsField: "galenicFormIds", label: "Forme galénique", quickFilterKey: "sameGalenicForm" },
2320
+ { key: "dosages", idsField: "dosageIds", label: "Dosage", quickFilterKey: "sameDosage" },
2321
+ { key: "routesOfAdministration", idsField: "routeOfAdministrationIds", label: "Voie d'administration", quickFilterKey: "sameRouteOfAdministration" }
2322
+ ];
2318
2323
  const INCLUDE_COUNT_TOOLTIP = "Nombre de résultats restants avec cette valeur";
2319
2324
  const EXCLUDE_COUNT_TOOLTIP = "Nombre de résultats retirés avec cette valeur";
2320
2325
  const MATCH_REASON_VERB_BY_DIMENSION = {
@@ -2322,8 +2327,11 @@ const MATCH_REASON_VERB_BY_DIMENSION = {
2322
2327
  activeSubstance: "contient"
2323
2328
  };
2324
2329
  const therapeuticAlternativeCSS = '.claudebernard-therapeutic-alternative {\n font-family: var(--custom-font-family, "Ubuntu"), sans-serif;\n padding: 1rem;\n}\n.claudebernard-therapeutic-alternative-without-title {\n padding-top: 0;\n max-width: 70vw;\n width: 60vw;\n max-height: 70vh;\n}\n.claudebernard-therapeutic-alternative-title {\n font-size: 0.75rem;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--bcbd-gray-500-color, #64748B);\n margin-bottom: 0.375rem;\n display: block;\n}\n.claudebernard-therapeutic-alternative__source {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n flex-wrap: wrap;\n}\n.claudebernard-therapeutic-alternative__disclaimer {\n margin-bottom: 1rem;\n}\n.claudebernard-therapeutic-alternative__loading {\n width: 100%;\n height: 30%;\n padding-top: 2rem;\n display: flex;\n justify-content: center;\n}\n.claudebernard-therapeutic-alternative__section {\n border: 1px solid var(--bcbd-border-gray-color, #CBD5E1);\n border-radius: 0.625rem;\n padding: 0.75rem 0.875rem;\n margin-bottom: 1rem;\n}\n.claudebernard-therapeutic-alternative__section-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n margin-bottom: 0.625rem;\n font-family: inherit;\n line-height: 1;\n}\n.claudebernard-therapeutic-alternative__section-header span:first-child {\n font-size: 0.75rem;\n font-weight: 700;\n color: var(--bcbd-gray-500-color, #64748B);\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n.claudebernard-therapeutic-alternative__chevron {\n font-size: 0.75rem;\n color: var(--bcbd-gray-400-color, #94A3B8);\n transition: transform 0.15s ease-out;\n display: inline-flex;\n}\n.claudebernard-therapeutic-alternative__chevron--expanded {\n transform: rotate(180deg);\n}\n.claudebernard-therapeutic-alternative__pills {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n.claudebernard-therapeutic-alternative__pill {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n border: 1px solid var(--bcbd-border-gray-color, #CBD5E1);\n background: #fff;\n color: var(--bcbd-gray-600-color, #475569);\n border-radius: 6.25rem;\n padding: 0.375rem 0.75rem;\n font-size: 0.8125rem;\n font-family: inherit;\n font-weight: 400;\n white-space: nowrap;\n cursor: pointer;\n line-height: 1;\n}\n.claudebernard-therapeutic-alternative__pill--active {\n border-color: var(--bcbd-primary-color, #0C75AD);\n background: color-mix(in srgb, var(--bcbd-primary-color, #0C75AD) 10%, transparent);\n color: var(--bcbd-primary-color, #0C75AD);\n font-weight: 700;\n}\n.claudebernard-therapeutic-alternative__panel {\n border-top: 1px solid var(--bcbd-border-color-200, #E2E8F0);\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.claudebernard-therapeutic-alternative__panel-rows {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n max-height: 16.25rem;\n overflow-y: auto;\n padding: 0.25rem 0.125rem;\n}\n.claudebernard-therapeutic-alternative__panel-empty {\n display: block;\n margin-top: 0.75rem;\n font-size: 0.875rem;\n color: var(--bcbd-gray-500-color, #64748B);\n}\n.claudebernard-therapeutic-alternative__panel-expand {\n align-self: flex-start;\n background: none;\n border: none;\n padding: 0.25rem 0 0;\n font-family: inherit;\n font-size: 0.8125rem;\n font-weight: 700;\n color: var(--bcbd-primary-color, #0C75AD);\n cursor: pointer;\n}\n.claudebernard-therapeutic-alternative__panel-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-radius: 0.375rem;\n --bcbd-checkbox-font-size: 0.875rem;\n --bcbd-checkbox-size: 0.9375rem;\n}\n.claudebernard-therapeutic-alternative__panel-row--pending {\n background: var(--bcbd-gray-100-color, #F1F5F9);\n padding: 0.25rem 0.375rem;\n margin: -0.25rem -0.375rem;\n}\n.claudebernard-therapeutic-alternative__count-badge {\n font-size: 0.75rem;\n color: var(--bcbd-white-color, #fff);\n background: var(--bcbd-primary-color, #0C75AD);\n border-radius: 0.25rem;\n padding: 0.125rem 0.375rem;\n flex-shrink: 0;\n}\n.claudebernard-therapeutic-alternative__selected-chips {\n display: flex;\n gap: 0.375rem;\n flex-wrap: wrap;\n margin-top: 0.5rem;\n}\n.claudebernard-therapeutic-alternative__chips {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n.claudebernard-therapeutic-alternative__chip {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n border: 1px solid var(--bcbd-border-gray-color, #CBD5E1);\n background: #fff;\n color: var(--bcbd-gray-500-color, #64748B);\n border-radius: 6.25rem;\n padding: 0.375rem 0.75rem;\n font-size: 0.8125rem;\n font-family: inherit;\n font-weight: 400;\n white-space: nowrap;\n cursor: pointer;\n line-height: 1;\n}\n.claudebernard-therapeutic-alternative__chip--active {\n border-color: var(--bcbd-primary-color, #0C75AD);\n background: color-mix(in srgb, var(--bcbd-primary-color, #0C75AD) 10%, transparent);\n color: var(--bcbd-primary-color, #0C75AD);\n font-weight: 700;\n}\n.claudebernard-therapeutic-alternative__results {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.claudebernard-therapeutic-alternative__results-header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n margin-bottom: 0.625rem;\n}\n.claudebernard-therapeutic-alternative__results-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n background: color-mix(in srgb, var(--bcbd-primary-color, #0C75AD) 10%, transparent);\n color: var(--bcbd-primary-color, #0C75AD);\n border-radius: 6.25rem;\n padding: 0.3125rem 0.75rem;\n font-size: 0.8125rem;\n font-weight: 700;\n line-height: 1;\n}\n.claudebernard-therapeutic-alternative__results-list {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.claudebernard-therapeutic-alternative__no-results {\n text-align: center;\n padding: 1.5rem 1rem;\n color: var(--bcbd-gray-500-color, #64748B);\n font-size: 0.875rem;\n}\n.claudebernard-therapeutic-alternative__no-medication {\n padding: 1.25rem;\n}';
2330
+ function emptyScopeCriteria() {
2331
+ return { indicationIds: [], atcClassIds: [] };
2332
+ }
2325
2333
  function emptyIncludeCriteria() {
2326
- return { indicationIds: [], activeSubstanceIds: [], atcClassIds: [] };
2334
+ return { activeSubstanceIds: [], galenicFormIds: [], dosageIds: [], routeOfAdministrationIds: [] };
2327
2335
  }
2328
2336
  function emptyExcludeCriteria() {
2329
2337
  return {
@@ -2338,8 +2346,11 @@ function emptyExcludeCriteria() {
2338
2346
  function emptyQuickFilters() {
2339
2347
  return { sameGalenicForm: false, sameDosage: false, sameRouteOfAdministration: false };
2340
2348
  }
2349
+ function emptyScopeFacets() {
2350
+ return { indications: [], atcClasses: [] };
2351
+ }
2341
2352
  function emptyIncludeFacets() {
2342
- return { indications: [], activeSubstances: [], atcClasses: [] };
2353
+ return { activeSubstances: [], galenicForms: [], dosages: [], routesOfAdministration: [] };
2343
2354
  }
2344
2355
  function emptyExcludeFacets() {
2345
2356
  return {
@@ -2351,8 +2362,11 @@ function emptyExcludeFacets() {
2351
2362
  precautions: []
2352
2363
  };
2353
2364
  }
2365
+ function emptyScopeLabels() {
2366
+ return { indications: {}, atcClasses: {} };
2367
+ }
2354
2368
  function emptyIncludeLabels() {
2355
- return { indications: {}, activeSubstances: {}, atcClasses: {} };
2369
+ return { activeSubstances: {}, galenicForms: {}, dosages: {}, routesOfAdministration: {} };
2356
2370
  }
2357
2371
  function labelsForIds(ids, values) {
2358
2372
  const labelById = new Map(values.filter((v2) => v2.id).map((v2) => [v2.id, v2.label ?? v2.id]));
@@ -2397,26 +2411,30 @@ let TherapeuticAlternative = class extends i$1 {
2397
2411
  this.totalElements = 0;
2398
2412
  this.lastRequestHadNoSelectedCriteria = true;
2399
2413
  this.currentPage = 1;
2414
+ this.scope = emptyScopeCriteria();
2400
2415
  this.include = emptyIncludeCriteria();
2401
2416
  this.exclude = emptyExcludeCriteria();
2402
2417
  this.quickFilters = emptyQuickFilters();
2418
+ this.scopeFacets = emptyScopeFacets();
2403
2419
  this.includeFacets = emptyIncludeFacets();
2404
2420
  this.excludeFacets = emptyExcludeFacets();
2405
2421
  this.truncatedGroups = [];
2406
2422
  this.expandFacetGroupKey = null;
2423
+ this.scopeLabels = emptyScopeLabels();
2407
2424
  this.includeLabels = emptyIncludeLabels();
2408
2425
  this.excludeLabels = emptyExcludeLabels();
2409
- this.openInclusionGroup = null;
2426
+ this.openScopeGroup = null;
2410
2427
  this.openExclusionGroup = null;
2428
+ this.openIncludeGroup = null;
2411
2429
  this.checklistSearchTerm = "";
2412
- this.sectionCollapsed = { inclusion: false, exclusion: false, quickFilters: false };
2430
+ this.sectionCollapsed = { scope: false, include: false, exclusion: false, quickFilters: false };
2413
2431
  this.apiSearchError = null;
2414
2432
  this.apiSearchLoading = false;
2415
2433
  this.pendingToggleIds = /* @__PURE__ */ new Set();
2416
2434
  this.apiReplaceError = null;
2417
2435
  this.apiReplaceLoading = false;
2418
2436
  this.missingPrescriptionError = null;
2419
- this.hasAppliedInitialInclude = false;
2437
+ this.hasAppliedInitialScope = false;
2420
2438
  this.requestSeq = 0;
2421
2439
  }
2422
2440
  get currentMedication() {
@@ -2440,16 +2458,19 @@ let TherapeuticAlternative = class extends i$1 {
2440
2458
  }
2441
2459
  if (changedProperties.has("medication") && this.currentMedication) {
2442
2460
  this.clearScheduledRefetch();
2461
+ this.scope = emptyScopeCriteria();
2443
2462
  this.include = emptyIncludeCriteria();
2444
2463
  this.exclude = emptyExcludeCriteria();
2464
+ this.scopeLabels = emptyScopeLabels();
2445
2465
  this.includeLabels = emptyIncludeLabels();
2446
2466
  this.excludeLabels = emptyExcludeLabels();
2447
2467
  this.quickFilters = emptyQuickFilters();
2448
- this.openInclusionGroup = null;
2468
+ this.openScopeGroup = null;
2449
2469
  this.openExclusionGroup = null;
2470
+ this.openIncludeGroup = null;
2450
2471
  this.expandFacetGroupKey = null;
2451
2472
  this.currentPage = 1;
2452
- this.hasAppliedInitialInclude = false;
2473
+ this.hasAppliedInitialScope = false;
2453
2474
  this.fetchAlternatives();
2454
2475
  }
2455
2476
  }
@@ -2471,10 +2492,11 @@ let TherapeuticAlternative = class extends i$1 {
2471
2492
  // Mirrors the backend's own hasNoSelectedCriteria(request) -- must be evaluated against
2472
2493
  // whatever's about to be SENT, not read back later, see lastRequestHadNoSelectedCriteria.
2473
2494
  hasNoSelectedCriteria() {
2474
- const anyInclude = this.include.indicationIds.length > 0 || this.include.activeSubstanceIds.length > 0 || this.include.atcClassIds.length > 0;
2495
+ const anyScope = this.scope.indicationIds.length > 0 || this.scope.atcClassIds.length > 0;
2496
+ const anyInclude = Object.values(this.include).some((ids) => ids.length > 0);
2475
2497
  const anyExclude = Object.values(this.exclude).some((ids) => ids.length > 0);
2476
2498
  const anyQuickFilter = this.quickFilters.sameGalenicForm || this.quickFilters.sameDosage || this.quickFilters.sameRouteOfAdministration;
2477
- return !anyInclude && !anyExclude && !anyQuickFilter;
2499
+ return !anyScope && !anyInclude && !anyExclude && !anyQuickFilter;
2478
2500
  }
2479
2501
  fetchAlternatives() {
2480
2502
  const productId = this.sourceProductId;
@@ -2486,6 +2508,7 @@ let TherapeuticAlternative = class extends i$1 {
2486
2508
  this.apiSearchError = null;
2487
2509
  this.lastRequestHadNoSelectedCriteria = this.hasNoSelectedCriteria();
2488
2510
  const body = {
2511
+ scope: this.scope,
2489
2512
  include: this.include,
2490
2513
  exclude: this.exclude,
2491
2514
  quickFilters: this.quickFilters,
@@ -2515,24 +2538,31 @@ let TherapeuticAlternative = class extends i$1 {
2515
2538
  this.sourceProduct = data.sourceProduct ?? null;
2516
2539
  this.results = data.results?.content ?? [];
2517
2540
  this.totalElements = data.results?.totalElements ?? 0;
2541
+ this.scopeFacets = TherapeuticAlternative.buildScopeFacets(data);
2518
2542
  this.includeFacets = TherapeuticAlternative.buildIncludeFacets(data);
2519
2543
  this.excludeFacets = TherapeuticAlternative.buildExcludeFacets(data);
2520
2544
  this.truncatedGroups = data.facets?.truncatedGroups ?? [];
2521
- if (!this.hasAppliedInitialInclude) {
2522
- this.hasAppliedInitialInclude = true;
2523
- this.include = TherapeuticAlternative.deriveInitialInclude(this.sourceProduct);
2524
- this.includeLabels = {
2525
- indications: labelsForIds(this.include.indicationIds, this.includeFacets.indications),
2526
- activeSubstances: labelsForIds(this.include.activeSubstanceIds, this.includeFacets.activeSubstances),
2527
- atcClasses: labelsForIds(this.include.atcClassIds, this.includeFacets.atcClasses)
2545
+ if (!this.hasAppliedInitialScope) {
2546
+ this.hasAppliedInitialScope = true;
2547
+ this.scope = TherapeuticAlternative.deriveInitialScope(this.sourceProduct);
2548
+ this.scopeLabels = {
2549
+ indications: labelsForIds(this.scope.indicationIds, this.scopeFacets.indications),
2550
+ atcClasses: labelsForIds(this.scope.atcClassIds, this.scopeFacets.atcClasses)
2528
2551
  };
2529
2552
  }
2530
2553
  }
2554
+ static buildScopeFacets(data) {
2555
+ return {
2556
+ indications: data.facets?.scope?.indications ?? [],
2557
+ atcClasses: data.facets?.scope?.atcClasses ?? []
2558
+ };
2559
+ }
2531
2560
  static buildIncludeFacets(data) {
2532
2561
  return {
2533
- indications: data.facets?.include?.indications ?? [],
2534
2562
  activeSubstances: data.facets?.include?.activeSubstances ?? [],
2535
- atcClasses: data.facets?.include?.atcClasses ?? []
2563
+ galenicForms: data.facets?.include?.galenicForms ?? [],
2564
+ dosages: data.facets?.include?.dosages ?? [],
2565
+ routesOfAdministration: data.facets?.include?.routesOfAdministration ?? []
2536
2566
  };
2537
2567
  }
2538
2568
  static buildExcludeFacets(data) {
@@ -2545,38 +2575,37 @@ let TherapeuticAlternative = class extends i$1 {
2545
2575
  precautions: data.facets?.exclude?.precautions ?? []
2546
2576
  };
2547
2577
  }
2548
- static deriveInitialInclude(sourceProduct) {
2578
+ static deriveInitialScope(sourceProduct) {
2549
2579
  const codesOf = (list) => (list ?? []).map((item) => item.code).filter((code) => !!code);
2550
2580
  return {
2551
2581
  indicationIds: codesOf(sourceProduct?.extendedProperties?.keywords?.indicationsKeywords),
2552
- activeSubstanceIds: codesOf(sourceProduct?.extendedProperties?.composition?.activeSubstances),
2553
- // Contrairement à indications/activeSubstances (des listes plates de valeurs propres au
2554
- // produit), sourceProduct.properties.atcClasses est un fil d'Ariane hiérarchique (classe >
2555
- // sous-classe > ... > code le plus spécifique, ex: M, M01, M01A, M01AX, M01AX02) — les
2556
- // mapper tous comme ids cochés n'a pas de sens (5 "sélections" pour UNE seule classification),
2557
- // et envoie en plus des codes ancêtres bien trop larges (ex: "M" = tout le système
2558
- // musculo-squelettique) dans la requête, invisibles dans l'UI puisque sans label correspondant
2559
- // dans includeFacets.atcClasses. Rien n'est pré-coché pour ce groupe : le backend applique déjà
2560
- // son propre défaut sur atcClassIds vide (voir le commentaire plus haut sur fetchAlternatives).
2582
+ // Contrairement à indications (une liste plate de valeurs propres au produit),
2583
+ // sourceProduct.properties.atcClasses est un fil d'Ariane hiérarchique (classe > sous-classe
2584
+ // > ... > code le plus spécifique, ex: M, M01, M01A, M01AX, M01AX02) — les mapper tous comme
2585
+ // ids cochés n'a pas de sens (5 "sélections" pour UNE seule classification), et envoie en
2586
+ // plus des codes ancêtres bien trop larges (ex: "M" = tout le système musculo-squelettique)
2587
+ // dans la requête, invisibles dans l'UI puisque sans label correspondant dans
2588
+ // scopeFacets.atcClasses. Rien n'est pré-coché pour ce groupe : le backend applique déjà son
2589
+ // propre défaut sur atcClassIds vide (voir le commentaire plus haut sur fetchAlternatives).
2561
2590
  atcClassIds: []
2562
2591
  };
2563
2592
  }
2564
- // label n'est utilisé que sur la branche "coche" (pour alimenter includeLabels/excludeLabels —
2565
- // voir emptyIncludeLabels) ; sur la branche "décoche" l'id est simplement retiré des deux côtés,
2566
- // que l'appelant soit une checkbox du panel (qui a le label sous la main) ou un chip de rappel
2567
- // (qui décoche un id dont le panel courant n'a peut-être même plus trace).
2568
- toggleInclude(group, id, label) {
2569
- const current = this.include[group.idsField] ?? [];
2593
+ // label n'est utilisé que sur la branche "coche" (pour alimenter scopeLabels/includeLabels/
2594
+ // excludeLabels — voir emptyScopeLabels) ; sur la branche "décoche" l'id est simplement retiré
2595
+ // des deux côtés, que l'appelant soit une checkbox du panel (qui a le label sous la main) ou un
2596
+ // chip de rappel (qui décoche un id dont le panel courant n'a peut-être même plus trace).
2597
+ toggleScope(group, id, label) {
2598
+ const current = this.scope[group.idsField] ?? [];
2570
2599
  const checking = !current.includes(id);
2571
2600
  const next = checking ? [...current, id] : current.filter((existing) => existing !== id);
2572
- this.include = { ...this.include, [group.idsField]: next };
2573
- const nextLabels = { ...this.includeLabels[group.key] };
2601
+ this.scope = { ...this.scope, [group.idsField]: next };
2602
+ const nextLabels = { ...this.scopeLabels[group.key] };
2574
2603
  if (checking) {
2575
2604
  nextLabels[id] = label;
2576
2605
  } else {
2577
2606
  delete nextLabels[id];
2578
2607
  }
2579
- this.includeLabels = { ...this.includeLabels, [group.key]: nextLabels };
2608
+ this.scopeLabels = { ...this.scopeLabels, [group.key]: nextLabels };
2580
2609
  this.pendingToggleIds = new Set(this.pendingToggleIds).add(id);
2581
2610
  this.scheduleRefetch();
2582
2611
  }
@@ -2595,25 +2624,66 @@ let TherapeuticAlternative = class extends i$1 {
2595
2624
  this.pendingToggleIds = new Set(this.pendingToggleIds).add(id);
2596
2625
  this.scheduleRefetch();
2597
2626
  }
2627
+ // Checking a specific value in an include group takes over from its corresponding quick filter,
2628
+ // when it has one (see IncludeGroupConfig.quickFilterKey -- activeSubstances has none) --
2629
+ // leaving both on could silently AND into an impossible/contradictory combination (e.g. quick
2630
+ // filter pinned to the source's own "Comprimé" while the checklist asks for "Solution
2631
+ // injectable") rather than the refinement the user asked for. Same rationale as
2632
+ // toggleQuickFilter below, the other direction.
2633
+ toggleInclude(group, id, label) {
2634
+ const current = this.include[group.idsField] ?? [];
2635
+ const checking = !current.includes(id);
2636
+ const next = checking ? [...current, id] : current.filter((existing) => existing !== id);
2637
+ this.include = { ...this.include, [group.idsField]: next };
2638
+ const nextLabels = { ...this.includeLabels[group.key] };
2639
+ if (checking) {
2640
+ nextLabels[id] = label;
2641
+ } else {
2642
+ delete nextLabels[id];
2643
+ }
2644
+ this.includeLabels = { ...this.includeLabels, [group.key]: nextLabels };
2645
+ if (checking && group.quickFilterKey && this.quickFilters[group.quickFilterKey]) {
2646
+ this.quickFilters = { ...this.quickFilters, [group.quickFilterKey]: false };
2647
+ }
2648
+ this.pendingToggleIds = new Set(this.pendingToggleIds).add(id);
2649
+ this.scheduleRefetch();
2650
+ }
2598
2651
  toggleQuickFilter(key) {
2599
- this.quickFilters = { ...this.quickFilters, [key]: !this.quickFilters[key] };
2652
+ const turningOn = !this.quickFilters[key];
2653
+ this.quickFilters = { ...this.quickFilters, [key]: turningOn };
2654
+ if (turningOn) {
2655
+ const group = INCLUDE_GROUPS.find((g2) => g2.quickFilterKey === key);
2656
+ if (group && this.include[group.idsField].length > 0) {
2657
+ this.include = { ...this.include, [group.idsField]: [] };
2658
+ this.includeLabels = { ...this.includeLabels, [group.key]: {} };
2659
+ }
2660
+ }
2600
2661
  this.scheduleRefetch();
2601
2662
  }
2602
2663
  // The exclude "activeSubstances" group's own backend/truncatedGroups key is
2603
2664
  // "excludeActiveSubstances" (see SearchCustomService.assignFacetValues) -- distinct from the
2604
- // inclusion dimension of the same frontend name, so it can't reuse ExcludeGroupKey as-is.
2665
+ // include dimension of the same frontend name, so it can't reuse ExcludeGroupKey as-is.
2605
2666
  // Every other exclude group's backend key matches its frontend key 1:1.
2606
2667
  exclusionTruncatedGroupKey(key) {
2607
2668
  return key === "activeSubstances" ? "excludeActiveSubstances" : key;
2608
2669
  }
2609
- toggleInclusionPanel(key) {
2610
- this.openInclusionGroup = this.openInclusionGroup === key ? null : key;
2670
+ // Same rationale as exclusionTruncatedGroupKey, the include side: "activeSubstances"' own
2671
+ // backend key is "includeActiveSubstances".
2672
+ includeTruncatedGroupKey(key) {
2673
+ return key === "activeSubstances" ? "includeActiveSubstances" : key;
2674
+ }
2675
+ toggleScopePanel(key) {
2676
+ this.openScopeGroup = this.openScopeGroup === key ? null : key;
2611
2677
  this.checklistSearchTerm = "";
2612
2678
  }
2613
2679
  toggleExclusionPanel(key) {
2614
2680
  this.openExclusionGroup = this.openExclusionGroup === key ? null : key;
2615
2681
  this.checklistSearchTerm = "";
2616
2682
  }
2683
+ toggleIncludePanel(key) {
2684
+ this.openIncludeGroup = this.openIncludeGroup === key ? null : key;
2685
+ this.checklistSearchTerm = "";
2686
+ }
2617
2687
  // "Afficher plus" for one truncated group's checklist -- see expandFacetGroupKey. Triggers an
2618
2688
  // immediate refetch (unlike opening/closing a panel) since this is the one action that actually
2619
2689
  // needs a new response to have an effect.
@@ -2764,8 +2834,8 @@ let TherapeuticAlternative = class extends i$1 {
2764
2834
  }
2765
2835
  // Only rendered past CHECKLIST_SEARCH_THRESHOLD (see renderChecklistPanel) -- below that, typing
2766
2836
  // is slower than just scanning the list by eye. checklistSearchTerm is reset whenever a different
2767
- // group/section opens (toggleInclusionPanel/toggleExclusionPanel) so a leftover term from a
2768
- // previous panel can't silently hide everything in the next one.
2837
+ // group/section opens (toggleScopePanel/toggleExclusionPanel/toggleIncludePanel) so a leftover
2838
+ // term from a previous panel can't silently hide everything in the next one.
2769
2839
  renderChecklistSearch(testIdPrefix) {
2770
2840
  return b`
2771
2841
  <bcbd-input
@@ -2781,9 +2851,9 @@ let TherapeuticAlternative = class extends i$1 {
2781
2851
  }
2782
2852
  // Rappel des valeurs actuellement cochées pour TOUTE la section (tous ses groupes confondus),
2783
2853
  // toujours affiché dès qu'il y en a — indépendamment de quel panel est ouvert, voire qu'aucun ne
2784
- // le soit. C'est la vue "source de vérité" (includeLabels/excludeLabels) plutôt que le panel
2785
- // ouvert : une valeur cochée reste visible et décochable ici même si un refetch a fait disparaître
2786
- // sa ligne du panel courant (voir emptyIncludeLabels).
2854
+ // le soit. C'est la vue "source de vérité" (scopeLabels/includeLabels/excludeLabels) plutôt que
2855
+ // le panel ouvert : une valeur cochée reste visible et décochable ici même si un refetch a fait
2856
+ // disparaître sa ligne du panel courant (voir emptyScopeLabels).
2787
2857
  renderSelectedChips(labelsByGroup, onRemove, testIdPrefix) {
2788
2858
  const entries = [];
2789
2859
  for (const [groupKey, labels] of Object.entries(labelsByGroup)) {
@@ -2830,36 +2900,36 @@ let TherapeuticAlternative = class extends i$1 {
2830
2900
  </button>
2831
2901
  `;
2832
2902
  }
2833
- renderInclusionSection() {
2834
- const chips = this.renderSelectedChips(this.includeLabels, (groupKey, id) => {
2835
- const group = INCLUSION_GROUPS.find((g2) => g2.key === groupKey);
2903
+ renderScopeSection() {
2904
+ const chips = this.renderSelectedChips(this.scopeLabels, (groupKey, id) => {
2905
+ const group = SCOPE_GROUPS.find((g2) => g2.key === groupKey);
2836
2906
  if (group) {
2837
- this.toggleInclude(group, id, "");
2907
+ this.toggleScope(group, id, "");
2838
2908
  }
2839
- }, "inclusion");
2840
- if (this.sectionCollapsed.inclusion) {
2909
+ }, "scope");
2910
+ if (this.sectionCollapsed.scope) {
2841
2911
  return chips;
2842
2912
  }
2843
- const openGroup = INCLUSION_GROUPS.find((group) => group.key === this.openInclusionGroup);
2844
- const pills = INCLUSION_GROUPS.map((group) => this.renderPill(
2913
+ const openGroup = SCOPE_GROUPS.find((group) => group.key === this.openScopeGroup);
2914
+ const pills = SCOPE_GROUPS.map((group) => this.renderPill(
2845
2915
  group.label,
2846
- this.include[group.idsField].length > 0,
2847
- group.key === this.openInclusionGroup,
2848
- () => this.toggleInclusionPanel(group.key),
2849
- `inclusion-pill-${group.key}`
2916
+ this.scope[group.idsField].length > 0,
2917
+ group.key === this.openScopeGroup,
2918
+ () => this.toggleScopePanel(group.key),
2919
+ `scope-pill-${group.key}`
2850
2920
  ));
2851
2921
  return b`
2852
- <div class="${PREFIX}-therapeutic-alternative__pills" data-test-id="inclusion-pills">
2922
+ <div class="${PREFIX}-therapeutic-alternative__pills" data-test-id="scope-pills">
2853
2923
  ${pills}
2854
2924
  </div>
2855
2925
  ${chips}
2856
2926
  ${openGroup ? this.renderChecklistPanel(
2857
- this.includeFacets[openGroup.key],
2858
- this.include[openGroup.idsField],
2927
+ this.scopeFacets[openGroup.key],
2928
+ this.scope[openGroup.idsField],
2859
2929
  true,
2860
2930
  INCLUDE_COUNT_TOOLTIP,
2861
- (id, label) => this.toggleInclude(openGroup, id, label),
2862
- `inclusion-${openGroup.key}`,
2931
+ (id, label) => this.toggleScope(openGroup, id, label),
2932
+ `scope-${openGroup.key}`,
2863
2933
  openGroup.key === "atcClasses",
2864
2934
  // Already expanded == clicking again would send the exact same expandFacetGroup and get
2865
2935
  // the exact same (still truncated, if it's a group with more than FACET_VALUES_EXPANDED_SIZE
@@ -2908,6 +2978,44 @@ let TherapeuticAlternative = class extends i$1 {
2908
2978
  ) : A}
2909
2979
  `;
2910
2980
  }
2981
+ renderIncludeSection() {
2982
+ const chips = this.renderSelectedChips(this.includeLabels, (groupKey, id) => {
2983
+ const group = INCLUDE_GROUPS.find((g2) => g2.key === groupKey);
2984
+ if (group) {
2985
+ this.toggleInclude(group, id, "");
2986
+ }
2987
+ }, "include");
2988
+ if (this.sectionCollapsed.include) {
2989
+ return chips;
2990
+ }
2991
+ const openGroup = INCLUDE_GROUPS.find((group) => group.key === this.openIncludeGroup);
2992
+ const pills = INCLUDE_GROUPS.map((group) => this.renderPill(
2993
+ group.label,
2994
+ this.include[group.idsField].length > 0,
2995
+ group.key === this.openIncludeGroup,
2996
+ () => this.toggleIncludePanel(group.key),
2997
+ `include-pill-${group.key}`
2998
+ ));
2999
+ const truncatedGroupKey = openGroup ? this.includeTruncatedGroupKey(openGroup.key) : null;
3000
+ const canExpand = truncatedGroupKey !== null && this.truncatedGroups.includes(truncatedGroupKey) && this.expandFacetGroupKey !== truncatedGroupKey;
3001
+ return b`
3002
+ <div class="${PREFIX}-therapeutic-alternative__pills" data-test-id="include-pills">
3003
+ ${pills}
3004
+ </div>
3005
+ ${chips}
3006
+ ${openGroup ? this.renderChecklistPanel(
3007
+ this.includeFacets[openGroup.key],
3008
+ this.include[openGroup.idsField],
3009
+ true,
3010
+ INCLUDE_COUNT_TOOLTIP,
3011
+ (id, label) => this.toggleInclude(openGroup, id, label),
3012
+ `include-${openGroup.key}`,
3013
+ false,
3014
+ canExpand,
3015
+ () => this.requestMoreFacetValues(truncatedGroupKey)
3016
+ ) : A}
3017
+ `;
3018
+ }
2911
3019
  renderQuickFiltersSection() {
2912
3020
  if (this.sectionCollapsed.quickFilters) {
2913
3021
  return A;
@@ -2952,8 +3060,8 @@ let TherapeuticAlternative = class extends i$1 {
2952
3060
  // Turns a result's raw matchReasons into short bullet lines, e.g. "- indiqué pour Arthrose" or
2953
3061
  // "- contient Bisoprolol (synonyme de Métoprolol)" -- label/synonymLabel come straight from the
2954
3062
  // backend (resolved from the source/matching product's own documents), not looked up in
2955
- // includeFacets: that only ever covers the request's criterion codes, never synonymCode, which
2956
- // is a code found on this specific result product, not a criterion at all.
3063
+ // scopeFacets/includeFacets: those only ever cover the request's criterion codes, never
3064
+ // synonymCode, which is a code found on this specific result product, not a criterion at all.
2957
3065
  matchReasonLabels(result) {
2958
3066
  const labels = (result.matchReasons ?? []).map((reason) => {
2959
3067
  const dimension = reason.dimension ?? "";
@@ -3059,13 +3167,18 @@ let TherapeuticAlternative = class extends i$1 {
3059
3167
  </bcbd-alert>
3060
3168
  </div>` : ""}
3061
3169
 
3062
- <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="inclusion-section">
3063
- ${this.renderSectionHeader("Rechercher une alternative", "inclusion")}
3064
- ${this.renderInclusionSection()}
3170
+ <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="scope-section">
3171
+ ${this.renderSectionHeader("Rechercher une alternative", "scope")}
3172
+ ${this.renderScopeSection()}
3173
+ </div>
3174
+
3175
+ <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="include-section">
3176
+ ${this.renderSectionHeader("Critères d'inclusion", "include")}
3177
+ ${this.renderIncludeSection()}
3065
3178
  </div>
3066
3179
 
3067
3180
  <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="exclusion-section">
3068
- ${this.renderSectionHeader("Affiner (exclure des critères)", "exclusion")}
3181
+ ${this.renderSectionHeader("Critères d'exclusion", "exclusion")}
3069
3182
  ${this.renderExclusionSection()}
3070
3183
  </div>
3071
3184
 
@@ -3118,6 +3231,9 @@ __decorateClass([
3118
3231
  __decorateClass([
3119
3232
  r()
3120
3233
  ], TherapeuticAlternative.prototype, "currentPage", 2);
3234
+ __decorateClass([
3235
+ r()
3236
+ ], TherapeuticAlternative.prototype, "scope", 2);
3121
3237
  __decorateClass([
3122
3238
  r()
3123
3239
  ], TherapeuticAlternative.prototype, "include", 2);
@@ -3127,6 +3243,9 @@ __decorateClass([
3127
3243
  __decorateClass([
3128
3244
  r()
3129
3245
  ], TherapeuticAlternative.prototype, "quickFilters", 2);
3246
+ __decorateClass([
3247
+ r()
3248
+ ], TherapeuticAlternative.prototype, "scopeFacets", 2);
3130
3249
  __decorateClass([
3131
3250
  r()
3132
3251
  ], TherapeuticAlternative.prototype, "includeFacets", 2);
@@ -3139,6 +3258,9 @@ __decorateClass([
3139
3258
  __decorateClass([
3140
3259
  r()
3141
3260
  ], TherapeuticAlternative.prototype, "expandFacetGroupKey", 2);
3261
+ __decorateClass([
3262
+ r()
3263
+ ], TherapeuticAlternative.prototype, "scopeLabels", 2);
3142
3264
  __decorateClass([
3143
3265
  r()
3144
3266
  ], TherapeuticAlternative.prototype, "includeLabels", 2);
@@ -3147,10 +3269,13 @@ __decorateClass([
3147
3269
  ], TherapeuticAlternative.prototype, "excludeLabels", 2);
3148
3270
  __decorateClass([
3149
3271
  r()
3150
- ], TherapeuticAlternative.prototype, "openInclusionGroup", 2);
3272
+ ], TherapeuticAlternative.prototype, "openScopeGroup", 2);
3151
3273
  __decorateClass([
3152
3274
  r()
3153
3275
  ], TherapeuticAlternative.prototype, "openExclusionGroup", 2);
3276
+ __decorateClass([
3277
+ r()
3278
+ ], TherapeuticAlternative.prototype, "openIncludeGroup", 2);
3154
3279
  __decorateClass([
3155
3280
  r()
3156
3281
  ], TherapeuticAlternative.prototype, "checklistSearchTerm", 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudebernard/therapeutic-alternative-element",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "main": "dist/therapeutic-alternative.es.js",
5
5
  "module": "dist/therapeutic-alternative.es.js",
6
6
  "types": "dist/therapeutic-alternative.d.ts",