valtech-components 2.0.995 → 2.0.997

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.mjs +667 -0
  2. package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences-view.i18n.mjs +71 -0
  3. package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences.routes.mjs +31 -0
  4. package/esm2022/lib/components/organisms/notification-preferences-view/types.mjs +2 -0
  5. package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +1 -53
  6. package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +1 -3
  7. package/esm2022/lib/components/organisms/permissions-view/permissions-view.component.mjs +511 -0
  8. package/esm2022/lib/components/organisms/permissions-view/permissions-view.i18n.mjs +116 -0
  9. package/esm2022/lib/components/organisms/permissions-view/permissions.routes.mjs +29 -0
  10. package/esm2022/lib/components/organisms/permissions-view/types.mjs +2 -0
  11. package/esm2022/lib/components/organisms/settings-hub/settings.routes.mjs +12 -1
  12. package/esm2022/lib/services/auth/auth.service.mjs +1 -14
  13. package/esm2022/lib/services/auth/types.mjs +1 -1
  14. package/esm2022/lib/services/org/org.service.mjs +1 -8
  15. package/esm2022/lib/services/org/permission-catalog.service.mjs +33 -0
  16. package/esm2022/lib/services/org/types.mjs +1 -1
  17. package/esm2022/lib/version.mjs +2 -2
  18. package/esm2022/public-api.mjs +8 -2
  19. package/fesm2022/valtech-components.mjs +1524 -541
  20. package/fesm2022/valtech-components.mjs.map +1 -1
  21. package/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.d.ts +146 -0
  22. package/lib/components/organisms/notification-preferences-view/notification-preferences-view.i18n.d.ts +11 -0
  23. package/lib/components/organisms/notification-preferences-view/notification-preferences.routes.d.ts +26 -0
  24. package/lib/components/organisms/notification-preferences-view/types.d.ts +36 -0
  25. package/lib/components/organisms/organization-view/organization-view.component.d.ts +0 -3
  26. package/lib/components/organisms/permissions-view/permissions-view.component.d.ts +71 -0
  27. package/lib/components/organisms/permissions-view/permissions-view.i18n.d.ts +9 -0
  28. package/lib/components/organisms/permissions-view/permissions.routes.d.ts +24 -0
  29. package/lib/components/organisms/permissions-view/types.d.ts +26 -0
  30. package/lib/components/organisms/settings-hub/settings.routes.d.ts +15 -0
  31. package/lib/services/auth/auth.service.d.ts +1 -7
  32. package/lib/services/auth/types.d.ts +0 -23
  33. package/lib/services/org/org.service.d.ts +1 -6
  34. package/lib/services/org/permission-catalog.service.d.ts +34 -0
  35. package/lib/services/org/types.d.ts +0 -30
  36. package/lib/version.d.ts +1 -1
  37. package/package.json +1 -1
  38. package/public-api.d.ts +7 -1
  39. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.component.mjs +0 -313
  40. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.i18n.mjs +0 -63
  41. package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +0 -47
  42. package/lib/components/organisms/member-import-modal/member-import-modal.i18n.d.ts +0 -6
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, ErrorHandler, Injectable, DestroyRef, Inject, signal, computed, PLATFORM_ID, isDevMode, Optional, runInInjectionContext, effect, Pipe, TemplateRef, ViewContainerRef, input, Directive, EventEmitter, Component, Input, Output, ChangeDetectionStrategy, HostBinding, HostListener, NgZone, ViewChild, ChangeDetectorRef, output, ContentChild, ElementRef, viewChild, untracked, Injector, isSignal, ViewEncapsulation } from '@angular/core';
3
- import { BehaviorSubject, throwError, Subject, map, distinctUntilChanged, filter as filter$1, take as take$1, firstValueFrom, of, from, EMPTY, Observable, interval, debounceTime, switchMap as switchMap$1, catchError as catchError$1, takeUntil, isObservable, race, timer, shareReplay, forkJoin } from 'rxjs';
3
+ import { BehaviorSubject, throwError, Subject, map, distinctUntilChanged, filter as filter$1, take as take$1, firstValueFrom, of, from, EMPTY, Observable, interval, debounceTime, switchMap as switchMap$1, catchError as catchError$1, takeUntil, isObservable, race, timer, forkJoin, shareReplay } from 'rxjs';
4
4
  import { catchError, switchMap, finalize, filter, take, map as map$1, tap, distinctUntilChanged as distinctUntilChanged$1, retry, timeout, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
5
5
  import * as i1$3 from '@angular/common/http';
6
6
  import { provideHttpClient, withInterceptors, HttpClient, HttpErrorResponse } from '@angular/common/http';
@@ -54,7 +54,7 @@ import 'prismjs/components/prism-json';
54
54
  * Current version of valtech-components.
55
55
  * This is automatically updated during the publish process.
56
56
  */
57
- const VERSION = '2.0.995';
57
+ const VERSION = '2.0.997';
58
58
 
59
59
  // Control de estado de refresco (singleton a nivel de módulo)
60
60
  let isRefreshing = false;
@@ -8658,19 +8658,6 @@ class AuthService {
8658
8658
  .post(`${this.baseUrl}/reset-password`, request)
8659
8659
  .pipe(catchError(error => this.handleAuthError(error)));
8660
8660
  }
8661
- /**
8662
- * Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
8663
- * la contraseña con el token del enlace de email. En éxito el backend devuelve
8664
- * tokens de auto-signin → el usuario queda logueado (mismo flujo que verify-email).
8665
- */
8666
- activateAccount(request) {
8667
- this.stateService.clearError();
8668
- return this.http.post(`${this.baseUrl}/activate`, request).pipe(tap(response => {
8669
- if (response.activated && response.accessToken) {
8670
- this.handleSuccessfulAuth(response);
8671
- }
8672
- }), catchError(error => this.handleAuthError(error)));
8673
- }
8674
8661
  /**
8675
8662
  * Cambia la contraseña del usuario autenticado.
8676
8663
  * Requiere la contraseña actual para verificación.
@@ -41750,7 +41737,7 @@ const PROFILE_VIEW_I18N = {
41750
41737
  const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41751
41738
  const HANDLE_MIN = 3;
41752
41739
  const HANDLE_MAX = 20;
41753
- const DEFAULT_NAMESPACE$e = 'Settings.Profile';
41740
+ const DEFAULT_NAMESPACE$f = 'Settings.Profile';
41754
41741
  /**
41755
41742
  * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41756
41743
  *
@@ -41800,7 +41787,7 @@ class ProfileViewComponent {
41800
41787
  showPhone: merged.showPhone ?? true,
41801
41788
  showShareCta: merged.showShareCta ?? true,
41802
41789
  profileBaseUrl: merged.profileBaseUrl ?? '',
41803
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
41790
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$f,
41804
41791
  onSaved: merged.onSaved,
41805
41792
  onAvatarUploaded: merged.onAvatarUploaded,
41806
41793
  };
@@ -42716,7 +42703,7 @@ const PREFERENCES_VIEW_I18N = {
42716
42703
  },
42717
42704
  };
42718
42705
 
42719
- const DEFAULT_NAMESPACE$d = 'Settings.General';
42706
+ const DEFAULT_NAMESPACE$e = 'Settings.General';
42720
42707
  const DEFAULT_LANGUAGES = ['es', 'en'];
42721
42708
  /**
42722
42709
  * `val-preferences-view` — vista de preferencias de UI cross-app (tema +
@@ -42753,7 +42740,7 @@ class PreferencesViewComponent {
42753
42740
  showLanguage: merged.showLanguage ?? true,
42754
42741
  showFontSize: merged.showFontSize ?? true,
42755
42742
  supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
42756
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
42743
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
42757
42744
  onThemeChanged: merged.onThemeChanged,
42758
42745
  onLanguageChanged: merged.onLanguageChanged,
42759
42746
  onFontSizeChanged: merged.onFontSizeChanged,
@@ -43401,62 +43388,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
43401
43388
  type: Input
43402
43389
  }] } });
43403
43390
 
43404
- /**
43405
- * Helper para montar el bloque completo de Settings en una app del factory: el
43406
- * HUB de navegación (`val-settings-hub`) en el index + las vistas promovidas
43407
- * (Perfil, Preferencias) + las rutas propias de la app (`extraRoutes`).
43408
- *
43409
- * Pensado para apps nuevas (greenfield) que adoptan el set entero. showcase NO
43410
- * lo usa hoy (preserva su `settings.routes.ts` por la semántica de sus muchas
43411
- * secciones no promovidas — ver guía de promoción), pero el provider debe
43412
- * existir y compilar para el resto de apps.
43413
- *
43414
- * @example
43415
- * ```ts
43416
- * // app.routes.ts
43417
- * import { provideValtechSettingsRoutes } from 'valtech-components';
43418
- *
43419
- * export const routes: Routes = [
43420
- * {
43421
- * path: 'app',
43422
- * children: [
43423
- * ...provideValtechSettingsRoutes({
43424
- * hubConfig: { onLogout: () => auth.logoutWithConfirmation() },
43425
- * extraRoutes: [
43426
- * { path: 'security', loadComponent: () => ... },
43427
- * ],
43428
- * }),
43429
- * ],
43430
- * },
43431
- * ];
43432
- * ```
43433
- */
43434
- function provideValtechSettingsRoutes(opts) {
43435
- const basePath = opts?.basePath ?? 'settings';
43436
- const includeProfile = opts?.includeProfile ?? true;
43437
- const includePreferences = opts?.includePreferences ?? true;
43438
- return [
43439
- {
43440
- path: basePath,
43441
- children: [
43442
- {
43443
- path: '',
43444
- component: SettingsHubComponent,
43445
- data: { settingsHubConfig: opts?.hubConfig },
43446
- },
43447
- ...(includeProfile ? provideValtechProfileRoutes({ config: opts?.profileConfig }) : []),
43448
- ...(includePreferences
43449
- ? provideValtechPreferencesRoutes({
43450
- path: opts?.preferencesPath ?? 'preferences',
43451
- config: opts?.preferencesConfig,
43452
- })
43453
- : []),
43454
- ...(opts?.extraRoutes ?? []),
43455
- ],
43456
- },
43457
- ];
43458
- }
43459
-
43460
43391
  addIcons({ shieldCheckmarkOutline, checkmarkCircleOutline });
43461
43392
  /**
43462
43393
  * `val-login-attempt-modal` — modal de confirmación de intento de inicio de sesión.
@@ -44565,7 +44496,7 @@ const SECURITY_VIEW_I18N = {
44565
44496
  };
44566
44497
 
44567
44498
  addIcons({ laptopOutline, phonePortraitOutline });
44568
- const DEFAULT_NAMESPACE$c = 'Settings.Security';
44499
+ const DEFAULT_NAMESPACE$d = 'Settings.Security';
44569
44500
  /**
44570
44501
  * `val-security-view` — vista Seguridad full-feature autocontenida (organism).
44571
44502
  *
@@ -44614,7 +44545,7 @@ class SecurityViewComponent {
44614
44545
  showChangeEmail: merged.showChangeEmail ?? true,
44615
44546
  showMfa: merged.showMfa ?? true,
44616
44547
  showPolicies: merged.showPolicies ?? true,
44617
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$c,
44548
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
44618
44549
  handleDeepLinks: merged.handleDeepLinks ?? true,
44619
44550
  homeRoute: merged.homeRoute ?? '/app/home',
44620
44551
  onMfaCompleted: merged.onMfaCompleted,
@@ -45416,13 +45347,6 @@ class OrgService {
45416
45347
  inviteUser(orgId, req) {
45417
45348
  return this.http.post(`${this.baseUrl}/${orgId}/invite`, req);
45418
45349
  }
45419
- /**
45420
- * Crea/asigna miembros en masa (ADR-023). Resultado por-fila (partial success):
45421
- * `created` (pre-aprovisionado), `assigned` (ya existía), `skipped`, `error`.
45422
- */
45423
- importMembers(orgId, req) {
45424
- return this.http.post(`${this.baseUrl}/${orgId}/members/import`, req);
45425
- }
45426
45350
  leaveOrg(orgId) {
45427
45351
  return this.http.post(`${this.baseUrl}/${orgId}/leave`, {});
45428
45352
  }
@@ -45527,7 +45451,7 @@ const CREATE_ORG_MODAL_I18N = {
45527
45451
  },
45528
45452
  };
45529
45453
 
45530
- const DEFAULT_NAMESPACE$b = 'CreateOrgModal';
45454
+ const DEFAULT_NAMESPACE$c = 'CreateOrgModal';
45531
45455
  /**
45532
45456
  * `val-create-org-modal` — modal de creación de organización (organism
45533
45457
  * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
@@ -45553,10 +45477,10 @@ class CreateOrgModalComponent {
45553
45477
  this.toast = inject(ToastService);
45554
45478
  this.errors = inject(ValtechErrorService);
45555
45479
  /** Namespace i18n con que la vista resuelve sus textos. */
45556
- this.i18nNamespace = DEFAULT_NAMESPACE$b;
45480
+ this.i18nNamespace = DEFAULT_NAMESPACE$c;
45557
45481
  this._busy = signal(false);
45558
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
45559
- this.i18n.registerContent(DEFAULT_NAMESPACE$b, CREATE_ORG_MODAL_I18N);
45482
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
45483
+ this.i18n.registerContent(DEFAULT_NAMESPACE$c, CREATE_ORG_MODAL_I18N);
45560
45484
  }
45561
45485
  this.formMeta = signal(this.buildFormMeta());
