@claudebernard/therapeutic-alternative-element 2.1.2 → 2.3.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,22 @@ 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 quickFilterFacets;
30
+ private scopeFacets;
27
31
  private includeFacets;
28
32
  private excludeFacets;
29
33
  private truncatedGroups;
30
34
  private expandFacetGroupKey;
35
+ private scopeLabels;
31
36
  private includeLabels;
32
37
  private excludeLabels;
33
- private openInclusionGroup;
38
+ private openScopeGroup;
34
39
  private openExclusionGroup;
40
+ private openIncludeGroup;
35
41
  private checklistSearchTerm;
36
42
  private sectionCollapsed;
37
43
  private apiSearchError;
@@ -40,28 +46,36 @@ export declare class TherapeuticAlternative extends LitElement {
40
46
  private apiReplaceError;
41
47
  private apiReplaceLoading;
42
48
  private missingPrescriptionError;
43
- private hasAppliedInitialInclude;
49
+ private hasAppliedInitialScope;
44
50
  private refetchTimer;
45
51
  private requestSeq;
46
52
  private get currentMedication();
47
53
  private get currentMedicationRequest();
48
54
  private get sourceProductId();
49
55
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
56
+ private resetAllCriteria;
50
57
  disconnectedCallback(): void;
51
58
  private clearScheduledRefetch;
52
59
  private scheduleRefetch;
53
60
  private hasNoSelectedCriteria;
54
61
  private fetchAlternatives;
55
62
  private applyResponse;
63
+ private static buildScopeFacets;
56
64
  private static buildIncludeFacets;
57
65
  private static buildExcludeFacets;
58
- private static deriveInitialInclude;
59
- private toggleInclude;
66
+ private static codesOf;
67
+ private static deriveInitialScope;
68
+ private includePossessedIds;
69
+ private excludePossessedIds;
70
+ private toggleScope;
60
71
  private toggleExclude;
72
+ private toggleInclude;
61
73
  private toggleQuickFilter;
62
74
  private exclusionTruncatedGroupKey;
63
- private toggleInclusionPanel;
75
+ private includeTruncatedGroupKey;
76
+ private toggleScopePanel;
64
77
  private toggleExclusionPanel;
78
+ private toggleIncludePanel;
65
79
  private requestMoreFacetValues;
66
80
  private toggleSection;
67
81
  private cancelAlternative;
@@ -71,12 +85,15 @@ export declare class TherapeuticAlternative extends LitElement {
71
85
  private renderChecklistSearch;
72
86
  private renderSelectedChips;
73
87
  private renderPill;
74
- private renderInclusionSection;
88
+ private renderScopeSection;
75
89
  private renderExclusionSection;
90
+ private renderIncludeSection;
76
91
  private renderQuickFiltersSection;
92
+ private renderQuickFilterCount;
77
93
  private renderSectionHeader;
78
94
  private matchReasonLabels;
79
95
  private renderResults;
96
+ private renderTitle;
80
97
  render(): TemplateResult<1>;
81
98
  static styles: CSSResult;
82
99
  }
@@ -2297,33 +2297,42 @@ 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" },
2307
2306
  { key: "notableExcipients", idsField: "notableExcipientIds", label: "Excipients à effet notoire" },
2308
2307
  { key: "excipients", idsField: "excipientIds", label: "Excipients" },
2309
2308
  { key: "contraindications", idsField: "contraindicationIds", label: "Utilisation contre-indiquée" },
2310
- { key: "cautions", idsField: "cautionIds", label: "Mises en garde" },
2311
- { key: "precautions", idsField: "precautionIds", label: "Précaution d'emploi" }
2309
+ { key: "cautions", idsField: "cautionIds", label: "Utilisation déconseillée" },
2310
+ { key: "precautions", idsField: "precautionIds", label: "Précaution d'emploi" },
2311
+ { key: "sideEffects", idsField: "sideEffectIds", label: "Effets indésirables" }
2312
2312
  ];
2313
2313
  const QUICK_FILTERS = [
2314
2314
  { key: "sameGalenicForm", label: "Même forme galénique" },
2315
2315
  { key: "sameDosage", label: "Même dosage" },
2316
2316
  { key: "sameRouteOfAdministration", label: "Même voie d'administration" }
2317
2317
  ];
2318
+ const INCLUDE_GROUPS = [
2319
+ { key: "activeSubstances", idsField: "activeSubstanceIds", label: "Principes actifs" },
2320
+ { key: "galenicForms", idsField: "galenicFormIds", label: "Forme galénique", quickFilterKey: "sameGalenicForm" },
2321
+ { key: "dosages", idsField: "dosageIds", label: "Dosage", quickFilterKey: "sameDosage" },
2322
+ { key: "routesOfAdministration", idsField: "routeOfAdministrationIds", label: "Voie d'administration", quickFilterKey: "sameRouteOfAdministration" }
2323
+ ];
2318
2324
  const INCLUDE_COUNT_TOOLTIP = "Nombre de résultats restants avec cette valeur";
2319
2325
  const EXCLUDE_COUNT_TOOLTIP = "Nombre de résultats retirés avec cette valeur";
2320
2326
  const MATCH_REASON_VERB_BY_DIMENSION = {
2321
2327
  indication: "indiqué pour",
2322
2328
  activeSubstance: "contient"
2323
2329
  };
2324
- 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
+ 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__source .claudebernard-therapeutic-alternative-title {\n margin-bottom: 0;\n}\n.claudebernard-therapeutic-alternative__source-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\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__panel-row--possessed {\n font-weight: 700;\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__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}';
2331
+ function emptyScopeCriteria() {
2332
+ return { indicationIds: [], atcClassIds: [] };
2333
+ }
2325
2334
  function emptyIncludeCriteria() {
2326
- return { indicationIds: [], activeSubstanceIds: [], atcClassIds: [] };
2335
+ return { activeSubstanceIds: [], galenicFormIds: [], dosageIds: [], routeOfAdministrationIds: [] };
2327
2336
  }