45562
45486
  effect(() => {
@@ -45792,7 +45716,7 @@ const DELETE_ACCOUNT_MODAL_I18N = {
45792
45716
  },
45793
45717
  };
45794
45718
 
45795
- const DEFAULT_NAMESPACE$a = 'Settings.DeleteAccount';
45719
+ const DEFAULT_NAMESPACE$b = 'Settings.DeleteAccount';
45796
45720
  /**
45797
45721
  * `val-delete-account-modal` — modal de eliminación de cuenta (organism,
45798
45722
  * cuenta-específico). Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -45818,7 +45742,7 @@ class DeleteAccountModalComponent {
45818
45742
  this.i18n = inject(I18nService);
45819
45743
  this.toast = inject(ToastService);
45820
45744
  /** Namespace i18n con que la vista resuelve sus textos. */
45821
- this.i18nNamespace = DEFAULT_NAMESPACE$a;
45745
+ this.i18nNamespace = DEFAULT_NAMESPACE$b;
45822
45746
  this.hasPassword = signal(null);
45823
45747
  this.codeSent = signal(false);
45824
45748
  this.formState = signal(ComponentStates.ENABLED);
@@ -45885,8 +45809,8 @@ class DeleteAccountModalComponent {
45885
45809
  ],
45886
45810
  actions: this.submitBtn('delete-code-submit', 'confirmCta'),
45887
45811
  }));
45888
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
45889
- this.i18n.registerContent(DEFAULT_NAMESPACE$a, DELETE_ACCOUNT_MODAL_I18N);
45812
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
45813
+ this.i18n.registerContent(DEFAULT_NAMESPACE$b, DELETE_ACCOUNT_MODAL_I18N);
45890
45814
  }
45891
45815
  }
45892
45816
  ngOnInit() {
@@ -46087,7 +46011,7 @@ const ORG_INFO_SHEET_I18N = {
46087
46011
  },
46088
46012
  };
46089
46013
 
46090
- const DEFAULT_NAMESPACE$9 = 'OrgInfoSheet';
46014
+ const DEFAULT_NAMESPACE$a = 'OrgInfoSheet';
46091
46015
  /**
46092
46016
  * `val-org-info-sheet` — sheet informativo "¿Qué son las organizaciones?".
46093
46017
  * Extraído del `OrgInfoSheetComponent` inline de `AccountPage` bajo el proceso de
@@ -46132,15 +46056,15 @@ class OrgInfoSheetComponent {
46132
46056
  text: this.closeProps.text || this.resolvedCloseLabel(),
46133
46057
  }));
46134
46058
  addIcons({ peopleOutline, swapHorizontalOutline, mailOutline });
46135
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
46136
- this.i18n.registerContent(DEFAULT_NAMESPACE$9, ORG_INFO_SHEET_I18N);
46059
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
46060
+ this.i18n.registerContent(DEFAULT_NAMESPACE$a, ORG_INFO_SHEET_I18N);
46137
46061
  }
46138
46062
  }
46139
46063
  dismiss() {
46140
46064
  this._modalRef?.dismiss(null, 'cancel');
46141
46065
  }
46142
46066
  tt(key) {
46143
- return this.i18n.t(key, DEFAULT_NAMESPACE$9);
46067
+ return this.i18n.t(key, DEFAULT_NAMESPACE$a);
46144
46068
  }
46145
46069
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrgInfoSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46146
46070
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: OrgInfoSheetComponent, isStandalone: true, selector: "val-org-info-sheet", inputs: { title: "title", subtitle: "subtitle", body: "body", closeLabel: "closeLabel", closeProps: "closeProps", _modalRef: "_modalRef" }, ngImport: i0, template: `
@@ -46307,7 +46231,7 @@ const SWITCH_ORG_MODAL_I18N = {
46307
46231
  },
46308
46232
  };
46309
46233
 
46310
- const DEFAULT_NAMESPACE$8 = 'Settings.SwitchOrg';
46234
+ const DEFAULT_NAMESPACE$9 = 'Settings.SwitchOrg';
46311
46235
  addIcons({ businessOutline, checkmarkCircleOutline });
46312
46236
  /**
46313
46237
  * `val-switch-org-modal` — modal de cambio de organización activa (organism
@@ -46333,7 +46257,7 @@ class SwitchOrgModalComponent {
46333
46257
  this.orgSwitch = inject(OrgSwitchService);
46334
46258
  this.errors = inject(ValtechErrorService);
46335
46259
  /** Namespace i18n con que la vista resuelve sus textos. */
46336
- this.i18nNamespace = DEFAULT_NAMESPACE$8;
46260
+ this.i18nNamespace = DEFAULT_NAMESPACE$9;
46337
46261
  this.orgs = signal([]);
46338
46262
  this.loading = signal(false);
46339
46263
  this.query = signal('');
@@ -46349,8 +46273,8 @@ class SwitchOrgModalComponent {
46349
46273
  return list;
46350
46274
  return list.filter(o => o.name.toLowerCase().includes(q));
46351
46275
  });
46352
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
46353
- this.i18n.registerContent(DEFAULT_NAMESPACE$8, SWITCH_ORG_MODAL_I18N);
46276
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
46277
+ this.i18n.registerContent(DEFAULT_NAMESPACE$9, SWITCH_ORG_MODAL_I18N);
46354
46278
  }
46355
46279
  this.loadOrgs();
46356
46280
  }
@@ -46668,7 +46592,7 @@ const ACCOUNT_VIEW_I18N = {
46668
46592
  };
46669
46593
 
46670
46594
  addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline });
46671
- const DEFAULT_NAMESPACE$7 = 'Settings.Account';
46595
+ const DEFAULT_NAMESPACE$8 = 'Settings.Account';
46672
46596
  const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
46673
46597
  /**
46674
46598
  * `val-account-view` — vista Cuenta full-feature autocontenida (organism).
@@ -46709,7 +46633,7 @@ class AccountViewComponent {
46709
46633
  showNewOrgCta: merged.showNewOrgCta ?? true,
46710
46634
  showLogout: merged.showLogout ?? true,
46711
46635
  showDeleteAccount: merged.showDeleteAccount ?? true,
46712
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$7,
46636
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$8,
46713
46637
  manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
46714
46638
  onOrgSwitched: merged.onOrgSwitched,
46715
46639
  onOrgCreated: merged.onOrgCreated,
@@ -47434,7 +47358,7 @@ const EDIT_ORG_MODAL_I18N = {
47434
47358
  },
47435
47359
  };
47436
47360
 
47437
- const DEFAULT_NAMESPACE$6 = 'EditOrgModal';
47361
+ const DEFAULT_NAMESPACE$7 = 'EditOrgModal';
47438
47362
  /**
47439
47363
  * `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
47440
47364
  * Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
@@ -47461,10 +47385,10 @@ class EditOrgModalComponent {
47461
47385
  /** Organización a editar (pasada por `componentProps`). */
47462
47386
  this.org = null;
47463
47387
  /** Namespace i18n con que la vista resuelve sus textos. */
47464
- this.i18nNamespace = DEFAULT_NAMESPACE$6;
47388
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
47465
47389
  this.saving = signal(false);
47466
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
47467
- this.i18n.registerContent(DEFAULT_NAMESPACE$6, EDIT_ORG_MODAL_I18N);
47390
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
47391
+ this.i18n.registerContent(DEFAULT_NAMESPACE$7, EDIT_ORG_MODAL_I18N);
47468
47392
  }
47469
47393
  this.formMeta = signal(this.buildFormMeta());
47470
47394
  effect(() => {
@@ -47702,7 +47626,7 @@ const INVITE_MEMBER_MODAL_I18N = {
47702
47626
  },
47703
47627
  };
47704
47628
 
47705
- const DEFAULT_NAMESPACE$5 = 'InviteModal';
47629
+ const DEFAULT_NAMESPACE$6 = 'InviteModal';
47706
47630
  addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
47707
47631
  /**
47708
47632
  * `val-invite-member-modal` — invitación de miembros a una organización.
@@ -47733,7 +47657,7 @@ class InviteMemberModalComponent {
47733
47657
  /** Ids de miembros ya existentes (excluidos de los resultados). */
47734
47658
  this.existingMemberIds = [];
47735
47659
  /** Namespace i18n con que la vista resuelve sus textos. */
47736
- this.i18nNamespace = DEFAULT_NAMESPACE$5;
47660
+ this.i18nNamespace = DEFAULT_NAMESPACE$6;
47737
47661
  this.query = signal('');
47738
47662
  this.searching = signal(false);
47739
47663
  this.searchResults = signal([]);
@@ -47780,8 +47704,8 @@ class InviteMemberModalComponent {
47780
47704
  state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
47781
47705
  }));
47782
47706
  this.searchSubject = new Subject();
47783
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
47784
- this.i18n.registerContent(DEFAULT_NAMESPACE$5, INVITE_MEMBER_MODAL_I18N);
47707
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
47708
+ this.i18n.registerContent(DEFAULT_NAMESPACE$6, INVITE_MEMBER_MODAL_I18N);
47785
47709
  }
47786
47710
  this.loadRoles();
47787
47711
  this.roleSub = this.roleControl.valueChanges.subscribe(v => {
@@ -48270,7 +48194,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
48270
48194
  },
48271
48195
  };
48272
48196
 
48273
- const DEFAULT_NAMESPACE$4 = 'Settings.MemberDetail';
48197
+ const DEFAULT_NAMESPACE$5 = 'Settings.MemberDetail';
48274
48198
  /**
48275
48199
  * `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
48276
48200
  * permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -48307,7 +48231,7 @@ class MemberDetailModalComponent {
48307
48231
  { id: 'admin', name: 'admin' },
48308
48232
  ];
48309
48233
  /** Namespace i18n con que la vista resuelve sus textos. */
48310
- this.i18nNamespace = DEFAULT_NAMESPACE$4;
48234
+ this.i18nNamespace = DEFAULT_NAMESPACE$5;
48311
48235
  this.working = signal(false);
48312
48236
  this.currentRole = signal('');
48313
48237
  this.roleControl = new FormControl('');
@@ -48347,8 +48271,8 @@ class MemberDetailModalComponent {
48347
48271
  type: 'button',
48348
48272
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
48349
48273
  }));
48350
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
48351
- this.i18n.registerContent(DEFAULT_NAMESPACE$4, MEMBER_DETAIL_MODAL_I18N);
48274
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
48275
+ this.i18n.registerContent(DEFAULT_NAMESPACE$5, MEMBER_DETAIL_MODAL_I18N);
48352
48276
  }
48353
48277
  }
48354
48278
  ngOnInit() {
@@ -48685,7 +48609,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
48685
48609
  },
48686
48610
  };
48687
48611
 
48688
- const DEFAULT_NAMESPACE$3 = 'Settings.TransferModal';
48612
+ const DEFAULT_NAMESPACE$4 = 'Settings.TransferModal';
48689
48613
  /**
48690
48614
  * `val-transfer-ownership-modal` — selector de miembro para transferir la
48691
48615
  * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
@@ -48709,7 +48633,7 @@ class TransferOwnershipModalComponent {
48709
48633
  /** Lista de miembros transferibles (todos excepto el owner actual). */
48710
48634
  this.members = [];
48711
48635
  /** Namespace i18n con que la vista resuelve sus textos. */
48712
- this.i18nNamespace = DEFAULT_NAMESPACE$3;
48636
+ this.i18nNamespace = DEFAULT_NAMESPACE$4;
48713
48637
  this.query = signal('');
48714
48638
  this.selectedId = signal('');
48715
48639
  this.filtered = computed(() => {
@@ -48721,8 +48645,8 @@ class TransferOwnershipModalComponent {
48721
48645
  m.userId.toLowerCase().includes(q));
48722
48646
  });
48723
48647
  this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
48724
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$3)) {
48725
- this.i18n.registerContent(DEFAULT_NAMESPACE$3, TRANSFER_OWNERSHIP_MODAL_I18N);
48648
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
48649
+ this.i18n.registerContent(DEFAULT_NAMESPACE$4, TRANSFER_OWNERSHIP_MODAL_I18N);
48726
48650
  }
48727
48651
  }
48728
48652
  ngOnInit() {
@@ -48907,371 +48831,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
48907
48831
  type: Input
48908
48832
  }] } });
48909
48833
 