2328
2337
  function emptyExcludeCriteria() {
2329
2338
  return {
@@ -2332,14 +2341,18 @@ function emptyExcludeCriteria() {
2332
2341
  excipientIds: [],
2333
2342
  contraindicationIds: [],
2334
2343
  cautionIds: [],
2335
- precautionIds: []
2344
+ precautionIds: [],
2345
+ sideEffectIds: []
2336
2346
  };
2337
2347
  }
2338
2348
  function emptyQuickFilters() {
2339
2349
  return { sameGalenicForm: false, sameDosage: false, sameRouteOfAdministration: false };
2340
2350
  }
2351
+ function emptyScopeFacets() {
2352
+ return { indications: [], atcClasses: [] };
2353
+ }
2341
2354
  function emptyIncludeFacets() {
2342
- return { indications: [], activeSubstances: [], atcClasses: [] };
2355
+ return { activeSubstances: [], galenicForms: [], dosages: [], routesOfAdministration: [] };
2343
2356
  }
2344
2357
  function emptyExcludeFacets() {
2345
2358
  return {
@@ -2348,11 +2361,15 @@ function emptyExcludeFacets() {
2348
2361
  excipients: [],
2349
2362
  contraindications: [],
2350
2363
  cautions: [],
2351
- precautions: []
2364
+ precautions: [],
2365
+ sideEffects: []
2352
2366
  };
2353
2367
  }
2368
+ function emptyScopeLabels() {
2369
+ return { indications: {}, atcClasses: {} };
2370
+ }
2354
2371
  function emptyIncludeLabels() {
2355
- return { indications: {}, activeSubstances: {}, atcClasses: {} };
2372
+ return { activeSubstances: {}, galenicForms: {}, dosages: {}, routesOfAdministration: {} };
2356
2373
  }
2357
2374
  function labelsForIds(ids, values) {
2358
2375
  const labelById = new Map(values.filter((v2) => v2.id).map((v2) => [v2.id, v2.label ?? v2.id]));
@@ -2372,7 +2389,8 @@ function emptyExcludeLabels() {
2372
2389
  excipients: {},
2373
2390
  contraindications: {},
2374
2391
  cautions: {},
2375
- precautions: {}
2392
+ precautions: {},
2393
+ sideEffects: {}
2376
2394
  };
2377
2395
  }
2378
2396
  var __defProp = Object.defineProperty;
@@ -2397,26 +2415,31 @@ let TherapeuticAlternative = class extends i$1 {
2397
2415
  this.totalElements = 0;
2398
2416
  this.lastRequestHadNoSelectedCriteria = true;
2399
2417
  this.currentPage = 1;
2418
+ this.scope = emptyScopeCriteria();
2400
2419
  this.include = emptyIncludeCriteria();
2401
2420
  this.exclude = emptyExcludeCriteria();
2402
2421
  this.quickFilters = emptyQuickFilters();
2422
+ this.quickFilterFacets = {};
2423
+ this.scopeFacets = emptyScopeFacets();
2403
2424
  this.includeFacets = emptyIncludeFacets();
2404
2425
  this.excludeFacets = emptyExcludeFacets();
2405
2426
  this.truncatedGroups = [];
2406
2427
  this.expandFacetGroupKey = null;
2428
+ this.scopeLabels = emptyScopeLabels();
2407
2429
  this.includeLabels = emptyIncludeLabels();
2408
2430
  this.excludeLabels = emptyExcludeLabels();
2409
- this.openInclusionGroup = null;
2431
+ this.openScopeGroup = null;
2410
2432
  this.openExclusionGroup = null;
2433
+ this.openIncludeGroup = null;
2411
2434
  this.checklistSearchTerm = "";
2412
- this.sectionCollapsed = { inclusion: false, exclusion: false, quickFilters: false };
2435
+ this.sectionCollapsed = { scope: false, include: false, exclusion: false, quickFilters: false };
2413
2436
  this.apiSearchError = null;
2414
2437
  this.apiSearchLoading = false;
2415
2438
  this.pendingToggleIds = /* @__PURE__ */ new Set();
2416
2439
  this.apiReplaceError = null;
2417
2440
  this.apiReplaceLoading = false;
2418
2441
  this.missingPrescriptionError = null;
2419
- this.hasAppliedInitialInclude = false;
2442
+ this.hasAppliedInitialScope = false;
2420
2443
  this.requestSeq = 0;
2421
2444
  }
2422
2445
  get currentMedication() {
@@ -2439,20 +2462,30 @@ let TherapeuticAlternative = class extends i$1 {
2439
2462
  });
2440
2463
  }
2441
2464
  if (changedProperties.has("medication") && this.currentMedication) {
2442
- this.clearScheduledRefetch();
2443
- this.include = emptyIncludeCriteria();
2444
- this.exclude = emptyExcludeCriteria();
2445
- this.includeLabels = emptyIncludeLabels();
2446
- this.excludeLabels = emptyExcludeLabels();
2447
- this.quickFilters = emptyQuickFilters();
2448
- this.openInclusionGroup = null;
2449
- this.openExclusionGroup = null;
2450
- this.expandFacetGroupKey = null;
2451
- this.currentPage = 1;
2452
- this.hasAppliedInitialInclude = false;
2453
- this.fetchAlternatives();
2465
+ this.resetAllCriteria();
2454
2466
  }
2455
2467
  }
2468
+ // Shared by a medication change (updated() above) and the user-facing "Réinitialiser" button:
2469
+ // both mean "start over from the initial state for this medication" -- clearing
2470
+ // hasAppliedInitialScope so the next response re-derives and re-applies scope's own pre-check
2471
+ // (see applyResponse) rather than leaving scope empty, which would NOT be the initial state.
2472
+ resetAllCriteria() {
2473
+ this.clearScheduledRefetch();
2474
+ this.scope = emptyScopeCriteria();
2475
+ this.include = emptyIncludeCriteria();
2476
+ this.exclude = emptyExcludeCriteria();
2477
+ this.scopeLabels = emptyScopeLabels();
2478
+ this.includeLabels = emptyIncludeLabels();
2479
+ this.excludeLabels = emptyExcludeLabels();
2480
+ this.quickFilters = emptyQuickFilters();
2481
+ this.openScopeGroup = null;
2482
+ this.openExclusionGroup = null;
2483
+ this.openIncludeGroup = null;
2484
+ this.expandFacetGroupKey = null;
2485
+ this.currentPage = 1;
2486
+ this.hasAppliedInitialScope = false;
2487
+ this.fetchAlternatives();
2488
+ }
2456
2489
  disconnectedCallback() {
2457
2490
  super.disconnectedCallback();
2458
2491
  this.clearScheduledRefetch();
@@ -2471,10 +2504,11 @@ let TherapeuticAlternative = class extends i$1 {
2471
2504
  // Mirrors the backend's own hasNoSelectedCriteria(request) -- must be evaluated against
2472
2505
  // whatever's about to be SENT, not read back later, see lastRequestHadNoSelectedCriteria.
2473
2506
  hasNoSelectedCriteria() {
2474
- const anyInclude = this.include.indicationIds.length > 0 || this.include.activeSubstanceIds.length > 0 || this.include.atcClassIds.length > 0;
2507
+ const anyScope = this.scope.indicationIds.length > 0 || this.scope.atcClassIds.length > 0;
2508
+ const anyInclude = Object.values(this.include).some((ids) => ids.length > 0);
2475
2509
  const anyExclude = Object.values(this.exclude).some((ids) => ids.length > 0);
2476
2510
  const anyQuickFilter = this.quickFilters.sameGalenicForm || this.quickFilters.sameDosage || this.quickFilters.sameRouteOfAdministration;
2477
- return !anyInclude && !anyExclude && !anyQuickFilter;
2511
+ return !anyScope && !anyInclude && !anyExclude && !anyQuickFilter;
2478
2512
  }
2479
2513
  fetchAlternatives() {
2480
2514
  const productId = this.sourceProductId;
@@ -2486,6 +2520,7 @@ let TherapeuticAlternative = class extends i$1 {
2486
2520
  this.apiSearchError = null;
2487
2521
  this.lastRequestHadNoSelectedCriteria = this.hasNoSelectedCriteria();
2488
2522
  const body = {
2523
+ scope: this.scope,
2489
2524
  include: this.include,
2490
2525
  exclude: this.exclude,
2491
2526
  quickFilters: this.quickFilters,
@@ -2515,68 +2550,122 @@ let TherapeuticAlternative = class extends i$1 {
2515
2550
  this.sourceProduct = data.sourceProduct ?? null;
2516
2551
  this.results = data.results?.content ?? [];
2517
2552
  this.totalElements = data.results?.totalElements ?? 0;
2553
+ this.scopeFacets = TherapeuticAlternative.buildScopeFacets(data);
2518
2554
  this.includeFacets = TherapeuticAlternative.buildIncludeFacets(data);
2519
2555
  this.excludeFacets = TherapeuticAlternative.buildExcludeFacets(data);
2556
+ this.quickFilterFacets = data.facets?.quickFilters ?? {};
2520
2557
  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)
2558
+ if (!this.hasAppliedInitialScope) {
2559
+ this.hasAppliedInitialScope = true;
2560
+ this.scope = TherapeuticAlternative.deriveInitialScope(this.sourceProduct);
2561
+ this.scopeLabels = {
2562
+ indications: labelsForIds(this.scope.indicationIds, this.scopeFacets.indications),
2563
+ atcClasses: labelsForIds(this.scope.atcClassIds, this.scopeFacets.atcClasses)
2528
2564
  };
2529
2565
  }
2530
2566
  }
2567
+ static buildScopeFacets(data) {
2568
+ return {
2569
+ indications: data.facets?.scope?.indications ?? [],
2570
+ atcClasses: data.facets?.scope?.atcClasses ?? []
2571
+ };
2572
+ }
2531
2573
  static buildIncludeFacets(data) {
2532
2574
  return {
2533
- indications: data.facets?.include?.indications ?? [],
2534
2575
  activeSubstances: data.facets?.include?.activeSubstances ?? [],
2535
- atcClasses: data.facets?.include?.atcClasses ?? []
2576
+ galenicForms: data.facets?.include?.galenicForms ?? [],
2577
+ dosages: data.facets?.include?.dosages ?? [],
2578
+ routesOfAdministration: data.facets?.include?.routesOfAdministration ?? []
2536
2579
  };
2537
2580
  }
2538
2581
  static buildExcludeFacets(data) {
2539
- return {
2540
- activeSubstances: data.facets?.exclude?.activeSubstances ?? [],
2541
- notableExcipients: data.facets?.exclude?.notableExcipients ?? [],
2542
- excipients: data.facets?.exclude?.excipients ?? [],
2543
- contraindications: data.facets?.exclude?.contraindications ?? [],
2544
- cautions: data.facets?.exclude?.cautions ?? [],
2545
- precautions: data.facets?.exclude?.precautions ?? []
2546
- };
2582
+ const facets = emptyExcludeFacets();
2583
+ for (const group of EXCLUSION_GROUPS) {
2584
+ facets[group.key] = data.facets?.exclude?.[group.key] ?? [];
2585
+ }
2586
+ return facets;
2547
2587
  }
2548
- static deriveInitialInclude(sourceProduct) {
2549
- const codesOf = (list) => (list ?? []).map((item) => item.code).filter((code) => !!code);
2588
+ static codesOf(list) {
2589
+ return (list ?? []).map((item) => item.code).filter((code) => !!code);
2590
+ }
2591
+ static deriveInitialScope(sourceProduct) {
2550
2592
  return {
2551
- 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).
2593
+ indicationIds: TherapeuticAlternative.codesOf(sourceProduct?.extendedProperties?.keywords?.indicationsKeywords),
2594
+ // Contrairement à indications (une liste plate de valeurs propres au produit),
2595
+ // sourceProduct.properties.atcClasses est un fil d'Ariane hiérarchique (classe > sous-classe
2596
+ // > ... > code le plus spécifique, ex: M, M01, M01A, M01AX, M01AX02) — les mapper tous comme
2597
+ // ids cochés n'a pas de sens (5 "sélections" pour UNE seule classification), et envoie en
2598
+ // plus des codes ancêtres bien trop larges (ex: "M" = tout le système musculo-squelettique)
2599
+ // dans la requête, invisibles dans l'UI puisque sans label correspondant dans
2600
+ // scopeFacets.atcClasses. Rien n'est pré-coché pour ce groupe : le backend applique déjà son
2601
+ // propre défaut sur atcClassIds vide (voir le commentaire plus haut sur fetchAlternatives).
2561
2602
  atcClassIds: []
2562
2603
  };
2563
2604
  }
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] ?? [];
2605
+ // The ids to bold in an include checklist panel (see renderChecklistPanel's possessedIds param)
2606
+ // -- what the SOURCE product itself already has for that dimension, not what's currently
2607
+ // selected. galenicForms/dosages are single-valued on the source (a product has exactly one), so
2608
+ // each contributes at most one id; activeSubstances/routesOfAdministration can be multi-valued.
2609
+ // dosages is a best-effort string match (source's raw productDosage against the facet's own
2610
+ // dosage-string ids) -- it can occasionally miss on formatting, unlike the others' exact codes.
2611
+ includePossessedIds(key) {
2612
+ const properties = this.sourceProduct?.properties;
2613
+ const composition = this.sourceProduct?.extendedProperties?.composition;
2614
+ switch (key) {
2615
+ case "activeSubstances":
2616
+ return new Set(TherapeuticAlternative.codesOf(composition?.activeSubstances));
2617
+ case "galenicForms":
2618
+ return properties?.galenicFormCode ? /* @__PURE__ */ new Set([properties.galenicFormCode]) : /* @__PURE__ */ new Set();
2619
+ case "dosages":
2620
+ return properties?.productDosage ? /* @__PURE__ */ new Set([properties.productDosage]) : /* @__PURE__ */ new Set();
2621
+ case "routesOfAdministration":
2622
+ return new Set(TherapeuticAlternative.codesOf(this.sourceProduct?.extendedProperties?.routes));
2623
+ default:
2624
+ return /* @__PURE__ */ new Set();
2625
+ }
2626
+ }
2627
+ // Same idea as includePossessedIds, for the exclude checklist panels: the exclude groups' own
2628
+ // values come from the whole candidate pool (see SearchCustomService.addExcludeMustNotQueries),
2629
+ // not just the source product, so "does the source itself already have this excipient/
2630
+ // contraindication/..." is a real, non-trivial question worth answering here too.
2631
+ excludePossessedIds(key) {
2632
+ const composition = this.sourceProduct?.extendedProperties?.composition;
2633
+ const keywords = this.sourceProduct?.extendedProperties?.keywords;
2634
+ switch (key) {
2635
+ case "activeSubstances":
2636
+ return new Set(TherapeuticAlternative.codesOf(composition?.activeSubstances));
2637
+ case "notableExcipients":
2638
+ return new Set(TherapeuticAlternative.codesOf(composition?.excipientsWithKnownEffects));
2639
+ case "excipients":
2640
+ return new Set(TherapeuticAlternative.codesOf(composition?.excipients));
2641
+ case "contraindications":
2642
+ return new Set(TherapeuticAlternative.codesOf(keywords?.contraindicationsKeywords));
2643
+ case "cautions":
2644
+ return new Set(TherapeuticAlternative.codesOf(keywords?.contraindicationsNotRecommendedKeywords));
2645
+ case "precautions":
2646
+ return new Set(TherapeuticAlternative.codesOf(keywords?.precautionsForUseKeywords));
2647
+ case "sideEffects":
2648
+ return new Set(TherapeuticAlternative.codesOf(keywords?.sideEffectsKeywords));
2649
+ default:
2650
+ return /* @__PURE__ */ new Set();
2651
+ }
2652
+ }
2653
+ // label n'est utilisé que sur la branche "coche" (pour alimenter scopeLabels/includeLabels/
2654
+ // excludeLabels — voir emptyScopeLabels) ; sur la branche "décoche" l'id est simplement retiré
2655
+ // des deux côtés, que l'appelant soit une checkbox du panel (qui a le label sous la main) ou un
2656
+ // chip de rappel (qui décoche un id dont le panel courant n'a peut-être même plus trace).
2657
+ toggleScope(group, id, label) {
2658
+ const current = this.scope[group.idsField] ?? [];
2570
2659
  const checking = !current.includes(id);
2571
2660
  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] };
2661
+ this.scope = { ...this.scope, [group.idsField]: next };
2662
+ const nextLabels = { ...this.scopeLabels[group.key] };
2574
2663
  if (checking) {
2575
2664
  nextLabels[id] = label;
2576
2665
  } else {
2577
2666
  delete nextLabels[id];
2578
2667
  }
2579
- this.includeLabels = { ...this.includeLabels, [group.key]: nextLabels };
2668
+ this.scopeLabels = { ...this.scopeLabels, [group.key]: nextLabels };
2580
2669
  this.pendingToggleIds = new Set(this.pendingToggleIds).add(id);
2581
2670
  this.scheduleRefetch();
2582
2671
  }
@@ -2595,25 +2684,67 @@ let TherapeuticAlternative = class extends i$1 {
2595
2684
  this.pendingToggleIds = new Set(this.pendingToggleIds).add(id);
2596
2685
  this.scheduleRefetch();
2597
2686
  }
2687
+ // Checking a specific value in an include group takes over from its corresponding quick filter,
2688
+ // when it has one (see IncludeGroupConfig.quickFilterKey -- activeSubstances has none) --
2689
+ // leaving both on could silently AND into an impossible/contradictory combination (e.g. quick
2690
+ // filter pinned to the source's own "Comprimé" while the checklist asks for "Solution
2691
+ // injectable") rather than the refinement the user asked for. Same rationale as
2692
+ // toggleQuickFilter below, the other direction.
2693
+ toggleInclude(group, id, label) {
2694
+ const current = this.include[group.idsField] ?? [];
2695
+ const checking = !current.includes(id);
2696
+ const next = checking ? [...current, id] : current.filter((existing) => existing !== id);
2697
+ this.include = { ...this.include, [group.idsField]: next };
2698
+ const nextLabels = { ...this.includeLabels[group.key] };
2699
+ if (checking) {
2700
+ nextLabels[id] = label;
2701
+ } else {
2702
+ delete nextLabels[id];
2703
+ }
2704
+ this.includeLabels = { ...this.includeLabels, [group.key]: nextLabels };
2705
+ if (checking && group.quickFilterKey && this.quickFilters[group.quickFilterKey]) {
2706
+ this.quickFilters = { ...this.quickFilters, [group.quickFilterKey]: false };
2707
+ }
2708
+ this.pendingToggleIds = new Set(this.pendingToggleIds).add(id);
2709
+ this.scheduleRefetch();
2710
+ }
2598
2711
  toggleQuickFilter(key) {
2599
- this.quickFilters = { ...this.quickFilters, [key]: !this.quickFilters[key] };
2712
+ const turningOn = !this.quickFilters[key];
2713
+ this.quickFilters = { ...this.quickFilters, [key]: turningOn };
2714
+ if (turningOn) {
2715
+ const group = INCLUDE_GROUPS.find((g2) => g2.quickFilterKey === key);
2716
+ if (group && this.include[group.idsField].length > 0) {
2717
+ this.include = { ...this.include, [group.idsField]: [] };
2718
+ this.includeLabels = { ...this.includeLabels, [group.key]: {} };
2719
+ }
2720
+ }
2721
+ this.pendingToggleIds = new Set(this.pendingToggleIds).add(key);
2600
2722
  this.scheduleRefetch();
2601
2723
  }
2602
2724
  // The exclude "activeSubstances" group's own backend/truncatedGroups key is
2603
2725
  // "excludeActiveSubstances" (see SearchCustomService.assignFacetValues) -- distinct from the
2604
- // inclusion dimension of the same frontend name, so it can't reuse ExcludeGroupKey as-is.
2726
+ // include dimension of the same frontend name, so it can't reuse ExcludeGroupKey as-is.
2605
2727
  // Every other exclude group's backend key matches its frontend key 1:1.
2606
2728
  exclusionTruncatedGroupKey(key) {
2607
2729
  return key === "activeSubstances" ? "excludeActiveSubstances" : key;
2608
2730
  }
2609
- toggleInclusionPanel(key) {
2610
- this.openInclusionGroup = this.openInclusionGroup === key ? null : key;
2731
+ // Same rationale as exclusionTruncatedGroupKey, the include side: "activeSubstances"' own
2732
+ // backend key is "includeActiveSubstances".
2733
+ includeTruncatedGroupKey(key) {
2734
+ return key === "activeSubstances" ? "includeActiveSubstances" : key;
2735
+ }
2736
+ toggleScopePanel(key) {
2737
+ this.openScopeGroup = this.openScopeGroup === key ? null : key;
2611
2738
  this.checklistSearchTerm = "";
2612
2739
  }
2613
2740
  toggleExclusionPanel(key) {
2614
2741
  this.openExclusionGroup = this.openExclusionGroup === key ? null : key;
2615
2742
  this.checklistSearchTerm = "";
2616
2743
  }
2744
+ toggleIncludePanel(key) {
2745
+ this.openIncludeGroup = this.openIncludeGroup === key ? null : key;
2746
+ this.checklistSearchTerm = "";
2747
+ }
2617
2748
  // "Afficher plus" for one truncated group's checklist -- see expandFacetGroupKey. Triggers an
2618
2749
  // immediate refetch (unlike opening/closing a panel) since this is the one action that actually
2619
2750
  // needs a new response to have an effect.
@@ -2691,7 +2822,7 @@ let TherapeuticAlternative = class extends i$1 {
2691
2822
  this.apiReplaceLoading = false;
2692
2823
  }
2693
2824
  }
2694
- renderChecklistPanel(items, selectedIds, showCount, countTooltip, onToggle, testIdPrefix, showCode = false, truncated = false, onExpand) {
2825
+ renderChecklistPanel(items, selectedIds, showCount, countTooltip, onToggle, testIdPrefix, showCode = false, truncated = false, onExpand, possessedIds = /* @__PURE__ */ new Set()) {
2695
2826
  const visibleItems = items.filter((item) => {
2696
2827
  if (item.id !== void 0 && this.pendingToggleIds.has(item.id)) {
2697
2828
  return true;
@@ -2718,7 +2849,8 @@ let TherapeuticAlternative = class extends i$1 {
2718
2849
  const pending = this.pendingToggleIds.has(item.id);
2719
2850
  const rowClasses = [
2720
2851
  `${PREFIX}-therapeutic-alternative__panel-row`,
2721
- pending ? `${PREFIX}-therapeutic-alternative__panel-row--pending` : ""
2852
+ pending ? `${PREFIX}-therapeutic-alternative__panel-row--pending` : "",
2853
+ possessedIds.has(item.id) ? `${PREFIX}-therapeutic-alternative__panel-row--possessed` : ""
2722
2854
  ].join(" ");
2723
2855
  return b`
2724
2856
  <div class=${rowClasses}>
@@ -2764,8 +2896,8 @@ let TherapeuticAlternative = class extends i$1 {
2764
2896
  }
2765
2897
  // Only rendered past CHECKLIST_SEARCH_THRESHOLD (see renderChecklistPanel) -- below that, typing
2766
2898
  // 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.
2899
+ // group/section opens (toggleScopePanel/toggleExclusionPanel/toggleIncludePanel) so a leftover
2900
+ // term from a previous panel can't silently hide everything in the next one.
2769
2901
  renderChecklistSearch(testIdPrefix) {
2770
2902
  return b`
2771
2903
  <bcbd-input
@@ -2781,9 +2913,9 @@ let TherapeuticAlternative = class extends i$1 {
2781
2913
  }
2782
2914
  // Rappel des valeurs actuellement cochées pour TOUTE la section (tous ses groupes confondus),
2783
2915
  // 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).
2916
+ // le soit. C'est la vue "source de vérité" (scopeLabels/includeLabels/excludeLabels) plutôt que
2917
+ // le panel ouvert : une valeur cochée reste visible et décochable ici même si un refetch a fait
2918
+ // disparaître sa ligne du panel courant (voir emptyScopeLabels).
2787
2919
  renderSelectedChips(labelsByGroup, onRemove, testIdPrefix) {
2788
2920
  const entries = [];
2789
2921
  for (const [groupKey, labels] of Object.entries(labelsByGroup)) {
@@ -2830,36 +2962,36 @@ let TherapeuticAlternative = class extends i$1 {
2830
2962
  </button>
2831
2963
  `;
2832
2964
  }
2833
- renderInclusionSection() {
2834
- const chips = this.renderSelectedChips(this.includeLabels, (groupKey, id) => {
2835
- const group = INCLUSION_GROUPS.find((g2) => g2.key === groupKey);
2965
+ renderScopeSection() {
2966
+ const chips = this.renderSelectedChips(this.scopeLabels, (groupKey, id) => {
2967
+ const group = SCOPE_GROUPS.find((g2) => g2.key === groupKey);
2836
2968
  if (group) {
2837
- this.toggleInclude(group, id, "");
2969
+ this.toggleScope(group, id, "");
2838
2970
  }
2839
- }, "inclusion");
2840
- if (this.sectionCollapsed.inclusion) {
2971
+ }, "scope");
2972
+ if (this.sectionCollapsed.scope) {
2841
2973
  return chips;
2842
2974
  }
2843
- const openGroup = INCLUSION_GROUPS.find((group) => group.key === this.openInclusionGroup);
2844
- const pills = INCLUSION_GROUPS.map((group) => this.renderPill(
2975
+ const openGroup = SCOPE_GROUPS.find((group) => group.key === this.openScopeGroup);
2976
+ const pills = SCOPE_GROUPS.map((group) => this.renderPill(
2845
2977
  group.label,
2846
- this.include[group.idsField].length > 0,
2847
- group.key === this.openInclusionGroup,
2848
- () => this.toggleInclusionPanel(group.key),
2849
- `inclusion-pill-${group.key}`
2978
+ this.scope[group.idsField].length > 0,
2979
+ group.key === this.openScopeGroup,
2980
+ () => this.toggleScopePanel(group.key),
2981
+ `scope-pill-${group.key}`
2850
2982
  ));
2851
2983
  return b`
2852
- <div class="${PREFIX}-therapeutic-alternative__pills" data-test-id="inclusion-pills">
2984
+ <div class="${PREFIX}-therapeutic-alternative__pills" data-test-id="scope-pills">
2853
2985
  ${pills}
2854
2986
  </div>
2855
2987
  ${chips}
2856
2988
  ${openGroup ? this.renderChecklistPanel(
2857
- this.includeFacets[openGroup.key],
2858
- this.include[openGroup.idsField],
2989
+ this.scopeFacets[openGroup.key],
2990
+ this.scope[openGroup.idsField],
2859
2991
  true,
2860
2992
  INCLUDE_COUNT_TOOLTIP,
2861
- (id, label) => this.toggleInclude(openGroup, id, label),
2862
- `inclusion-${openGroup.key}`,
2993
+ (id, label) => this.toggleScope(openGroup, id, label),
2994
+ `scope-${openGroup.key}`,
2863
2995
  openGroup.key === "atcClasses",
2864
2996
  // Already expanded == clicking again would send the exact same expandFacetGroup and get
2865
2997
  // the exact same (still truncated, if it's a group with more than FACET_VALUES_EXPANDED_SIZE
@@ -2904,7 +3036,47 @@ let TherapeuticAlternative = class extends i$1 {
2904
3036
  `exclusion-${openGroup.key}`,
2905
3037
  false,
2906
3038
  canExpand,
2907
- () => this.requestMoreFacetValues(truncatedGroupKey)
3039
+ () => this.requestMoreFacetValues(truncatedGroupKey),
3040
+ this.excludePossessedIds(openGroup.key)
3041
+ ) : A}
3042
+ `;
3043
+ }
3044
+ renderIncludeSection() {
3045
+ const chips = this.renderSelectedChips(this.includeLabels, (groupKey, id) => {
3046
+ const group = INCLUDE_GROUPS.find((g2) => g2.key === groupKey);
3047
+ if (group) {
3048
+ this.toggleInclude(group, id, "");
3049
+ }
3050
+ }, "include");
3051
+ if (this.sectionCollapsed.include) {
3052
+ return chips;
3053
+ }
3054
+ const openGroup = INCLUDE_GROUPS.find((group) => group.key === this.openIncludeGroup);
3055
+ const pills = INCLUDE_GROUPS.map((group) => this.renderPill(
3056
+ group.label,
3057
+ this.include[group.idsField].length > 0,
3058
+ group.key === this.openIncludeGroup,
3059
+ () => this.toggleIncludePanel(group.key),
3060
+ `include-pill-${group.key}`
3061
+ ));
3062
+ const truncatedGroupKey = openGroup ? this.includeTruncatedGroupKey(openGroup.key) : null;
3063
+ const canExpand = truncatedGroupKey !== null && this.truncatedGroups.includes(truncatedGroupKey) && this.expandFacetGroupKey !== truncatedGroupKey;
3064
+ return b`
3065
+ <div class="${PREFIX}-therapeutic-alternative__pills" data-test-id="include-pills">
3066
+ ${pills}
3067
+ </div>
3068
+ ${chips}
3069
+ ${openGroup ? this.renderChecklistPanel(
3070
+ this.includeFacets[openGroup.key],
3071
+ this.include[openGroup.idsField],
3072
+ true,
3073
+ INCLUDE_COUNT_TOOLTIP,
3074
+ (id, label) => this.toggleInclude(openGroup, id, label),
3075
+ `include-${openGroup.key}`,
3076
+ false,
3077
+ canExpand,
3078
+ () => this.requestMoreFacetValues(truncatedGroupKey),
3079
+ this.includePossessedIds(openGroup.key)
2908
3080
  ) : A}
2909
3081
  `;
2910
3082
  }
@@ -2912,25 +3084,56 @@ let TherapeuticAlternative = class extends i$1 {
2912
3084
  if (this.sectionCollapsed.quickFilters) {
2913
3085
  return A;
2914
3086
  }
2915
- const chips = QUICK_FILTERS.map((filter) => {
2916
- const active = this.quickFilters[filter.key];
2917
- const chipClasses = [
2918
- `${PREFIX}-therapeutic-alternative__chip`,
2919
- active ? `${PREFIX}-therapeutic-alternative__chip--active` : ""
3087
+ const visibleFilters = QUICK_FILTERS.filter((filter) => {
3088
+ if (this.quickFilters[filter.key] || this.pendingToggleIds.has(filter.key)) {
3089
+ return true;
3090
+ }
3091
+ return this.quickFilterFacets[filter.key] !== 0;
3092
+ });
3093
+ if (visibleFilters.length === 0) {
3094
+ return b`<span class="${PREFIX}-therapeutic-alternative__panel-empty">Aucune valeur disponible.</span>`;
3095
+ }
3096
+ const rows = visibleFilters.map((filter) => {
3097
+ const count = this.quickFilterFacets[filter.key];
3098
+ const pending = this.pendingToggleIds.has(filter.key);
3099
+ const rowClasses = [
3100
+ `${PREFIX}-therapeutic-alternative__panel-row`,
3101
+ pending ? `${PREFIX}-therapeutic-alternative__panel-row--pending` : ""
2920
3102
  ].join(" ");
2921
3103
  return b`
2922
- <button type="button" class=${chipClasses} @click=${() => this.toggleQuickFilter(filter.key)} data-test-id=${`quick-filter-${filter.key}`}>
2923
- ${active ? b`<bcbd-icon icon="check" size="extra-small"></bcbd-icon>` : A}
2924
- ${filter.label}
2925
- </button>
3104
+ <div class=${rowClasses}>
3105
+ <bcbd-checkbox
3106
+ id=${`quick-filter-${filter.key}`}
3107
+ label=${filter.label}
3108
+ ?checked=${this.quickFilters[filter.key]}
3109
+ color="primary"
3110
+ @change=${() => this.toggleQuickFilter(filter.key)}
3111
+ data-test-id=${`quick-filter-${filter.key}`}>
3112
+ </bcbd-checkbox>
3113
+ ${pending ? b`
3114
+ <bcbd-loader theme="standard" size="18" data-test-id=${`quick-filter-loader-${filter.key}`}></bcbd-loader>
3115
+ ` : this.renderQuickFilterCount(filter.key, count)}
3116
+ </div>
2926
3117
  `;
2927
3118
  });
2928
3119
  return b`
2929
- <div class="${PREFIX}-therapeutic-alternative__chips" data-test-id="quick-filters-chips">
2930
- ${chips}
3120
+ <div class="${PREFIX}-therapeutic-alternative__panel" data-test-id="quick-filters-panel">
3121
+ <div class="${PREFIX}-therapeutic-alternative__panel-rows">
3122
+ ${rows}
3123
+ </div>
2931
3124
  </div>
2932
3125
  `;
2933
3126
  }
3127
+ renderQuickFilterCount(key, count) {
3128
+ if (count === void 0) {
3129
+ return A;
3130
+ }
3131
+ return b`
3132
+ <bcbd-tooltip placement="left" content=${INCLUDE_COUNT_TOOLTIP} data-test-id=${`quick-filter-count-tooltip-${key}`}>
3133
+ <span class="${PREFIX}-therapeutic-alternative__count-badge" data-test-id=${`quick-filter-count-${key}`}>${count}</span>
3134
+ </bcbd-tooltip>
3135
+ `;
3136
+ }
2934
3137
  renderSectionHeader(title, section) {
2935
3138
  const collapsed = this.sectionCollapsed[section];
2936
3139
  const chevronClasses = [
@@ -2952,8 +3155,8 @@ let TherapeuticAlternative = class extends i$1 {
2952
3155
  // Turns a result's raw matchReasons into short bullet lines, e.g. "- indiqué pour Arthrose" or
2953
3156
  // "- contient Bisoprolol (synonyme de Métoprolol)" -- label/synonymLabel come straight from the
2954
3157
  // 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.
3158
+ // scopeFacets/includeFacets: those only ever cover the request's criterion codes, never
3159
+ // synonymCode, which is a code found on this specific result product, not a criterion at all.
2957
3160
  matchReasonLabels(result) {
2958
3161
  const labels = (result.matchReasons ?? []).map((reason) => {
2959
3162
  const dimension = reason.dimension ?? "";
@@ -3022,6 +3225,11 @@ let TherapeuticAlternative = class extends i$1 {
3022
3225
  </bcbd-pagination>`}
3023
3226
  `;
3024
3227
  }
3228
+ // The product name in the title replaces the "Produit sélectionné" line when has-title is on; without
3229
+ // a title, that line (see render) is the only place the product name shows.
3230
+ renderTitle(medicationTitle) {
3231
+ return b`<span class="${PREFIX}-therapeutic-alternative-title" data-test-id="title">Alternative Thérapeutique pour ${medicationTitle !== "" ? decodeHtml(medicationTitle) : "ce produit"}</span>`;
3232
+ }
3025
3233
  render() {
3026
3234
  const containerClasses = [
3027
3235
  `${PREFIX}-therapeutic-alternative`,
@@ -3030,16 +3238,23 @@ let TherapeuticAlternative = class extends i$1 {
3030
3238
  const medicationTitle = getMedicationTitle(this.currentMedication);
3031
3239
  return b`
3032
3240
  <div class=${containerClasses} data-test-id="therapeutic-alternative-container">
3033
- ${this.hasTitle ? b`<span class="${PREFIX}-therapeutic-alternative-title">Alternative thérapeutique</span>` : ""}
3034
3241
  ${this.currentMedication ? b`
3035
3242
  <div class="${PREFIX}-therapeutic-alternative__source mb-3" data-test-id="source-product">
3036
- <span data-test-id="source-product-label">
3037
- Produit sélectionné : <span class="text-primary">${medicationTitle !== "" ? decodeHtml(medicationTitle) : "Inconnu"}</span>
3038
- </span>
3039
- <bcbd-button size="small" background-color="primary" outlined
3040
- @click=${this.cancelAlternative} data-test-id="cancel-alternative-button">
3041
- Retour au contrôle d'ordonnance
3042
- </bcbd-button>
3243
+ ${this.hasTitle ? this.renderTitle(medicationTitle) : b`
3244
+ <span data-test-id="source-product-label">
3245
+ Produit sélectionné : <span class="text-primary">${medicationTitle !== "" ? decodeHtml(medicationTitle) : "Inconnu"}</span>
3246
+ </span>`}
3247
+ <div class="${PREFIX}-therapeutic-alternative__source-actions">
3248
+ <bcbd-button size="small" background-color="secondary" outlined
3249
+ ?disabled=${this.hasNoSelectedCriteria()}
3250
+ @click=${this.resetAllCriteria} data-test-id="reset-criteria-button">
3251
+ Réinitialiser
3252
+ </bcbd-button>
3253
+ <bcbd-button size="small" background-color="primary" outlined
3254
+ @click=${this.cancelAlternative} data-test-id="cancel-alternative-button">
3255
+ Annuler
3256
+ </bcbd-button>
3257
+ </div>
3043
3258
  </div>
3044
3259
 
3045
3260
  <div class="${PREFIX}-therapeutic-alternative__disclaimer">
@@ -3059,13 +3274,18 @@ let TherapeuticAlternative = class extends i$1 {
3059
3274
  </bcbd-alert>
3060
3275
  </div>` : ""}
3061
3276
 
3062
- <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="inclusion-section">
3063
- ${this.renderSectionHeader("Rechercher une alternative", "inclusion")}
3064
- ${this.renderInclusionSection()}
3277
+ <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="scope-section">
3278
+ ${this.renderSectionHeader("Rechercher une alternative", "scope")}
3279
+ ${this.renderScopeSection()}
3280
+ </div>
3281
+
3282
+ <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="include-section">
3283
+ ${this.renderSectionHeader("Critères d'inclusion", "include")}
3284
+ ${this.renderIncludeSection()}
3065
3285
  </div>
3066
3286
 
3067
3287
  <div class="${PREFIX}-therapeutic-alternative__section" data-test-id="exclusion-section">
3068
- ${this.renderSectionHeader("Affiner (exclure des critères)", "exclusion")}
3288
+ ${this.renderSectionHeader("Critères d'exclusion", "exclusion")}
3069
3289
  ${this.renderExclusionSection()}
3070
3290
  </div>
3071
3291
 
@@ -3077,6 +3297,7 @@ let TherapeuticAlternative = class extends i$1 {
3077
3297
  <div class="${PREFIX}-therapeutic-alternative__results" data-test-id="results-container">
3078
3298
  ${this.renderResults()}
3079
3299
  </div>` : b`
3300
+ ${this.hasTitle ? this.renderTitle(medicationTitle) : ""}
3080
3301
  <div class="${PREFIX}-therapeutic-alternative__no-medication" data-test-id="no-medication-container">
3081
3302
  <bcbd-alert title-message="Pas de médicament sélectionné"
3082
3303
  description-message="Un problème est survenu au moment de sélectionner le produit"
@@ -3118,6 +3339,9 @@ __decorateClass([
3118
3339
  __decorateClass([
3119
3340
  r()
3120
3341
  ], TherapeuticAlternative.prototype, "currentPage", 2);
3342
+ __decorateClass([
3343
+ r()
3344
+ ], TherapeuticAlternative.prototype, "scope", 2);
3121
3345
  __decorateClass([
3122
3346
  r()
3123
3347
  ], TherapeuticAlternative.prototype, "include", 2);
@@ -3127,6 +3351,12 @@ __decorateClass([
3127
3351
  __decorateClass([
3128
3352
  r()
3129
3353
  ], TherapeuticAlternative.prototype, "quickFilters", 2);
3354
+ __decorateClass([
3355
+ r()
3356
+ ], TherapeuticAlternative.prototype, "quickFilterFacets", 2);
3357
+ __decorateClass([
3358
+ r()
3359
+ ], TherapeuticAlternative.prototype, "scopeFacets", 2);
3130
3360
  __decorateClass([
3131
3361
  r()
3132
3362
  ], TherapeuticAlternative.prototype, "includeFacets", 2);
@@ -3139,6 +3369,9 @@ __decorateClass([
3139
3369
  __decorateClass([
3140
3370
  r()
3141
3371
  ], TherapeuticAlternative.prototype, "expandFacetGroupKey", 2);
3372
+ __decorateClass([
3373
+ r()
3374
+ ], TherapeuticAlternative.prototype, "scopeLabels", 2);
3142
3375
  __decorateClass([
3143
3376
  r()
3144
3377
  ], TherapeuticAlternative.prototype, "includeLabels", 2);
@@ -3147,10 +3380,13 @@ __decorateClass([
3147
3380
  ], TherapeuticAlternative.prototype, "excludeLabels", 2);
3148
3381
  __decorateClass([
3149
3382
  r()
3150
- ], TherapeuticAlternative.prototype, "openInclusionGroup", 2);
3383
+ ], TherapeuticAlternative.prototype, "openScopeGroup", 2);
3151
3384
  __decorateClass([
3152
3385
  r()
3153
3386
  ], TherapeuticAlternative.prototype, "openExclusionGroup", 2);
3387
+ __decorateClass([
3388
+ r()
3389
+ ], TherapeuticAlternative.prototype, "openIncludeGroup", 2);
3154
3390
  __decorateClass([
3155
3391
  r()
3156
3392
  ], 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.3.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",