48910
- /**
48911
- * Defaults i18n (es/en) embebidos en `val-member-import-modal` (ADR-023).
48912
- * Auto-registrados si el consumer no proveyó el namespace `Settings.ImportModal`.
48913
- */
48914
- const MEMBER_IMPORT_MODAL_I18N = {
48915
- es: {
48916
- title: 'Importar miembros',
48917
- hint: 'Pega un CSV con columnas: email, nombre, rol (una fila por persona).',
48918
- csvPlaceholder: 'email,nombre,rol\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48919
- rolesAvailable: 'Roles disponibles:',
48920
- onConflictLabel: 'Si el email ya existe',
48921
- conflictAssign: 'Asignar el rol',
48922
- conflictSkip: 'Omitir',
48923
- conflictError: 'Marcar error',
48924
- sendActivation: 'Enviar email de activación a las cuentas nuevas',
48925
- submit: 'Importar',
48926
- submitting: 'Importando...',
48927
- parseError: 'No se pudo interpretar el CSV. Revisa el formato.',
48928
- noRows: 'Pega al menos una fila válida (email y rol).',
48929
- summary: 'Resumen',
48930
- created: 'creados',
48931
- assigned: 'asignados',
48932
- skipped: 'omitidos',
48933
- failed: 'con error',
48934
- colEmail: 'Email',
48935
- colStatus: 'Estado',
48936
- colDetail: 'Detalle',
48937
- activationSent: 'activación enviada',
48938
- activationPending: 'activación pendiente',
48939
- again: 'Importar otro lote',
48940
- close: 'Cerrar',
48941
- genericError: 'No se pudo completar la importación. Intenta de nuevo.',
48942
- },
48943
- en: {
48944
- title: 'Import members',
48945
- hint: 'Paste a CSV with columns: email, name, role (one row per person).',
48946
- csvPlaceholder: 'email,name,role\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48947
- rolesAvailable: 'Available roles:',
48948
- onConflictLabel: 'If the email already exists',
48949
- conflictAssign: 'Assign the role',
48950
- conflictSkip: 'Skip',
48951
- conflictError: 'Mark as error',
48952
- sendActivation: 'Send activation email to new accounts',
48953
- submit: 'Import',
48954
- submitting: 'Importing...',
48955
- parseError: "Couldn't parse the CSV. Check the format.",
48956
- noRows: 'Paste at least one valid row (email and role).',
48957
- summary: 'Summary',
48958
- created: 'created',
48959
- assigned: 'assigned',
48960
- skipped: 'skipped',
48961
- failed: 'failed',
48962
- colEmail: 'Email',
48963
- colStatus: 'Status',
48964
- colDetail: 'Detail',
48965
- activationSent: 'activation sent',
48966
- activationPending: 'activation pending',
48967
- again: 'Import another batch',
48968
- close: 'Close',
48969
- genericError: "Couldn't complete the import. Try again.",
48970
- },
48971
- };
48972
-
48973
- const DEFAULT_NAMESPACE$2 = 'Settings.ImportModal';
48974
- /**
48975
- * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
48976
- *
48977
- * El admin pega un CSV (`email,nombre,rol`); el modal lo parsea a filas y llama
48978
- * `OrgService.importMembers`. Muestra el resultado por-fila (partial success):
48979
- * `created` / `assigned` / `skipped` / `error`. Header canónico (Regla #5).
48980
- *
48981
- * Auto-registra defaults i18n (es/en) en `Settings.ImportModal`.
48982
- */
48983
- class MemberImportModalComponent {
48984
- constructor() {
48985
- this.i18n = inject(I18nService);
48986
- this.orgService = inject(OrgService);
48987
- /** Org destino del import. */
48988
- this.orgId = '';
48989
- /** Nombres de roles disponibles (para el hint). Opcional. */
48990
- this.availableRoles = [];
48991
- /** Namespace i18n. */
48992
- this.i18nNamespace = DEFAULT_NAMESPACE$2;
48993
- this.csv = '';
48994
- this.onConflict = 'assignRole';
48995
- this.sendActivation = true;
48996
- this.submitting = signal(false);
48997
- this.errorMsg = signal('');
48998
- this.results = signal(null);
48999
- this.rolesHint = computed(() => this.availableRoles.length ? `${this.t('rolesAvailable')} ${this.availableRoles.join(', ')}` : '');
49000
- this.summaryLabel = computed(() => {
49001
- const s = this.results()?.summary;
49002
- if (!s)
49003
- return this.t('summary');
49004
- return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
49005
- });
49006
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$2)) {
49007
- this.i18n.registerContent(DEFAULT_NAMESPACE$2, MEMBER_IMPORT_MODAL_I18N);
49008
- }
49009
- }
49010
- ngOnInit() {
49011
- if (!this.i18n.hasNamespace(this.i18nNamespace)) {
49012
- this.i18n.registerContent(this.i18nNamespace, MEMBER_IMPORT_MODAL_I18N);
49013
- }
49014
- }
49015
- /** Parsea el CSV pegado a filas {email,name,roleName}. Tolera header. */
49016
- parseCsv() {
49017
- const rows = [];
49018
- for (const raw of this.csv.split(/\r?\n/)) {
49019
- const line = raw.trim();
49020
- if (!line)
49021
- continue;
49022
- const cols = line.split(',').map(c => c.trim());
49023
- const email = (cols[0] ?? '').toLowerCase();
49024
- // Saltar header (primera col "email") o líneas sin @.
49025
- if (!email || !email.includes('@'))
49026
- continue;
49027
- rows.push({ email, name: cols[1] ?? '', roleName: cols[2] ?? '' });
49028
- }
49029
- return rows;
49030
- }
49031
- submit() {
49032
- this.errorMsg.set('');
49033
- const users = this.parseCsv();
49034
- if (users.length === 0) {
49035
- this.errorMsg.set(this.t('noRows'));
49036
- return;
49037
- }
49038
- if (users.some(u => !u.roleName)) {
49039
- this.errorMsg.set(this.t('parseError'));
49040
- return;
49041
- }
49042
- this.submitting.set(true);
49043
- this.orgService
49044
- .importMembers(this.orgId, {
49045
- users,
49046
- onConflict: this.onConflict,
49047
- sendActivation: this.sendActivation,
49048
- })
49049
- .subscribe({
49050
- next: res => {
49051
- this.submitting.set(false);
49052
- this.results.set(res);
49053
- // Refrescar lista del consumer si hubo altas/asignaciones efectivas.
49054
- if (res.summary.created + res.summary.assigned > 0) {
49055
- this.onSuccess?.();
49056
- }
49057
- },
49058
- error: err => {
49059
- this.submitting.set(false);
49060
- this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49061
- },
49062
- });
49063
- }
49064
- statusLabel(r) {
49065
- const base = this.t(r.status === 'error' ? 'failed' : r.status);
49066
- if (r.status === 'created' && r.activation) {
49067
- return `${base} · ${this.t(r.activation === 'sent' ? 'activationSent' : 'activationPending')}`;
49068
- }
49069
- return base;
49070
- }
49071
- reset() {
49072
- this.csv = '';
49073
- this.errorMsg.set('');
49074
- this.results.set(null);
49075
- }
49076
- dismiss() {
49077
- this._modalRef?.dismiss(null, 'cancel');
49078
- }
49079
- t(key) {
49080
- return this.i18n.t(key, this.i18nNamespace);
49081
- }
49082
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49083
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MemberImportModalComponent, isStandalone: true, selector: "val-member-import-modal", inputs: { orgId: "orgId", availableRoles: "availableRoles", onSuccess: "onSuccess", _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
49084
- <ion-header>
49085
- <ion-toolbar>
49086
- <ion-buttons slot="end">
49087
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49088
- <strong>{{ t('close') }}</strong>
49089
- </ion-button>
49090
- </ion-buttons>
49091
- </ion-toolbar>
49092
- </ion-header>
49093
-
49094
- <ion-content class="ion-padding">
49095
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49096
-
49097
- @if (!results()) {
49098
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49099
-
49100
- @if (rolesHint()) {
49101
- <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49102
- }
49103
-
49104
- <ion-textarea
49105
- class="import-csv"
49106
- [autoGrow]="true"
49107
- [rows]="6"
49108
- [placeholder]="t('csvPlaceholder')"
49109
- [(ngModel)]="csv"
49110
- />
49111
-
49112
- <ion-select
49113
- class="import-conflict"
49114
- label="{{ t('onConflictLabel') }}"
49115
- labelPlacement="stacked"
49116
- interface="popover"
49117
- [(ngModel)]="onConflict"
49118
- >
49119
- <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49120
- <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49121
- <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49122
- </ion-select>
49123
-
49124
- <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49125
- {{ t('sendActivation') }}
49126
- </ion-checkbox>
49127
-
49128
- @if (errorMsg()) {
49129
- <p class="import-error">{{ errorMsg() }}</p>
49130
- }
49131
-
49132
- <div class="import-actions">
49133
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49134
- @if (submitting()) {
49135
- <ion-spinner name="dots" />
49136
- &nbsp;{{ t('submitting') }}
49137
- } @else {
49138
- {{ t('submit') }}
49139
- }
49140
- </ion-button>
49141
- </div>
49142
- } @else {
49143
- <!-- Resultados -->
49144
- <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49145
- <div class="import-results">
49146
- @for (r of results()!.results; track r.email) {
49147
- <div class="import-row import-row--{{ r.status }}">
49148
- <span class="import-row__email">{{ r.email }}</span>
49149
- <span class="import-row__status">{{ statusLabel(r) }}</span>
49150
- @if (r.reason) {
49151
- <span class="import-row__reason">{{ r.reason }}</span>
49152
- }
49153
- </div>
49154
- }
49155
- </div>
49156
- <div class="import-actions">
49157
- <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49158
- {{ t('again') }}
49159
- </ion-button>
49160
- </div>
49161
- }
49162
- </ion-content>
49163
- `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:16px;font-family:monospace;font-size:13px}.import-conflict{margin-bottom:12px}.import-activation{display:block;margin-bottom:16px;font-size:14px}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
49164
- }
49165
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, decorators: [{
49166
- type: Component,
49167
- args: [{ selector: 'val-member-import-modal', standalone: true, imports: [
49168
- FormsModule,
49169
- IonHeader,
49170
- IonToolbar,
49171
- IonButtons,
49172
- IonButton,
49173
- IonContent,
49174
- IonTextarea,
49175
- IonSelect,
49176
- IonSelectOption,
49177
- IonCheckbox,
49178
- IonSpinner,
49179
- DisplayComponent,
49180
- TextComponent,
49181
- TitleComponent,
49182
- ], template: `
49183
- <ion-header>
49184
- <ion-toolbar>
49185
- <ion-buttons slot="end">
49186
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49187
- <strong>{{ t('close') }}</strong>
49188
- </ion-button>
49189
- </ion-buttons>
49190
- </ion-toolbar>
49191
- </ion-header>
49192
-
49193
- <ion-content class="ion-padding">
49194
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49195
-
49196
- @if (!results()) {
49197
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49198
-
49199
- @if (rolesHint()) {
49200
- <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49201
- }
49202
-
49203
- <ion-textarea
49204
- class="import-csv"
49205
- [autoGrow]="true"
49206
- [rows]="6"
49207
- [placeholder]="t('csvPlaceholder')"
49208
- [(ngModel)]="csv"
49209
- />
49210
-
49211
- <ion-select
49212
- class="import-conflict"
49213
- label="{{ t('onConflictLabel') }}"
49214
- labelPlacement="stacked"
49215
- interface="popover"
49216
- [(ngModel)]="onConflict"
49217
- >
49218
- <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49219
- <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49220
- <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49221
- </ion-select>
49222
-
49223
- <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49224
- {{ t('sendActivation') }}
49225
- </ion-checkbox>
49226
-
49227
- @if (errorMsg()) {
49228
- <p class="import-error">{{ errorMsg() }}</p>
49229
- }
49230
-
49231
- <div class="import-actions">
49232
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49233
- @if (submitting()) {
49234
- <ion-spinner name="dots" />
49235
- &nbsp;{{ t('submitting') }}
49236
- } @else {
49237
- {{ t('submit') }}
49238
- }
49239
- </ion-button>
49240
- </div>
49241
- } @else {
49242
- <!-- Resultados -->
49243
- <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49244
- <div class="import-results">
49245
- @for (r of results()!.results; track r.email) {
49246
- <div class="import-row import-row--{{ r.status }}">
49247
- <span class="import-row__email">{{ r.email }}</span>
49248
- <span class="import-row__status">{{ statusLabel(r) }}</span>
49249
- @if (r.reason) {
49250
- <span class="import-row__reason">{{ r.reason }}</span>
49251
- }
49252
- </div>
49253
- }
49254
- </div>
49255
- <div class="import-actions">
49256
- <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49257
- {{ t('again') }}
49258
- </ion-button>
49259
- </div>
49260
- }
49261
- </ion-content>
49262
- `, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:16px;font-family:monospace;font-size:13px}.import-conflict{margin-bottom:12px}.import-activation{display:block;margin-bottom:16px;font-size:14px}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"] }]
49263
- }], ctorParameters: () => [], propDecorators: { orgId: [{
49264
- type: Input
49265
- }], availableRoles: [{
49266
- type: Input
49267
- }], onSuccess: [{
49268
- type: Input
49269
- }], _modalRef: [{
49270
- type: Input
49271
- }], i18nNamespace: [{
49272
- type: Input
49273
- }] } });
49274
-
49275
48834
  /**
49276
48835
  * Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
49277
48836
  * el constructor del componente si el consumer no proveyó el namespace
@@ -49327,7 +48886,6 @@ const ORGANIZATION_VIEW_I18N = {
49327
48886
  inviteCtaTitle: '¿Deseas agregar a alguien?',
49328
48887
  inviteCtaHint: 'Invita a usuarios a unirse a tu organización.',
49329
48888
  inviteOpen: 'Invitar',
49330
- importOpen: 'Importar',
49331
48889
  viewMemberCta: 'Ver miembro',
49332
48890
  roleViewer: 'Visor',
49333
48891
  roleEditor: 'Editor',
@@ -49397,7 +48955,6 @@ const ORGANIZATION_VIEW_I18N = {
49397
48955
  inviteCtaTitle: 'Want to add someone?',
49398
48956
  inviteCtaHint: 'Invite users to join your organization.',
49399
48957
  inviteOpen: 'Invite',
49400
- importOpen: 'Import',
49401
48958
  viewMemberCta: 'View member',
49402
48959
  roleViewer: 'Viewer',
49403
48960
  roleEditor: 'Editor',
@@ -49422,7 +48979,7 @@ const ORGANIZATION_VIEW_I18N = {
49422
48979
  },
49423
48980
  };
49424
48981
 
49425
- const DEFAULT_NAMESPACE$1 = 'Settings.Organization';
48982
+ const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
49426
48983
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
49427
48984
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
49428
48985
  /**
@@ -49479,7 +49036,7 @@ class OrganizationViewComponent {
49479
49036
  showInvite: merged.showInvite ?? true,
49480
49037
  showTransferOwnership: merged.showTransferOwnership ?? true,
49481
49038
  showLeave: merged.showLeave ?? true,
49482
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
49039
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
49483
49040
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
49484
49041
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
49485
49042
  onOrgEdited: merged.onOrgEdited,
@@ -49511,22 +49068,6 @@ class OrganizationViewComponent {
49511
49068
  this.members = signal([]);
49512
49069
  this.membersLoading = signal(false);
49513
49070
  this.membersLoadError = signal(null);
49514
- this.membersErrorState = computed(() => {
49515
- this.i18n.lang();
49516
- const err = this.membersLoadError();
49517
- if (!err)
49518
- return null;
49519
- return createErrorStateProps(err, {
49520
- title: { offline: this.tt('offlineTitle'), error: this.tt('errorTitle') },
49521
- description: {
49522
- offline: this.tt('offlineHint'),
49523
- error: interpretError(err).message,
49524
- },
49525
- retryLabel: this.tt('retry'),
49526
- onRetry: () => this.loadMembers(),
49527
- retrying: this.membersLoading(),
49528
- });
49529
- });
49530
49071
  this.membersNextToken = signal(null);
49531
49072
  this.loadingMoreMembers = signal(false);
49532
49073
  this.showAllMembers = signal(false);
@@ -49613,14 +49154,6 @@ class OrganizationViewComponent {
49613
49154
  shape: 'round',
49614
49155
  type: 'button',
49615
49156
  }));
49616
- this.openImportButtonProps = computed(() => ({
49617
- text: this.tt('importOpen'),
49618
- color: 'dark',
49619
- fill: 'outline',
49620
- size: 'default',
49621
- shape: 'round',
49622
- type: 'button',
49623
- }));
49624
49157
  /** Guarda para detectar el primer disparo del effect (evita doble carga). */
49625
49158
  this.lastLoadedOrgId = null;
49626
49159
  const ns = this.ns;
@@ -49721,25 +49254,6 @@ class OrganizationViewComponent {
49721
49254
  backdropDismiss: true,
49722
49255
  });
49723
49256
  }
49724
- onOpenImport() {
49725
- void this.modalService.open({
49726
- component: MemberImportModalComponent,
49727
- componentProps: {
49728
- orgId: this.activeOrgId(),
49729
- availableRoles: this.availableRoles().map(r => r.name),
49730
- onSuccess: () => {
49731
- this.loadMembers();
49732
- this.resolvedConfig().onMemberInvited?.();
49733
- },
49734
- },
49735
- breakpoints: {
49736
- breakpoints: [0, 0.92],
49737
- initialBreakpoint: 0.92,
49738
- showHandle: true,
49739
- },
49740
- backdropDismiss: true,
49741
- });
49742
- }
49743
49257
  onViewPermissions() {
49744
49258
  this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
49745
49259
  }
@@ -49821,7 +49335,6 @@ class OrganizationViewComponent {
49821
49335
  if (!orgId)
49822
49336
  return Promise.resolve();
49823
49337
  this.membersLoading.set(true);
49824
- this.membersLoadError.set(null);
49825
49338
  this.membersNextToken.set(null);
49826
49339
  this.showAllMembers.set(false);
49827
49340
  return new Promise(resolve => {
@@ -50032,8 +49545,6 @@ class OrganizationViewComponent {
50032
49545
  <div class="section-body">
50033
49546
  @if (membersLoading()) {
50034
49547
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50035
- } @else if (membersErrorState()) {
50036
- <val-empty-state [props]="membersErrorState()!" />
50037
49548
  } @else if (members().length === 0) {
50038
49549
  <val-text
50039
49550
  [props]="{
@@ -50126,7 +49637,6 @@ class OrganizationViewComponent {
50126
49637
  />
50127
49638
  </div>
50128
49639
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50129
- <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
50130
49640
  </div>
50131
49641
  </section>
50132
49642
  }
@@ -50235,7 +49745,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50235
49745
  ButtonComponent,
50236
49746
  MemberCardComponent,
50237
49747
  TransferOwnershipModalComponent,
50238
- MemberImportModalComponent,
50239
49748
  ], template: `
50240
49749
  <div class="page">
50241
49750
  <header class="page-header">
@@ -50314,8 +49823,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50314
49823
  <div class="section-body">
50315
49824
  @if (membersLoading()) {
50316
49825
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50317
- } @else if (membersErrorState()) {
50318
- <val-empty-state [props]="membersErrorState()!" />
50319
49826
  } @else if (members().length === 0) {
50320
49827
  <val-text
50321
49828
  [props]="{
@@ -50408,7 +49915,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50408
49915
  />
50409
49916
  </div>
50410
49917
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50411
- <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
50412
49918
  </div>
50413
49919
  </section>
50414
49920
  }
@@ -50537,6 +50043,70 @@ function provideValtechOrganizationRoutes(opts) {
50537
50043
  ];
50538
50044
  }
50539
50045
 
50046
+ /**
50047
+ * Helper para montar el bloque completo de Settings en una app del factory: el
50048
+ * HUB de navegación (`val-settings-hub`) en el index + las vistas promovidas
50049
+ * (Perfil, Preferencias) + las rutas propias de la app (`extraRoutes`).
50050
+ *
50051
+ * Pensado para apps nuevas (greenfield) que adoptan el set entero. showcase NO
50052
+ * lo usa hoy (preserva su `settings.routes.ts` por la semántica de sus muchas
50053
+ * secciones no promovidas — ver guía de promoción), pero el provider debe
50054
+ * existir y compilar para el resto de apps.
50055
+ *
50056
+ * @example
50057
+ * ```ts
50058
+ * // app.routes.ts
50059
+ * import { provideValtechSettingsRoutes } from 'valtech-components';
50060
+ *
50061
+ * export const routes: Routes = [
50062
+ * {
50063
+ * path: 'app',
50064
+ * children: [
50065
+ * ...provideValtechSettingsRoutes({
50066
+ * hubConfig: { onLogout: () => auth.logoutWithConfirmation() },
50067
+ * extraRoutes: [
50068
+ * { path: 'security', loadComponent: () => ... },
50069
+ * ],
50070
+ * }),
50071
+ * ],
50072
+ * },
50073
+ * ];
50074
+ * ```
50075
+ */
50076
+ function provideValtechSettingsRoutes(opts) {
50077
+ const basePath = opts?.basePath ?? 'settings';
50078
+ const includeProfile = opts?.includeProfile ?? true;
50079
+ const includePreferences = opts?.includePreferences ?? true;
50080
+ const includeSecurity = opts?.includeSecurity ?? true;
50081
+ const includeAccount = opts?.includeAccount ?? true;
50082
+ const includeOrganization = opts?.includeOrganization ?? true;
50083
+ return [
50084
+ {
50085
+ path: basePath,
50086
+ children: [
50087
+ {
50088
+ path: '',
50089
+ component: SettingsHubComponent,
50090
+ data: { settingsHubConfig: opts?.hubConfig },
50091
+ },
50092
+ ...(includeProfile ? provideValtechProfileRoutes({ config: opts?.profileConfig }) : []),
50093
+ ...(includePreferences
50094
+ ? provideValtechPreferencesRoutes({
50095
+ path: opts?.preferencesPath ?? 'preferences',
50096
+ config: opts?.preferencesConfig,
50097
+ })
50098
+ : []),
50099
+ ...(includeSecurity ? provideValtechSecurityRoutes({ config: opts?.securityConfig }) : []),
50100
+ ...(includeAccount ? provideValtechAccountRoutes({ config: opts?.accountConfig }) : []),
50101
+ ...(includeOrganization
50102
+ ? provideValtechOrganizationRoutes({ config: opts?.organizationConfig })
50103
+ : []),
50104
+ ...(opts?.extraRoutes ?? []),
50105
+ ],
50106
+ },
50107
+ ];
50108
+ }
50109
+
50540
50110
  /**
50541
50111
  * Refreshable Stream
50542
50112
  *
@@ -50745,7 +50315,7 @@ addIcons({
50745
50315
  openOutline,
50746
50316
  personOutline,
50747
50317
  });
50748
- const DEFAULT_NAMESPACE = 'Notifications';
50318
+ const DEFAULT_NAMESPACE$2 = 'Notifications';
50749
50319
  const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
50750
50320
  const DEFAULT_PAGE_SIZE = 10;
50751
50321
  const DEFAULT_MAX_LOAD = 50;
@@ -50805,7 +50375,7 @@ class NotificationsViewComponent {
50805
50375
  showPushOffCta: merged.showPushOffCta ?? true,
50806
50376
  enableMarkAll: merged.enableMarkAll ?? true,
50807
50377
  settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
50808
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
50378
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
50809
50379
  pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
50810
50380
  maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
50811
50381
  onNotificationOpen: merged.onNotificationOpen,
@@ -51473,6 +51043,1419 @@ function provideValtechNotificationsRoutes(opts) {
51473
51043
  ];
51474
51044
  }
51475
51045
 
51046
+ /**
51047
+ * Defaults i18n (es/en) embebidos en `val-notification-preferences-view`.
51048
+ * Auto-registrados en el constructor del componente si el consumer no proveyó
51049
+ * el namespace (`Settings.Notifications` por default). SOLO incluye las keys que
51050
+ * la vista usa.
51051
+ *
51052
+ * Portado del override `Settings.Notifications` (showcase) bajo el proceso de
51053
+ * ADR-021.
51054
+ */
51055
+ const NOTIFICATION_PREFERENCES_VIEW_I18N = {
51056
+ es: {
51057
+ pageTitle: 'Notificaciones',
51058
+ pageDescription: 'Configura como quieres recibir notificaciones',
51059
+ // Control principal (UX simplificado)
51060
+ simpleTitle: 'Notificaciones push en este dispositivo',
51061
+ simpleDescription: 'Recibe alertas en tiempo real incluso con la app cerrada.',
51062
+ enabling: 'Activando…',
51063
+ disabling: 'Desactivando…',
51064
+ enabled: 'Activadas',
51065
+ errorGeneric: 'No pudimos completar la configuración. Verifica los permisos del navegador y reintenta.',
51066
+ retryCta: 'Reintentar',
51067
+ // Mensajes contextuales
51068
+ permissionUnsupported: 'Tu navegador no soporta notificaciones push.',
51069
+ permissionDeniedInfo: 'Tu navegador esta bloqueando las notificaciones. Para habilitarlas, ve a la configuracion del sitio y permitelas manualmente.',
51070
+ // iOS PWA install
51071
+ iosInstallTitle: 'En iOS, instala la app para recibir notificaciones',
51072
+ iosInstallHint: 'Las notificaciones push en iOS (iPhone/iPad) solo funcionan cuando la app está instalada en la pantalla de inicio. Edge, Chrome y Firefox en iOS comparten esta limitación.',
51073
+ iosInstallStep1: 'Abre este sitio en Safari (no en Chrome/Edge).',
51074
+ iosInstallStep2: 'Toca el botón Compartir (□ con flecha hacia arriba).',
51075
+ iosInstallStep3: "Elige 'Añadir a pantalla de inicio'.",
51076
+ iosInstallStep4: 'Abre la app desde el ícono recién creado y vuelve aquí.',
51077
+ // Detalles técnicos (expandible)
51078
+ detailsShow: 'Ver detalles técnicos',
51079
+ detailsHide: 'Ocultar detalles',
51080
+ detailsToken: 'Token',
51081
+ detailsNone: 'Sin registrar',
51082
+ diagnosticsPermission: 'Permiso del navegador',
51083
+ diagnosticsSupported: 'Soporte del navegador',
51084
+ // Toasts
51085
+ enableSuccess: 'Notificaciones habilitadas',
51086
+ disableSuccess: 'Notificaciones deshabilitadas',
51087
+ },
51088
+ en: {
51089
+ pageTitle: 'Notifications',
51090
+ pageDescription: 'Configure how you want to receive notifications',
51091
+ simpleTitle: 'Push notifications on this device',
51092
+ simpleDescription: 'Get real-time alerts even when the app is closed.',
51093
+ enabling: 'Enabling…',
51094
+ disabling: 'Disabling…',
51095
+ enabled: 'Enabled',
51096
+ errorGeneric: "We couldn't complete the setup. Check your browser permissions and retry.",
51097
+ retryCta: 'Retry',
51098
+ permissionUnsupported: 'Your browser does not support push notifications.',
51099
+ permissionDeniedInfo: 'Your browser is blocking notifications. To enable them, go to site settings and allow them manually.',
51100
+ iosInstallTitle: 'On iOS, install the app to receive notifications',
51101
+ iosInstallHint: 'Push notifications on iOS (iPhone/iPad) only work when the app is installed on the Home Screen. Edge, Chrome and Firefox on iOS share this limitation.',
51102
+ iosInstallStep1: 'Open this site in Safari (not Chrome/Edge).',
51103
+ iosInstallStep2: 'Tap the Share button (□ with an up arrow).',
51104
+ iosInstallStep3: "Choose 'Add to Home Screen'.",
51105
+ iosInstallStep4: 'Open the app from the new icon and come back here.',
51106
+ detailsShow: 'View technical details',
51107
+ detailsHide: 'Hide details',
51108
+ detailsToken: 'Token',
51109
+ detailsNone: 'Not registered',
51110
+ diagnosticsPermission: 'Browser permission',
51111
+ diagnosticsSupported: 'Browser support',
51112
+ enableSuccess: 'Notifications enabled',
51113
+ disableSuccess: 'Notifications disabled',
51114
+ },
51115
+ };
51116
+
51117
+ addIcons({
51118
+ alertCircleOutline,
51119
+ chevronDownOutline,
51120
+ chevronUpOutline,
51121
+ closeCircleOutline,
51122
+ notificationsOffOutline,
51123
+ notificationsOutline,
51124
+ refreshOutline,
51125
+ });
51126
+ const DEFAULT_NAMESPACE$1 = 'Settings.Notifications';
51127
+ /**
51128
+ * `val-notification-preferences-view` — **preferencias de notificaciones push**
51129
+ * (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
51130
+ * proceso de ADR-021.
51131
+ *
51132
+ * Separada de `val-notifications-view` (el inbox / feed). Aquí el user **activa o
51133
+ * desactiva** las notificaciones push en este dispositivo.
51134
+ *
51135
+ * UX simplificado: UN solo toggle que ejecuta toda la secuencia (pedir permiso →
51136
+ * SW ready → getToken → registrar en backend → set master pref) vía
51137
+ * `MessagingService.enable()`. El registro del device en el backend va como
51138
+ * callback (la lib no depende de `AuthService` para evitar ciclos). Mensajes
51139
+ * contextuales y botón "Reintentar" aparecen solo cuando aplican.
51140
+ *
51141
+ * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
51142
+ * inyecta `{ optional: true }` → funciona sin router (embebida / Storybook).
51143
+ */
51144
+ class NotificationPreferencesViewComponent {
51145
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
51146
+ get ns() {
51147
+ return this.resolvedConfig().i18nNamespace;
51148
+ }
51149
+ // ---------------------------------------------------------------------------
51150
+ // Lifecycle
51151
+ // ---------------------------------------------------------------------------
51152
+ constructor() {
51153
+ this.nav = inject(NavigationService);
51154
+ this.i18n = inject(I18nService);
51155
+ this.toast = inject(ToastService);
51156
+ this.auth = inject(AuthService);
51157
+ this.messaging = inject(MessagingService);
51158
+ this.pageRefresh = inject(PageRefreshService);
51159
+ this.prefs = inject(PreferencesService);
51160
+ // `ActivatedRoute` opcional → la vista funciona embebida / en Storybook sin
51161
+ // router (solo lo usa para leer el route data de config como fallback).
51162
+ this.route = inject(ActivatedRoute, { optional: true });
51163
+ this.resolvedConfig = computed(() => {
51164
+ const fromRoute = (this.route?.snapshot.data['notificationPreferencesConfig'] ??
51165
+ {});
51166
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
51167
+ return {
51168
+ showDetails: merged.showDetails ?? true,
51169
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
51170
+ onPushEnabled: merged.onPushEnabled,
51171
+ onPushDisabled: merged.onPushDisabled,
51172
+ };
51173
+ });
51174
+ /** Operación en curso (gobierna spinner del toggle + label "Activando…"). */
51175
+ this._busy = signal(null);
51176
+ /** True si el último intento falló — gobierna estado `error` + retry button. */
51177
+ this._lastFailed = signal(false);
51178
+ /** True mientras se re-obtiene el token FCM en background (no afecta la UI). */
51179
+ this._repairingToken = false;
51180
+ /** Toggle del expand de detalles técnicos. */
51181
+ this.showDetails = signal(false);
51182
+ this.permission = signal('default');
51183
+ this.isSupported = signal(false);
51184
+ this.currentToken = signal(null);
51185
+ this.needsIOSInstall = signal(this.detectNeedsIOSInstall());
51186
+ /** FormControl del toggle (sync-eado con `isEnabled` via effect). */
51187
+ this.toggleControl = new FormControl(false, {
51188
+ nonNullable: true,
51189
+ });
51190
+ /** Handler del listener `focus` — referencia guardada para poder removerlo. */
51191
+ this.onWindowFocus = () => this.scheduleBrowserRefresh();
51192
+ /** Timer del debounce del refresh disparado por `focus`. */
51193
+ this.focusDebounceTimer = null;
51194
+ /** Ventana de debounce (ms) para el refresh disparado por `focus`. */
51195
+ this.FOCUS_DEBOUNCE_MS = 500;
51196
+ // ---------------------------------------------------------------------------
51197
+ // Estado derivado
51198
+ // ---------------------------------------------------------------------------
51199
+ this.pageTitle = computed(() => this.tt('pageTitle'));
51200
+ this.pageDescription = computed(() => this.tt('pageDescription'));
51201
+ /**
51202
+ * Estado efectivo del toggle: "el usuario tiene push activo".
51203
+ *
51204
+ * Se deriva SOLO de la decisión del usuario (`master`) y del permiso del SO
51205
+ * (`permission`). El token FCM NO forma parte del estado de UI: vive en
51206
+ * memoria, se pierde en cada recarga/suspensión de la PWA y produciría flicker.
51207
+ * Si falta token con `master && granted`, es un detalle reparable en
51208
+ * background (ver `repairTokenIfNeeded`), no un cambio del toggle.
51209
+ */
51210
+ this.isEnabled = computed(() => this.prefs.notificationsMaster() && this.permission() === 'granted');
51211
+ this.state = computed(() => {
51212
+ if (this.needsIOSInstall())
51213
+ return 'needs-install';
51214
+ if (this.permission() === 'unsupported' || !this.isSupported()) {
51215
+ // iOS standalone aún puede dar isSupported false temprano antes de init —
51216
+ // solo bloqueamos si el browser explícitamente no soporta APIs.
51217
+ if (this.permission() === 'unsupported')
51218
+ return 'unsupported';
51219
+ }
51220
+ const busy = this._busy();
51221
+ if (busy === 'enabling')
51222
+ return 'enabling';
51223
+ if (busy === 'disabling')
51224
+ return 'disabling';
51225
+ if (this.isEnabled())
51226
+ return 'enabled';
51227
+ if (this.permission() === 'denied')
51228
+ return 'blocked';
51229
+ if (this._lastFailed())
51230
+ return 'error';
51231
+ return 'idle';
51232
+ });
51233
+ this.busyLabel = computed(() => {
51234
+ const s = this.state();
51235
+ if (s === 'enabling')
51236
+ return this.tt('enabling');
51237
+ if (s === 'disabling')
51238
+ return this.tt('disabling');
51239
+ return null;
51240
+ });
51241
+ this.contextMessage = computed(() => {
51242
+ switch (this.state()) {
51243
+ case 'unsupported':
51244
+ return this.tt('permissionUnsupported');
51245
+ case 'blocked':
51246
+ return this.tt('permissionDeniedInfo');
51247
+ case 'error':
51248
+ return this.tt('errorGeneric');
51249
+ default:
51250
+ return null;
51251
+ }
51252
+ });
51253
+ this.alertVariant = computed(() => {
51254
+ const s = this.state();
51255
+ if (s === 'error' || s === 'blocked')
51256
+ return 'warning';
51257
+ if (s === 'unsupported')
51258
+ return 'danger';
51259
+ return 'info';
51260
+ });
51261
+ this.alertIcon = computed(() => {
51262
+ const s = this.state();
51263
+ if (s === 'unsupported')
51264
+ return 'close-circle-outline';
51265
+ return 'alert-circle-outline';
51266
+ });
51267
+ this.toggleProps = computed(() => ({
51268
+ control: this.toggleControl,
51269
+ color: 'primary',
51270
+ disabled: this._busy() !== null ||
51271
+ this.state() === 'unsupported' ||
51272
+ this.state() === 'needs-install' ||
51273
+ this.state() === 'blocked',
51274
+ }));
51275
+ this.retryButtonProps = computed(() => ({
51276
+ text: this.tt('retryCta'),
51277
+ color: 'dark',
51278
+ fill: 'outline',
51279
+ size: 'default',
51280
+ shape: 'round',
51281
+ type: 'button',
51282
+ icon: { name: 'refresh-outline', slot: 'start' },
51283
+ state: this._busy() !== null ? ComponentStates.WORKING : ComponentStates.ENABLED,
51284
+ }));
51285
+ this.tokenPreview = computed(() => {
51286
+ const t = this.currentToken();
51287
+ if (!t)
51288
+ return null;
51289
+ return `${t.slice(0, 22)}…${t.slice(-6)}`;
51290
+ });
51291
+ const ns = this.ns;
51292
+ if (!this.i18n.hasNamespace(ns)) {
51293
+ this.i18n.registerContent(ns, NOTIFICATION_PREFERENCES_VIEW_I18N);
51294
+ }
51295
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
51296
+ void this.refreshBrowserState();
51297
+ // Re-aplicar header en cambio de idioma.
51298
+ effect(() => {
51299
+ this.i18n.lang();
51300
+ this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
51301
+ }, { allowSignalWrites: true });
51302
+ // Sync FormControl ←→ estado efectivo. Cuando el estado cambia (cambio de
51303
+ // permission por el browser, o snapshot Firestore de `master`), el toggle se
51304
+ // re-alinea con la realidad.
51305
+ //
51306
+ // IMPORTANTE: se ignoran los cambios mientras hay una operación en curso
51307
+ // (`_busy() !== null`). Durante `onEnable`/`onDisable` las señales fuente se
51308
+ // actualizan en momentos distintos y producirían estados intermedios →
51309
+ // flicker visible. El toggle se re-alinea UNA sola vez en el `finally`.
51310
+ effect(() => {
51311
+ const eff = this.isEnabled();
51312
+ if (this._busy() !== null)
51313
+ return;
51314
+ if (this.toggleControl.value !== eff) {
51315
+ this.toggleControl.setValue(eff, { emitEvent: false });
51316
+ }
51317
+ }, { allowSignalWrites: true });
51318
+ // Interceptar click del user: el toggle ya cambió visualmente, ejecutar
51319
+ // la acción correspondiente.
51320
+ this.toggleControl.valueChanges.subscribe(v => {
51321
+ if (this._busy() !== null)
51322
+ return;
51323
+ if (v && !this.isEnabled()) {
51324
+ void this.onEnable();
51325
+ }
51326
+ else if (!v && this.isEnabled()) {
51327
+ void this.onDisable();
51328
+ }
51329
+ });
51330
+ // Detectar regreso a la pestaña — el user pudo haber cambiado permisos
51331
+ // desde la config del browser. El refresh va con debounce para no
51332
+ // dispararse en ráfagas (en PWA mobile `focus` salta con cada cambio de
51333
+ // app / aparición del teclado). El listener se remueve en ngOnDestroy.
51334
+ if (typeof window !== 'undefined') {
51335
+ window.addEventListener('focus', this.onWindowFocus);
51336
+ }
51337
+ }
51338
+ /**
51339
+ * Registra el pull-to-refresh estándar. Aunque esta vista no muestra una
51340
+ * lista remota, sí refleja estado mutable del browser (permiso, token,
51341
+ * soporte FCM) que puede cambiar fuera de la app — el refresh re-evalúa todo.
51342
+ *
51343
+ * Se usa `ngOnInit`/`ngOnDestroy` (NO `ionViewWillEnter`) porque
51344
+ * `val-page-wrapper` usa `<router-outlet>` plano — los hooks `ionView*` solo
51345
+ * disparan dentro de `<ion-router-outlet>`.
51346
+ */
51347
+ ngOnInit() {
51348
+ this.pageRefresh.register(() => this.refreshBrowserState());
51349
+ }
51350
+ ngOnDestroy() {
51351
+ this.pageRefresh.unregister();
51352
+ if (typeof window !== 'undefined') {
51353
+ window.removeEventListener('focus', this.onWindowFocus);
51354
+ }
51355
+ if (this.focusDebounceTimer !== null) {
51356
+ clearTimeout(this.focusDebounceTimer);
51357
+ this.focusDebounceTimer = null;
51358
+ }
51359
+ }
51360
+ /** Refresca el estado del browser con debounce — evita ráfagas de `focus`. */
51361
+ scheduleBrowserRefresh() {
51362
+ if (this.focusDebounceTimer !== null) {
51363
+ clearTimeout(this.focusDebounceTimer);
51364
+ }
51365
+ this.focusDebounceTimer = setTimeout(() => {
51366
+ this.focusDebounceTimer = null;
51367
+ void this.refreshBrowserState();
51368
+ }, this.FOCUS_DEBOUNCE_MS);
51369
+ }
51370
+ // ---------------------------------------------------------------------------
51371
+ // Actions
51372
+ // ---------------------------------------------------------------------------
51373
+ /**
51374
+ * Secuencia completa de activación.
51375
+ *
51376
+ * Toda la mecánica (permission → SW ready → getToken con retry → watchdog de
51377
+ * auto-reload) vive en `MessagingService.enable()` de la lib. El paso de
51378
+ * registro del device en el backend se pasa como callback (`registerDevice`)
51379
+ * porque la lib no puede depender de `AuthService` sin crear un ciclo.
51380
+ *
51381
+ * `enable()` no hace throw: resuelve con un `EnablePushResult` descriptivo.
51382
+ */
51383
+ async onEnable() {
51384
+ this._busy.set('enabling');
51385
+ this._lastFailed.set(false);
51386
+ try {
51387
+ const result = await this.messaging.enable({
51388
+ registerDevice: token => this.auth.registerDevice(token).then(r => r.registered),
51389
+ });
51390
+ if (result.status === 'enabled') {
51391
+ // Persistir flag master si aún no estaba activo.
51392
+ if (!this.prefs.notificationsMaster()) {
51393
+ await this.prefs.setNotificationsMaster(true);
51394
+ }
51395
+ this.toast.show({
51396
+ message: this.tt('enableSuccess'),
51397
+ duration: 2000,
51398
+ color: 'dark',
51399
+ position: 'top',
51400
+ });
51401
+ this.resolvedConfig().onPushEnabled?.();
51402
+ }
51403
+ else if (result.status === 'timeout' && result.reloaded) {
51404
+ // La lib disparó el auto-reload — la página está por recargarse.
51405
+ return;
51406
+ }
51407
+ else {
51408
+ // denied / unsupported / error / timeout sin reload → fallo recuperable.
51409
+ this._lastFailed.set(true);
51410
+ this.toast.show({
51411
+ message: this.tt('errorGeneric'),
51412
+ duration: 3000,
51413
+ color: 'dark',
51414
+ position: 'top',
51415
+ });
51416
+ }
51417
+ }
51418
+ catch {
51419
+ // `enable()` no hace throw, pero el callback de registro sí puede.
51420
+ this._lastFailed.set(true);
51421
+ this.toast.show({
51422
+ message: this.tt('errorGeneric'),
51423
+ duration: 3000,
51424
+ color: 'dark',
51425
+ position: 'top',
51426
+ });
51427
+ }
51428
+ finally {
51429
+ // Único refresh final: re-alinea el toggle UNA sola vez con el estado real.
51430
+ await this.refreshBrowserState();
51431
+ this._busy.set(null);
51432
+ }
51433
+ }
51434
+ async onDisable() {
51435
+ this._busy.set('disabling');
51436
+ this._lastFailed.set(false);
51437
+ try {
51438
+ await this.auth.disableNotifications();
51439
+ if (this.prefs.notificationsMaster()) {
51440
+ await this.prefs.setNotificationsMaster(false);
51441
+ }
51442
+ this.toast.show({
51443
+ message: this.tt('disableSuccess'),
51444
+ duration: 2000,
51445
+ color: 'dark',
51446
+ position: 'top',
51447
+ });
51448
+ this.resolvedConfig().onPushDisabled?.();
51449
+ }
51450
+ catch {
51451
+ this._lastFailed.set(true);
51452
+ }
51453
+ finally {
51454
+ await this.refreshBrowserState();
51455
+ this._busy.set(null);
51456
+ }
51457
+ }
51458
+ /** Reintentar la secuencia completa de activación, sin importar el estado actual. */
51459
+ async onRetry() {
51460
+ this._lastFailed.set(false);
51461
+ await this.onEnable();
51462
+ }
51463
+ toggleDetails() {
51464
+ this.showDetails.update(v => !v);
51465
+ }
51466
+ // ---------------------------------------------------------------------------
51467
+ // Privados
51468
+ // ---------------------------------------------------------------------------
51469
+ async refreshBrowserState() {
51470
+ this.isSupported.set(await this.messaging.isSupported());
51471
+ this.permission.set(this.auth.getNotificationPermissionState());
51472
+ this.currentToken.set(this.messaging.currentToken ?? null);
51473
+ void this.repairTokenIfNeeded();
51474
+ }
51475
+ /**
51476
+ * Repara el token FCM en background.
51477
+ *
51478
+ * Si el usuario tiene push activo (`master && granted`) pero no hay token en
51479
+ * memoria — caso típico tras un cold start de la PWA — re-obtenemos el token
51480
+ * y re-registramos el device sin tocar el toggle ni mostrar un error
51481
+ * alarmista. El token es un detalle de implementación reparable, no parte del
51482
+ * estado de UI (ver `isEnabled`).
51483
+ */
51484
+ async repairTokenIfNeeded() {
51485
+ if (this._busy() !== null)
51486
+ return;
51487
+ if (this._repairingToken)
51488
+ return;
51489
+ if (!this.isEnabled())
51490
+ return;
51491
+ if (this.currentToken())
51492
+ return;
51493
+ this._repairingToken = true;
51494
+ try {
51495
+ const token = await this.messaging.getToken();
51496
+ if (token) {
51497
+ this.currentToken.set(token);
51498
+ await this.auth.registerDevice(token);
51499
+ }
51500
+ }
51501
+ catch {
51502
+ // Reparación best-effort: no degradamos el toggle ni mostramos error.
51503
+ }
51504
+ finally {
51505
+ this._repairingToken = false;
51506
+ }
51507
+ }
51508
+ /**
51509
+ * iOS browser sin estar instalado como PWA — push web no disponible.
51510
+ * Aplica a Safari y a Edge/Chrome/Firefox en iOS (todos WebKit por mandato Apple).
51511
+ */
51512
+ detectNeedsIOSInstall() {
51513
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
51514
+ return false;
51515
+ }
51516
+ const ua = navigator.userAgent || '';
51517
+ const isIOS = /iPad|iPhone|iPod/.test(ua);
51518
+ if (!isIOS)
51519
+ return false;
51520
+ const isStandalone = window.matchMedia?.('(display-mode: standalone)').matches ||
51521
+ navigator.standalone === true;
51522
+ return !isStandalone;
51523
+ }
51524
+ tt(key) {
51525
+ return this.i18n.t(key, this.ns);
51526
+ }
51527
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
51528
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: NotificationPreferencesViewComponent, isStandalone: true, selector: "val-notification-preferences-view", inputs: { config: "config" }, ngImport: i0, template: `
51529
+ <div class="page">
51530
+ <header class="page-header">
51531
+ <val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
51532
+ <val-title
51533
+ [props]="{
51534
+ size: 'large',
51535
+ color: 'dark',
51536
+ bold: false,
51537
+ content: pageDescription(),
51538
+ }"
51539
+ />
51540
+ </header>
51541
+
51542
+ @if (state() === 'needs-install') {
51543
+ <section class="settings-section">
51544
+ <val-title
51545
+ [props]="{
51546
+ size: 'medium',
51547
+ color: 'dark',
51548
+ bold: true,
51549
+ content: tt('iosInstallTitle'),
51550
+ }"
51551
+ />
51552
+ <div class="section-body">
51553
+ <val-text
51554
+ [props]="{
51555
+ size: 'small',
51556
+ color: 'medium',
51557
+ bold: false,
51558
+ content: tt('iosInstallHint'),
51559
+ }"
51560
+ />
51561
+ <ol class="install-steps">
51562
+ <li>{{ tt('iosInstallStep1') }}</li>
51563
+ <li>{{ tt('iosInstallStep2') }}</li>
51564
+ <li>{{ tt('iosInstallStep3') }}</li>
51565
+ <li>{{ tt('iosInstallStep4') }}</li>
51566
+ </ol>
51567
+ </div>
51568
+ </section>
51569
+ }
51570
+
51571
+ <section class="settings-section">
51572
+ <div class="notif-row">
51573
+ <div class="notif-row__icon">
51574
+ <ion-icon name="notifications-outline" aria-hidden="true" />
51575
+ </div>
51576
+ <div class="notif-row__body">
51577
+ <val-title
51578
+ [props]="{
51579
+ size: 'medium',
51580
+ color: 'dark',
51581
+ bold: true,
51582
+ content: tt('simpleTitle'),
51583
+ }"
51584
+ />
51585
+ <val-text
51586
+ [props]="{
51587
+ size: 'small',
51588
+ color: 'medium',
51589
+ bold: false,
51590
+ content: tt('simpleDescription'),
51591
+ }"
51592
+ />
51593
+ @if (busyLabel(); as label) {
51594
+ <span class="notif-row__busy">{{ label }}</span>
51595
+ }
51596
+ </div>
51597
+ <div class="notif-row__control">
51598
+ <val-toggle-input [props]="toggleProps()" />
51599
+ </div>
51600
+ </div>
51601
+
51602
+ @if (contextMessage(); as msg) {
51603
+ <div class="alert" [class]="'alert--' + alertVariant()">
51604
+ <ion-icon [name]="alertIcon()" aria-hidden="true" />
51605
+ <span>{{ msg }}</span>
51606
+ </div>
51607
+ }
51608
+
51609
+ @if (state() === 'error') {
51610
+ <div class="row-actions">
51611
+ <val-button [props]="retryButtonProps()" (click)="onRetry()" />
51612
+ </div>
51613
+ }
51614
+ </section>
51615
+
51616
+ @if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
51617
+ <section class="settings-section">
51618
+ <button class="details-toggle" type="button" (click)="toggleDetails()">
51619
+ <span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
51620
+ <ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
51621
+ </button>
51622
+ @if (showDetails()) {
51623
+ <dl class="details">
51624
+ <div class="details__row">
51625
+ <dt>{{ tt('detailsToken') }}</dt>
51626
+ <dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
51627
+ </div>
51628
+ <div class="details__row">
51629
+ <dt>{{ tt('diagnosticsPermission') }}</dt>
51630
+ <dd>{{ permission() }}</dd>
51631
+ </div>
51632
+ <div class="details__row">
51633
+ <dt>{{ tt('diagnosticsSupported') }}</dt>
51634
+ <dd>{{ isSupported() ? '✓' : '✗' }}</dd>
51635
+ </div>
51636
+ </dl>
51637
+ }
51638
+ </section>
51639
+ }
51640
+ </div>
51641
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: ToggleInputComponent, selector: "val-toggle-input", inputs: ["preset", "props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
51642
+ }
51643
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, decorators: [{
51644
+ type: Component,
51645
+ args: [{ selector: 'val-notification-preferences-view', standalone: true, imports: [IonIcon, ToggleInputComponent, ButtonComponent, DisplayComponent, TitleComponent, TextComponent], template: `
51646
+ <div class="page">
51647
+ <header class="page-header">
51648
+ <val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
51649
+ <val-title
51650
+ [props]="{
51651
+ size: 'large',
51652
+ color: 'dark',
51653
+ bold: false,
51654
+ content: pageDescription(),
51655
+ }"
51656
+ />
51657
+ </header>
51658
+
51659
+ @if (state() === 'needs-install') {
51660
+ <section class="settings-section">
51661
+ <val-title
51662
+ [props]="{
51663
+ size: 'medium',
51664
+ color: 'dark',
51665
+ bold: true,
51666
+ content: tt('iosInstallTitle'),
51667
+ }"
51668
+ />
51669
+ <div class="section-body">
51670
+ <val-text
51671
+ [props]="{
51672
+ size: 'small',
51673
+ color: 'medium',
51674
+ bold: false,
51675
+ content: tt('iosInstallHint'),
51676
+ }"
51677
+ />
51678
+ <ol class="install-steps">
51679
+ <li>{{ tt('iosInstallStep1') }}</li>
51680
+ <li>{{ tt('iosInstallStep2') }}</li>
51681
+ <li>{{ tt('iosInstallStep3') }}</li>
51682
+ <li>{{ tt('iosInstallStep4') }}</li>
51683
+ </ol>
51684
+ </div>
51685
+ </section>
51686
+ }
51687
+
51688
+ <section class="settings-section">
51689
+ <div class="notif-row">
51690
+ <div class="notif-row__icon">
51691
+ <ion-icon name="notifications-outline" aria-hidden="true" />
51692
+ </div>
51693
+ <div class="notif-row__body">
51694
+ <val-title
51695
+ [props]="{
51696
+ size: 'medium',
51697
+ color: 'dark',
51698
+ bold: true,
51699
+ content: tt('simpleTitle'),
51700
+ }"
51701
+ />
51702
+ <val-text
51703
+ [props]="{
51704
+ size: 'small',
51705
+ color: 'medium',
51706
+ bold: false,
51707
+ content: tt('simpleDescription'),
51708
+ }"
51709
+ />
51710
+ @if (busyLabel(); as label) {
51711
+ <span class="notif-row__busy">{{ label }}</span>
51712
+ }
51713
+ </div>
51714
+ <div class="notif-row__control">
51715
+ <val-toggle-input [props]="toggleProps()" />
51716
+ </div>
51717
+ </div>
51718
+
51719
+ @if (contextMessage(); as msg) {
51720
+ <div class="alert" [class]="'alert--' + alertVariant()">
51721
+ <ion-icon [name]="alertIcon()" aria-hidden="true" />
51722
+ <span>{{ msg }}</span>
51723
+ </div>
51724
+ }
51725
+
51726
+ @if (state() === 'error') {
51727
+ <div class="row-actions">
51728
+ <val-button [props]="retryButtonProps()" (click)="onRetry()" />
51729
+ </div>
51730
+ }
51731
+ </section>
51732
+
51733
+ @if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
51734
+ <section class="settings-section">
51735
+ <button class="details-toggle" type="button" (click)="toggleDetails()">
51736
+ <span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
51737
+ <ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
51738
+ </button>
51739
+ @if (showDetails()) {
51740
+ <dl class="details">
51741
+ <div class="details__row">
51742
+ <dt>{{ tt('detailsToken') }}</dt>
51743
+ <dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
51744
+ </div>
51745
+ <div class="details__row">
51746
+ <dt>{{ tt('diagnosticsPermission') }}</dt>
51747
+ <dd>{{ permission() }}</dd>
51748
+ </div>
51749
+ <div class="details__row">
51750
+ <dt>{{ tt('diagnosticsSupported') }}</dt>
51751
+ <dd>{{ isSupported() ? '✓' : '✗' }}</dd>
51752
+ </div>
51753
+ </dl>
51754
+ }
51755
+ </section>
51756
+ }
51757
+ </div>
51758
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"] }]
51759
+ }], ctorParameters: () => [], propDecorators: { config: [{
51760
+ type: Input
51761
+ }] } });
51762
+
51763
+ /**
51764
+ * Helper para montar las **preferencias de notificaciones push**
51765
+ * (`val-notification-preferences-view`) como ruta en una app del factory. El
51766
+ * `config` se pasa por route `data` (`notificationPreferencesConfig`) y el
51767
+ * componente lo lee como fallback de su `@Input() config`.
51768
+ *
51769
+ * No confundir con `provideValtechNotificationsRoutes` (el inbox / feed).
51770
+ *
51771
+ * @example
51772
+ * ```ts
51773
+ * // app.routes.ts (típicamente bajo settings)
51774
+ * export const settingsRoutes: Routes = [
51775
+ * ...provideValtechNotificationPreferencesRoutes(), // → notifications
51776
+ * ];
51777
+ *
51778
+ * // con config acotada:
51779
+ * ...provideValtechNotificationPreferencesRoutes({ config: { showDetails: false } }),
51780
+ * ...provideValtechNotificationPreferencesRoutes({ path: 'push' }),
51781
+ * ```
51782
+ */
51783
+ function provideValtechNotificationPreferencesRoutes(opts) {
51784
+ return [
51785
+ {
51786
+ path: opts?.path ?? 'notifications',
51787
+ component: NotificationPreferencesViewComponent,
51788
+ data: { notificationPreferencesConfig: opts?.config },
51789
+ },
51790
+ ];
51791
+ }
51792
+
51793
+ /**
51794
+ * Consume el catálogo de permisos del backend (`GET /org/{orgId}/permissions-catalog`).
51795
+ * El backend es la fuente de verdad de qué permisos existen y de su alcance
51796
+ * (globales de plataforma vs. propios de una app). La respuesta incluye el
51797
+ * `appId` del request para que la vista distinga la app actual.
51798
+ *
51799
+ * Promovido desde `showcase` a la lib bajo el proceso de ADR-021. Usa
51800
+ * `VALTECH_AUTH_CONFIG.apiUrl` (como `OrgService`) en vez del `environment` de la
51801
+ * app — la lib no importa el environment del consumer.
51802
+ */
51803
+ class PermissionCatalogService {
51804
+ constructor(config, http) {
51805
+ this.config = config;
51806
+ this.http = http;
51807
+ }
51808
+ getCatalog(orgId) {
51809
+ return this.http.get(`${this.config.apiUrl}/org/${orgId}/permissions-catalog`);
51810
+ }
51811
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
51812
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, providedIn: 'root' }); }
51813
+ }
51814
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, decorators: [{
51815
+ type: Injectable,
51816
+ args: [{ providedIn: 'root' }]
51817
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
51818
+ type: Inject,
51819
+ args: [VALTECH_AUTH_CONFIG]
51820
+ }] }, { type: i1$3.HttpClient }] });
51821
+
51822
+ /**
51823
+ * Defaults i18n (es/en) embebidos en `val-permissions-view`. Auto-registrados en
51824
+ * el constructor del componente si el consumer no proveyó el namespace
51825
+ * (`Settings.Permissions` por default). SOLO incluye las keys que la vista usa.
51826
+ *
51827
+ * Portado EXACTO de `PERMISSIONS_I18N` (showcase) bajo el proceso de ADR-021.
51828
+ */
51829
+ const PERMISSIONS_VIEW_I18N = {
51830
+ es: {
51831
+ pageTitle: 'Permisos',
51832
+ pageDescription: 'Roles y permisos de esta app en la organización activa',
51833
+ systemRolesTitle: 'Roles del sistema',
51834
+ systemRolesHint: 'Predefinidos por la plataforma. No editables.',
51835
+ customRolesTitle: 'Roles personalizados',
51836
+ customRolesHint: 'Definidos por tu organización.',
51837
+ systemBadge: 'Sistema',
51838
+ noCustomRoles: 'Esta organización no tiene roles personalizados.',
51839
+ noPermissions: 'Sin permisos asignados.',
51840
+ // Grupos de alcance
51841
+ groupApp: 'Permisos de la app',
51842
+ groupPlatform: 'Permisos de plataforma',
51843
+ groupOther: 'Otras apps',
51844
+ // Roles del sistema
51845
+ roleViewer: 'Visor',
51846
+ roleEditor: 'Editor',
51847
+ roleAdmin: 'Administrador',
51848
+ roleSuperAdmin: 'Super Administrador',
51849
+ roleBingoAdmin: 'Bingo · Administrador',
51850
+ roleBingoTesorero: 'Bingo · Tesorero',
51851
+ roleBingoTaquilla: 'Bingo · Taquilla',
51852
+ roleBingoOperator: 'Bingo · Operador',
51853
+ // Apps
51854
+ app_showcase: 'Showcase',
51855
+ app_bingo: 'Bingo',
51856
+ // Recursos
51857
+ permUsers: 'Usuarios',
51858
+ permRoles: 'Roles',
51859
+ permRbac: 'Control de acceso',
51860
+ permApps: 'Apps',
51861
+ 'permApi-keys': 'API keys',
51862
+ 'permEmail-logs': 'Logs de correo',
51863
+ 'permSms-logs': 'Logs de SMS',
51864
+ permDocuments: 'Documentos',
51865
+ permMedia: 'Media',
51866
+ permTemplates: 'Plantillas',
51867
+ permRequests: 'Solicitudes',
51868
+ permBingo: 'Bingo',
51869
+ // Acciones
51870
+ permRead: 'Leer',
51871
+ permWrite: 'Escribir',
51872
+ permCreate: 'Crear',
51873
+ permDelete: 'Eliminar',
51874
+ permManage: 'Gestionar',
51875
+ permEdit: 'Editar',
51876
+ permSell: 'Vender',
51877
+ permCheckin: 'Canjear',
51878
+ permSettle: 'Rendir',
51879
+ permExport: 'Exportar',
51880
+ permOperate: 'Operar',
51881
+ permAll: 'Todo',
51882
+ // Loading / error
51883
+ errorTitle: 'Error al cargar permisos',
51884
+ retry: 'Reintentar',
51885
+ },
51886
+ en: {
51887
+ pageTitle: 'Permissions',
51888
+ pageDescription: 'Roles and permissions of this app in the active organization',
51889
+ systemRolesTitle: 'System roles',
51890
+ systemRolesHint: 'Defined by the platform. Not editable.',
51891
+ customRolesTitle: 'Custom roles',
51892
+ customRolesHint: 'Defined by your organization.',
51893
+ systemBadge: 'System',
51894
+ noCustomRoles: 'This organization has no custom roles.',
51895
+ noPermissions: 'No permissions assigned.',
51896
+ groupApp: 'App permissions',
51897
+ groupPlatform: 'Platform permissions',
51898
+ groupOther: 'Other apps',
51899
+ roleViewer: 'Viewer',
51900
+ roleEditor: 'Editor',
51901
+ roleAdmin: 'Administrator',
51902
+ roleSuperAdmin: 'Super Administrator',
51903
+ roleBingoAdmin: 'Bingo · Administrator',
51904
+ roleBingoTesorero: 'Bingo · Treasurer',
51905
+ roleBingoTaquilla: 'Bingo · Box office',
51906
+ roleBingoOperator: 'Bingo · Operator',
51907
+ app_showcase: 'Showcase',
51908
+ app_bingo: 'Bingo',
51909
+ permUsers: 'Users',
51910
+ permRoles: 'Roles',
51911
+ permRbac: 'Access control',
51912
+ permApps: 'Apps',
51913
+ 'permApi-keys': 'API keys',
51914
+ 'permEmail-logs': 'Email logs',
51915
+ 'permSms-logs': 'SMS logs',
51916
+ permDocuments: 'Documents',
51917
+ permMedia: 'Media',
51918
+ permTemplates: 'Templates',
51919
+ permRequests: 'Requests',
51920
+ permBingo: 'Bingo',
51921
+ permRead: 'Read',
51922
+ permWrite: 'Write',
51923
+ permCreate: 'Create',
51924
+ permDelete: 'Delete',
51925
+ permManage: 'Manage',
51926
+ permEdit: 'Edit',
51927
+ permSell: 'Sell',
51928
+ permCheckin: 'Check-in',
51929
+ permSettle: 'Settle',
51930
+ permExport: 'Export',
51931
+ permOperate: 'Operate',
51932
+ permAll: 'All',
51933
+ errorTitle: 'Error loading permissions',
51934
+ retry: 'Retry',
51935
+ },
51936
+ };
51937
+
51938
+ const DEFAULT_NAMESPACE = 'Settings.Permissions';
51939
+ /**
51940
+ * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
51941
+ * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
51942
+ * roles de plataforma) y los roles personalizados de la org activa, con el
51943
+ * catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
51944
+ * Promovida desde `showcase` bajo el proceso de ADR-021.
51945
+ *
51946
+ * **RBAC interno** (como `organization-view`): lee la org activa vía
51947
+ * `AuthService.user().activeOrgId`. La vista NO muta nada — el control de quién
51948
+ * puede verla es responsabilidad del guard de ruta del consumer; no hay gating
51949
+ * fino interno ni se expone como config.
51950
+ *
51951
+ * Datos: `forkJoin` de `OrgService.listOrgRoles(orgId)` (roles de la org) +
51952
+ * `PermissionCatalogService.getCatalog(orgId)` (catálogo + appId actual). El
51953
+ * catálogo define el alcance (`scope`/`appId`) de cada recurso para agrupar.
51954
+ *
51955
+ * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
51956
+ * inyecta `{ optional: true }` solo para leer el route data de config. Auto-registra
51957
+ * sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
51958
+ * `Settings.Permissions`).
51959
+ */
51960
+ class PermissionsViewComponent {
51961
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
51962
+ get ns() {
51963
+ return this.resolvedConfig().i18nNamespace;
51964
+ }
51965
+ constructor() {
51966
+ this.i18n = inject(I18nService);
51967
+ this.orgService = inject(OrgService);
51968
+ this.catalog = inject(PermissionCatalogService);
51969
+ this.auth = inject(AuthService);
51970
+ this.nav = inject(NavigationService);
51971
+ this.route = inject(ActivatedRoute, { optional: true });
51972
+ this.resolvedConfig = computed(() => {
51973
+ const fromRoute = (this.route?.snapshot.data['permissionsConfig'] ?? {});
51974
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
51975
+ return {
51976
+ showSystemRoles: merged.showSystemRoles ?? true,
51977
+ showCustomRoles: merged.showCustomRoles ?? true,
51978
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
51979
+ };
51980
+ });
51981
+ this.loading = signal(false);
51982
+ this.loadError = signal(null);
51983
+ this.roles = signal([]);
51984
+ this.systemRoles = computed(() => this.roles().filter(r => r.isSystem));
51985
+ this.customRoles = computed(() => this.roles().filter(r => !r.isSystem));
51986
+ this.activeOrgId = computed(() => {
51987
+ const u = this.auth.user();
51988
+ return u?.activeOrgId ?? u?.activeOrg ?? '';
51989
+ });
51990
+ this.retryButtonProps = computed(() => ({
51991
+ text: this.tt('retry'),
51992
+ color: 'dark',
51993
+ fill: 'outline',
51994
+ size: 'default',
51995
+ type: 'button',
51996
+ state: this.loading() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
51997
+ }));
51998
+ const ns = this.ns;
51999
+ if (!this.i18n.hasNamespace(ns)) {
52000
+ this.i18n.registerContent(ns, PERMISSIONS_VIEW_I18N);
52001
+ }
52002
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
52003
+ this.load();
52004
+ connectPageRefresh(() => this.load());
52005
+ }
52006
+ load() {
52007
+ const orgId = this.activeOrgId();
52008
+ if (!orgId)
52009
+ return;
52010
+ this.loading.set(true);
52011
+ this.loadError.set(null);
52012
+ forkJoin({
52013
+ roles: this.orgService.listOrgRoles(orgId),
52014
+ catalog: this.catalog.getCatalog(orgId),
52015
+ }).subscribe({
52016
+ next: ({ roles: rawRoles, catalog }) => {
52017
+ const byResource = new Map();
52018
+ for (const r of catalog.permissions)
52019
+ byResource.set(r.resource, r);
52020
+ const currentAppId = catalog.appId;
52021
+ this.roles.set((rawRoles ?? []).map((r) => this.buildRoleView({
52022
+ id: r.id,
52023
+ name: r.name,
52024
+ description: r.description,
52025
+ permissions: r.permissions ?? [],
52026
+ isSystem: r.isSystem ?? false,
52027
+ }, byResource, currentAppId)));
52028
+ this.loading.set(false);
52029
+ },
52030
+ error: err => {
52031
+ this.loadError.set(err);
52032
+ this.loading.set(false);
52033
+ },
52034
+ });
52035
+ }
52036
+ buildRoleView(role, byResource, currentAppId) {
52037
+ const appPerms = [];
52038
+ const platformPerms = [];
52039
+ const otherByApp = new Map();
52040
+ for (const perm of role.permissions) {
52041
+ const [resource, action] = perm.split(':');
52042
+ const label = this.permLabel(resource, action);
52043
+ const entry = byResource.get(resource);
52044
+ if (entry?.scope === 'app') {
52045
+ if (entry.appId === currentAppId) {
52046
+ appPerms.push(label);
52047
+ }
52048
+ else {
52049
+ const appId = entry.appId ?? '?';
52050
+ const arr = otherByApp.get(appId) ?? [];
52051
+ arr.push(label);
52052
+ otherByApp.set(appId, arr);
52053
+ }
52054
+ }
52055
+ else {
52056
+ // global o desconocido → plataforma
52057
+ platformPerms.push(label);
52058
+ }
52059
+ }
52060
+ const groups = [];
52061
+ if (appPerms.length) {
52062
+ groups.push({ key: 'app', label: this.tt('groupApp'), perms: appPerms });
52063
+ }
52064
+ if (platformPerms.length) {
52065
+ groups.push({
52066
+ key: 'platform',
52067
+ label: this.tt('groupPlatform'),
52068
+ perms: platformPerms,
52069
+ });
52070
+ }
52071
+ for (const [appId, perms] of otherByApp) {
52072
+ groups.push({
52073
+ key: `other:${appId}`,
52074
+ label: `${this.tt('groupOther')}: ${this.appLabel(appId)}`,
52075
+ perms,
52076
+ });
52077
+ }
52078
+ return {
52079
+ id: role.id,
52080
+ name: role.name,
52081
+ description: role.description,
52082
+ isSystem: role.isSystem,
52083
+ groups,
52084
+ hasPerms: groups.length > 0,
52085
+ };
52086
+ }
52087
+ tt(key) {
52088
+ return this.i18n.t(key, this.ns);
52089
+ }
52090
+ appLabel(appId) {
52091
+ const key = `app_${appId}`;
52092
+ const label = this.tt(key);
52093
+ return label === key ? appId : label;
52094
+ }
52095
+ roleLabel(name) {
52096
+ const map = {
52097
+ viewer: 'roleViewer',
52098
+ editor: 'roleEditor',
52099
+ admin: 'roleAdmin',
52100
+ super_admin: 'roleSuperAdmin',
52101
+ bingo_admin: 'roleBingoAdmin',
52102
+ bingo_tesorero: 'roleBingoTesorero',
52103
+ bingo_taquilla: 'roleBingoTaquilla',
52104
+ bingo_operator: 'roleBingoOperator',
52105
+ };
52106
+ return this.tt(map[name] ?? name);
52107
+ }
52108
+ permLabel(resource, action) {
52109
+ if (resource === '*' && action === '*')
52110
+ return this.tt('permAll');
52111
+ const resKey = resource === '*' ? 'permAll' : `perm${resource.charAt(0).toUpperCase()}${resource.slice(1)}`;
52112
+ const actKey = action === '*' ? 'permAll' : `perm${action.charAt(0).toUpperCase()}${action.slice(1)}`;
52113
+ const res = this.tt(resKey);
52114
+ const act = this.tt(actKey);
52115
+ return `${res}: ${act}`;
52116
+ }
52117
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
52118
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config" }, ngImport: i0, template: `
52119
+ <div class="page">
52120
+ <header class="page-header">
52121
+ <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
52122
+ <val-title
52123
+ [props]="{
52124
+ size: 'large',
52125
+ color: 'dark',
52126
+ bold: false,
52127
+ content: tt('pageDescription'),
52128
+ }"
52129
+ />
52130
+ </header>
52131
+
52132
+ @if (loading()) {
52133
+ <div class="spinner-row">
52134
+ <ion-spinner name="crescent" />
52135
+ </div>
52136
+ } @else if (loadError()) {
52137
+ <div class="error-row">
52138
+ <val-text
52139
+ [props]="{
52140
+ content: tt('errorTitle'),
52141
+ size: 'medium',
52142
+ color: 'medium',
52143
+ bold: false,
52144
+ }"
52145
+ />
52146
+ <val-button [props]="retryButtonProps()" (click)="load()" />
52147
+ </div>
52148
+ } @else {
52149
+ @if (resolvedConfig().showSystemRoles) {
52150
+ <section class="perm-section">
52151
+ <div class="perm-section__header">
52152
+ <val-title
52153
+ [props]="{
52154
+ size: 'medium',
52155
+ color: 'dark',
52156
+ bold: true,
52157
+ content: tt('systemRolesTitle'),
52158
+ }"
52159
+ />
52160
+ <val-text
52161
+ [props]="{
52162
+ content: tt('systemRolesHint'),
52163
+ size: 'medium',
52164
+ color: 'medium',
52165
+ bold: false,
52166
+ }"
52167
+ />
52168
+ </div>
52169
+ <div class="role-list">
52170
+ @for (role of systemRoles(); track role.id) {
52171
+ <div class="role-card">
52172
+ <div class="role-card__header">
52173
+ <span class="role-card__name">{{ roleLabel(role.name) }}</span>
52174
+ <val-pill
52175
+ [props]="{
52176
+ label: tt('systemBadge'),
52177
+ color: 'primary',
52178
+ weight: 'bold',
52179
+ }"
52180
+ />
52181
+ </div>
52182
+ @if (role.description) {
52183
+ <span class="role-card__desc">{{ role.description }}</span>
52184
+ }
52185
+ @if (!role.hasPerms) {
52186
+ <div class="perm-chips">
52187
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
52188
+ </div>
52189
+ } @else {
52190
+ @for (g of role.groups; track g.key) {
52191
+ <div class="perm-group">
52192
+ <span class="perm-group__label">{{ g.label }}</span>
52193
+ <div class="perm-chips">
52194
+ @for (p of g.perms; track p) {
52195
+ <span class="perm-chip">{{ p }}</span>
52196
+ }
52197
+ </div>
52198
+ </div>
52199
+ }
52200
+ }
52201
+ </div>
52202
+ }
52203
+ </div>
52204
+ </section>
52205
+ }
52206
+
52207
+ @if (resolvedConfig().showCustomRoles) {
52208
+ <section class="perm-section">
52209
+ <div class="perm-section__header">
52210
+ <val-title
52211
+ [props]="{
52212
+ size: 'medium',
52213
+ color: 'dark',
52214
+ bold: true,
52215
+ content: tt('customRolesTitle'),
52216
+ }"
52217
+ />
52218
+ <val-text
52219
+ [props]="{
52220
+ content: tt('customRolesHint'),
52221
+ size: 'medium',
52222
+ color: 'medium',
52223
+ bold: false,
52224
+ }"
52225
+ />
52226
+ </div>
52227
+ @if (customRoles().length === 0) {
52228
+ <val-text
52229
+ [props]="{
52230
+ content: tt('noCustomRoles'),
52231
+ size: 'medium',
52232
+ color: 'medium',
52233
+ bold: false,
52234
+ }"
52235
+ />
52236
+ } @else {
52237
+ <div class="role-list">
52238
+ @for (role of customRoles(); track role.id) {
52239
+ <div class="role-card">
52240
+ <div class="role-card__header">
52241
+ <span class="role-card__name">{{ role.name }}</span>
52242
+ </div>
52243
+ @if (role.description) {
52244
+ <span class="role-card__desc">{{ role.description }}</span>
52245
+ }
52246
+ @if (!role.hasPerms) {
52247
+ <div class="perm-chips">
52248
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
52249
+ </div>
52250
+ } @else {
52251
+ @for (g of role.groups; track g.key) {
52252
+ <div class="perm-group">
52253
+ <span class="perm-group__label">{{ g.label }}</span>
52254
+ <div class="perm-chips">
52255
+ @for (p of g.perms; track p) {
52256
+ <span class="perm-chip">{{ p }}</span>
52257
+ }
52258
+ </div>
52259
+ </div>
52260
+ }
52261
+ }
52262
+ </div>
52263
+ }
52264
+ </div>
52265
+ }
52266
+ </section>
52267
+ }
52268
+ }
52269
+ </div>
52270
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"], dependencies: [{ kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
52271
+ }
52272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, decorators: [{
52273
+ type: Component,
52274
+ args: [{ selector: 'val-permissions-view', standalone: true, imports: [IonSpinner, DisplayComponent, PillComponent, TitleComponent, TextComponent, ButtonComponent], template: `
52275
+ <div class="page">
52276
+ <header class="page-header">
52277
+ <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
52278
+ <val-title
52279
+ [props]="{
52280
+ size: 'large',
52281
+ color: 'dark',
52282
+ bold: false,
52283
+ content: tt('pageDescription'),
52284
+ }"
52285
+ />
52286
+ </header>
52287
+
52288
+ @if (loading()) {
52289
+ <div class="spinner-row">
52290
+ <ion-spinner name="crescent" />
52291
+ </div>
52292
+ } @else if (loadError()) {
52293
+ <div class="error-row">
52294
+ <val-text
52295
+ [props]="{
52296
+ content: tt('errorTitle'),
52297
+ size: 'medium',
52298
+ color: 'medium',
52299
+ bold: false,
52300
+ }"
52301
+ />
52302
+ <val-button [props]="retryButtonProps()" (click)="load()" />
52303
+ </div>
52304
+ } @else {
52305
+ @if (resolvedConfig().showSystemRoles) {
52306
+ <section class="perm-section">
52307
+ <div class="perm-section__header">
52308
+ <val-title
52309
+ [props]="{
52310
+ size: 'medium',
52311
+ color: 'dark',
52312
+ bold: true,
52313
+ content: tt('systemRolesTitle'),
52314
+ }"
52315
+ />
52316
+ <val-text
52317
+ [props]="{
52318
+ content: tt('systemRolesHint'),
52319
+ size: 'medium',
52320
+ color: 'medium',
52321
+ bold: false,
52322
+ }"
52323
+ />
52324
+ </div>
52325
+ <div class="role-list">
52326
+ @for (role of systemRoles(); track role.id) {
52327
+ <div class="role-card">
52328
+ <div class="role-card__header">
52329
+ <span class="role-card__name">{{ roleLabel(role.name) }}</span>
52330
+ <val-pill
52331
+ [props]="{
52332
+ label: tt('systemBadge'),
52333
+ color: 'primary',
52334
+ weight: 'bold',
52335
+ }"
52336
+ />
52337
+ </div>
52338
+ @if (role.description) {
52339
+ <span class="role-card__desc">{{ role.description }}</span>
52340
+ }
52341
+ @if (!role.hasPerms) {
52342
+ <div class="perm-chips">
52343
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
52344
+ </div>
52345
+ } @else {
52346
+ @for (g of role.groups; track g.key) {
52347
+ <div class="perm-group">
52348
+ <span class="perm-group__label">{{ g.label }}</span>
52349
+ <div class="perm-chips">
52350
+ @for (p of g.perms; track p) {
52351
+ <span class="perm-chip">{{ p }}</span>
52352
+ }
52353
+ </div>
52354
+ </div>
52355
+ }
52356
+ }
52357
+ </div>
52358
+ }
52359
+ </div>
52360
+ </section>
52361
+ }
52362
+
52363
+ @if (resolvedConfig().showCustomRoles) {
52364
+ <section class="perm-section">
52365
+ <div class="perm-section__header">
52366
+ <val-title
52367
+ [props]="{
52368
+ size: 'medium',
52369
+ color: 'dark',
52370
+ bold: true,
52371
+ content: tt('customRolesTitle'),
52372
+ }"
52373
+ />
52374
+ <val-text
52375
+ [props]="{
52376
+ content: tt('customRolesHint'),
52377
+ size: 'medium',
52378
+ color: 'medium',
52379
+ bold: false,
52380
+ }"
52381
+ />
52382
+ </div>
52383
+ @if (customRoles().length === 0) {
52384
+ <val-text
52385
+ [props]="{
52386
+ content: tt('noCustomRoles'),
52387
+ size: 'medium',
52388
+ color: 'medium',
52389
+ bold: false,
52390
+ }"
52391
+ />
52392
+ } @else {
52393
+ <div class="role-list">
52394
+ @for (role of customRoles(); track role.id) {
52395
+ <div class="role-card">
52396
+ <div class="role-card__header">
52397
+ <span class="role-card__name">{{ role.name }}</span>
52398
+ </div>
52399
+ @if (role.description) {
52400
+ <span class="role-card__desc">{{ role.description }}</span>
52401
+ }
52402
+ @if (!role.hasPerms) {
52403
+ <div class="perm-chips">
52404
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
52405
+ </div>
52406
+ } @else {
52407
+ @for (g of role.groups; track g.key) {
52408
+ <div class="perm-group">
52409
+ <span class="perm-group__label">{{ g.label }}</span>
52410
+ <div class="perm-chips">
52411
+ @for (p of g.perms; track p) {
52412
+ <span class="perm-chip">{{ p }}</span>
52413
+ }
52414
+ </div>
52415
+ </div>
52416
+ }
52417
+ }
52418
+ </div>
52419
+ }
52420
+ </div>
52421
+ }
52422
+ </section>
52423
+ }
52424
+ }
52425
+ </div>
52426
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"] }]
52427
+ }], ctorParameters: () => [], propDecorators: { config: [{
52428
+ type: Input
52429
+ }] } });
52430
+
52431
+ /**
52432
+ * Helper para montar la vista Permisos (`val-permissions-view`) como ruta en una
52433
+ * app del factory. El `config` se pasa por route `data` (`permissionsConfig`) y el
52434
+ * componente lo lee como fallback de su `@Input() config`.
52435
+ *
52436
+ * @example
52437
+ * ```ts
52438
+ * // settings.routes.ts
52439
+ * export const settingsRoutes: Routes = [
52440
+ * ...provideValtechPermissionsRoutes(), // → /app/settings/permissions
52441
+ * { path: 'preferences', loadComponent: () => ... },
52442
+ * ];
52443
+ *
52444
+ * // con config acotada:
52445
+ * ...provideValtechPermissionsRoutes({ config: { showCustomRoles: false } }),
52446
+ * ...provideValtechPermissionsRoutes({ path: 'roles' }),
52447
+ * ```
52448
+ */
52449
+ function provideValtechPermissionsRoutes(opts) {
52450
+ return [
52451
+ {
52452
+ path: opts?.path ?? 'permissions',
52453
+ component: PermissionsViewComponent,
52454
+ data: { permissionsConfig: opts?.config },
52455
+ },
52456
+ ];
52457
+ }
52458
+
51476
52459
  /** Built-in label sets for the three platform locales. */
51477
52460
  const CALLOUT_LABELS = {
51478
52461
  es: {
@@ -61679,5 +62662,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
61679
62662
  * Generated bundle index. Do not edit.
61680
62663
  */
61681
62664
 
61682
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
62665
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PermissionCatalogService, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
61683
62666
  //# sourceMappingURL=valtech-components.mjs.map