valtech-components 2.0.996 → 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 (50) 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 -76
  6. package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +1 -5
  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 -4
  19. package/fesm2022/valtech-components.mjs +1484 -1003
  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 -5
  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 +8 -5
  39. package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.component.mjs +0 -385
  40. package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.mjs +0 -63
  41. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.component.mjs +0 -313
  42. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.i18n.mjs +0 -63
  43. package/esm2022/lib/services/apikeys/api-keys.service.mjs +0 -46
  44. package/esm2022/lib/services/apikeys/types.mjs +0 -7
  45. package/lib/components/organisms/api-keys-modal/api-keys-modal.component.d.ts +0 -44
  46. package/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.d.ts +0 -6
  47. package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +0 -47
  48. package/lib/components/organisms/member-import-modal/member-import-modal.i18n.d.ts +0 -6
  49. package/lib/services/apikeys/api-keys.service.d.ts +0 -25
  50. package/lib/services/apikeys/types.d.ts +0 -46
@@ -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.996';
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.
@@ -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.
@@ -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
  }
@@ -48907,849 +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$3 = '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$3;
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$3)) {
49007
- this.i18n.registerContent(DEFAULT_NAMESPACE$3, 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
- /**
49276
- * Cliente HTTP de Client API Keys org-scopeadas (ADR-023). La org activa se
49277
- * resuelve en backend desde el JWT, así que el path no la incluye.
49278
- */
49279
- class ApiKeyService {
49280
- constructor(config, http) {
49281
- this.config = config;
49282
- this.http = http;
49283
- }
49284
- get baseUrl() {
49285
- return `${this.config.apiUrl}/org/api-keys`;
49286
- }
49287
- /** Lista las API keys de la org activa. */
49288
- list() {
49289
- return this.http.get(`${this.baseUrl}/`).pipe(map$1(r => r.keys ?? []));
49290
- }
49291
- /** Crea una API key. La respuesta incluye el secreto (se muestra UNA vez). */
49292
- create(req) {
49293
- return this.http.post(`${this.baseUrl}/`, req);
49294
- }
49295
- /** Revoca (elimina) una API key por id. */
49296
- revoke(keyId) {
49297
- return this.http.delete(`${this.baseUrl}/${keyId}`);
49298
- }
49299
- /** Catálogo de permisos asignables a una key. */
49300
- availablePermissions() {
49301
- return this.http
49302
- .get(`${this.baseUrl}/permissions`)
49303
- .pipe(map$1(r => r.permissions ?? []));
49304
- }
49305
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
49306
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, providedIn: 'root' }); }
49307
- }
49308
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, decorators: [{
49309
- type: Injectable,
49310
- args: [{ providedIn: 'root' }]
49311
- }], ctorParameters: () => [{ type: undefined, decorators: [{
49312
- type: Inject,
49313
- args: [VALTECH_AUTH_CONFIG]
49314
- }] }, { type: i1$3.HttpClient }] });
49315
-
49316
- /**
49317
- * Defaults i18n (es/en) embebidos en `val-api-keys-modal` (ADR-023).
49318
- * Auto-registrados si el consumer no proveyó el namespace `Settings.ApiKeysModal`.
49319
- */
49320
- const API_KEYS_MODAL_I18N = {
49321
- es: {
49322
- title: 'Client API Keys',
49323
- hint: 'Llaves M2M org-scopeadas para integraciones (p. ej. importar miembros).',
49324
- empty: 'Aún no hay API keys en esta organización.',
49325
- newKey: 'Nueva API key',
49326
- nameLabel: 'Nombre',
49327
- namePlaceholder: 'Ej: Integración RRHH',
49328
- permissionsLabel: 'Permisos',
49329
- expiresLabel: 'Expira en (días, opcional)',
49330
- create: 'Crear key',
49331
- creating: 'Creando...',
49332
- cancel: 'Cancelar',
49333
- revoke: 'Revocar',
49334
- revokeConfirm: '¿Revocar esta API key? Las integraciones que la usen dejarán de funcionar.',
49335
- secretTitle: 'Copia tu API key ahora',
49336
- secretWarning: 'Este secreto se muestra una sola vez. Guárdalo en un lugar seguro.',
49337
- copy: 'Copiar',
49338
- copied: 'Copiada',
49339
- done: 'Listo',
49340
- statusActive: 'activa',
49341
- lastUsed: 'Último uso',
49342
- never: 'nunca',
49343
- permsCount: 'permisos',
49344
- noName: 'El nombre es obligatorio.',
49345
- noPerms: 'Selecciona al menos un permiso.',
49346
- close: 'Cerrar',
49347
- genericError: 'No se pudo completar la operación. Intenta de nuevo.',
49348
- },
49349
- en: {
49350
- title: 'Client API Keys',
49351
- hint: 'Org-scoped M2M keys for integrations (e.g. member import).',
49352
- empty: 'No API keys in this organization yet.',
49353
- newKey: 'New API key',
49354
- nameLabel: 'Name',
49355
- namePlaceholder: 'E.g: HR integration',
49356
- permissionsLabel: 'Permissions',
49357
- expiresLabel: 'Expires in (days, optional)',
49358
- create: 'Create key',
49359
- creating: 'Creating...',
49360
- cancel: 'Cancel',
49361
- revoke: 'Revoke',
49362
- revokeConfirm: 'Revoke this API key? Integrations using it will stop working.',
49363
- secretTitle: 'Copy your API key now',
49364
- secretWarning: 'This secret is shown only once. Store it somewhere safe.',
49365
- copy: 'Copy',
49366
- copied: 'Copied',
49367
- done: 'Done',
49368
- statusActive: 'active',
49369
- lastUsed: 'Last used',
49370
- never: 'never',
49371
- permsCount: 'permissions',
49372
- noName: 'Name is required.',
49373
- noPerms: 'Select at least one permission.',
49374
- close: 'Close',
49375
- genericError: "Couldn't complete the operation. Try again.",
49376
- },
49377
- };
49378
-
49379
- const DEFAULT_NAMESPACE$2 = 'Settings.ApiKeysModal';
49380
- /**
49381
- * `val-api-keys-modal` — gestión de Client API Keys org-scopeadas (ADR-023 fase 6).
49382
- *
49383
- * Lista las keys de la org, permite crear (mostrando el secreto UNA vez con copia)
49384
- * y revocar. Pensado para habilitar integraciones M2M del import de miembros, pero
49385
- * sirve a cualquier permiso del catálogo. Header canónico (Regla #5).
49386
- */
49387
- class ApiKeysModalComponent {
49388
- constructor() {
49389
- this.i18n = inject(I18nService);
49390
- this.apiKeys = inject(ApiKeyService);
49391
- /** Namespace i18n. */
49392
- this.i18nNamespace = DEFAULT_NAMESPACE$2;
49393
- this.keys = signal([]);
49394
- this.availablePermissions = signal([]);
49395
- this.loading = signal(true);
49396
- this.showCreate = signal(false);
49397
- this.submitting = signal(false);
49398
- this.errorMsg = signal('');
49399
- this.secret = signal(null);
49400
- this.copied = signal(false);
49401
- this.name = '';
49402
- this.expiresInDays = null;
49403
- this.selectedPerms = signal(new Set());
49404
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$2)) {
49405
- this.i18n.registerContent(DEFAULT_NAMESPACE$2, API_KEYS_MODAL_I18N);
49406
- }
49407
- }
49408
- ngOnInit() {
49409
- if (!this.i18n.hasNamespace(this.i18nNamespace)) {
49410
- this.i18n.registerContent(this.i18nNamespace, API_KEYS_MODAL_I18N);
49411
- }
49412
- this.refresh();
49413
- this.apiKeys.availablePermissions().subscribe({
49414
- next: perms => this.availablePermissions.set(perms),
49415
- error: () => { },
49416
- });
49417
- }
49418
- refresh() {
49419
- this.loading.set(true);
49420
- this.apiKeys.list().subscribe({
49421
- next: keys => {
49422
- this.keys.set(keys);
49423
- this.loading.set(false);
49424
- },
49425
- error: () => this.loading.set(false),
49426
- });
49427
- }
49428
- openCreate() {
49429
- this.errorMsg.set('');
49430
- this.name = '';
49431
- this.expiresInDays = null;
49432
- this.selectedPerms.set(new Set());
49433
- this.showCreate.set(true);
49434
- }
49435
- togglePerm(perm, ev) {
49436
- const checked = ev.detail?.checked ?? false;
49437
- const next = new Set(this.selectedPerms());
49438
- if (checked) {
49439
- next.add(perm);
49440
- }
49441
- else {
49442
- next.delete(perm);
49443
- }
49444
- this.selectedPerms.set(next);
49445
- }
49446
- submit() {
49447
- this.errorMsg.set('');
49448
- if (!this.name.trim()) {
49449
- this.errorMsg.set(this.t('noName'));
49450
- return;
49451
- }
49452
- const permissions = Array.from(this.selectedPerms());
49453
- if (permissions.length === 0) {
49454
- this.errorMsg.set(this.t('noPerms'));
49455
- return;
49456
- }
49457
- this.submitting.set(true);
49458
- this.apiKeys
49459
- .create({
49460
- name: this.name.trim(),
49461
- permissions,
49462
- expiresInDays: this.expiresInDays && this.expiresInDays > 0 ? this.expiresInDays : undefined,
49463
- })
49464
- .subscribe({
49465
- next: res => {
49466
- this.submitting.set(false);
49467
- this.showCreate.set(false);
49468
- this.secret.set(res.key);
49469
- },
49470
- error: err => {
49471
- this.submitting.set(false);
49472
- this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49473
- },
49474
- });
49475
- }
49476
- revoke(key) {
49477
- if (!confirm(this.t('revokeConfirm')))
49478
- return;
49479
- this.apiKeys.revoke(key.keyId).subscribe({
49480
- next: () => this.refresh(),
49481
- error: err => this.errorMsg.set(err?.error?.message ?? this.t('genericError')),
49482
- });
49483
- }
49484
- async copy(secret) {
49485
- try {
49486
- await navigator.clipboard.writeText(secret);
49487
- this.copied.set(true);
49488
- }
49489
- catch {
49490
- // clipboard no disponible — el user puede seleccionar el texto manualmente
49491
- }
49492
- }
49493
- finishSecret() {
49494
- this.secret.set(null);
49495
- this.copied.set(false);
49496
- this.refresh();
49497
- }
49498
- dismiss() {
49499
- this._modalRef?.dismiss(null, 'cancel');
49500
- }
49501
- t(key) {
49502
- return this.i18n.t(key, this.i18nNamespace);
49503
- }
49504
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49505
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ApiKeysModalComponent, isStandalone: true, selector: "val-api-keys-modal", inputs: { _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
49506
- <ion-header>
49507
- <ion-toolbar>
49508
- <ion-buttons slot="end">
49509
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49510
- <strong>{{ t('close') }}</strong>
49511
- </ion-button>
49512
- </ion-buttons>
49513
- </ion-toolbar>
49514
- </ion-header>
49515
-
49516
- <ion-content class="ion-padding">
49517
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49518
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49519
-
49520
- @if (secret(); as s) {
49521
- <!-- Secreto recién creado: mostrar UNA vez -->
49522
- <div class="apikey-secret">
49523
- <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49524
- <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49525
- <div class="apikey-secret__box">
49526
- <code>{{ s.secret }}</code>
49527
- </div>
49528
- <div class="apikey-actions">
49529
- <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49530
- {{ copied() ? t('copied') : t('copy') }}
49531
- </ion-button>
49532
- <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49533
- {{ t('done') }}
49534
- </ion-button>
49535
- </div>
49536
- </div>
49537
- } @else {
49538
- @if (loading()) {
49539
- <ion-spinner name="dots" />
49540
- } @else {
49541
- <!-- Lista -->
49542
- @if (keys().length === 0) {
49543
- <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49544
- } @else {
49545
- <div class="apikey-list">
49546
- @for (k of keys(); track k.keyId) {
49547
- <div class="apikey-row">
49548
- <div class="apikey-row__main">
49549
- <span class="apikey-row__name">{{ k.name }}</span>
49550
- <span class="apikey-row__meta">
49551
- {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49552
- {{ k.lastUsedAt || t('never') }}
49553
- </span>
49554
- </div>
49555
- <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49556
- {{ t('revoke') }}
49557
- </ion-button>
49558
- </div>
49559
- }
49560
- </div>
49561
- }
49562
-
49563
- @if (errorMsg()) {
49564
- <p class="apikey-error">{{ errorMsg() }}</p>
49565
- }
49566
-
49567
- <!-- Crear -->
49568
- @if (showCreate()) {
49569
- <div class="apikey-create">
49570
- <ion-input
49571
- label="{{ t('nameLabel') }}"
49572
- labelPlacement="stacked"
49573
- [placeholder]="t('namePlaceholder')"
49574
- [(ngModel)]="name"
49575
- />
49576
- <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49577
- <div class="apikey-perms">
49578
- @for (p of availablePermissions(); track p) {
49579
- <ion-checkbox
49580
- labelPlacement="end"
49581
- [checked]="selectedPerms().has(p)"
49582
- (ionChange)="togglePerm(p, $event)"
49583
- >
49584
- {{ p }}
49585
- </ion-checkbox>
49586
- }
49587
- </div>
49588
- <ion-input
49589
- label="{{ t('expiresLabel') }}"
49590
- labelPlacement="stacked"
49591
- type="number"
49592
- [(ngModel)]="expiresInDays"
49593
- />
49594
- <div class="apikey-actions">
49595
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49596
- @if (submitting()) {
49597
- <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49598
- } @else {
49599
- {{ t('create') }}
49600
- }
49601
- </ion-button>
49602
- <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49603
- {{ t('cancel') }}
49604
- </ion-button>
49605
- </div>
49606
- </div>
49607
- } @else {
49608
- <div class="apikey-actions">
49609
- <ion-button expand="block" color="primary" (click)="openCreate()">
49610
- {{ t('newKey') }}
49611
- </ion-button>
49612
- </div>
49613
- }
49614
- }
49615
- }
49616
- </ion-content>
49617
- `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.apikey-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.apikey-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;border:1.5px solid var(--ion-color-light)}.apikey-row__main{display:flex;flex-direction:column}.apikey-row__name{font-weight:600;color:var(--ion-color-dark)}.apikey-row__meta{font-size:12px;color:var(--ion-color-medium-shade, #6b6e76)}.apikey-create{margin-top:8px;padding:16px;border-radius:12px;background:var(--ion-color-light)}.apikey-create__label{margin:12px 0 4px;font-size:13px;font-weight:600;color:var(--ion-color-dark)}.apikey-perms{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}.apikey-actions{margin-top:16px;display:flex;flex-direction:column;gap:8px}.apikey-error{margin:8px 0;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.apikey-secret__warn{font-size:.875rem;color:var(--ion-color-warning-shade, #b88600);margin:0 0 12px}.apikey-secret__box{padding:12px;border-radius:8px;background:var(--ion-color-dark);color:#fff;word-break:break-all;font-family:monospace;font-size:13px}\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: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "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"] }] }); }
49618
- }
49619
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, decorators: [{
49620
- type: Component,
49621
- args: [{ selector: 'val-api-keys-modal', standalone: true, imports: [
49622
- FormsModule,
49623
- IonHeader,
49624
- IonToolbar,
49625
- IonButtons,
49626
- IonButton,
49627
- IonContent,
49628
- IonInput,
49629
- IonCheckbox,
49630
- IonSpinner,
49631
- DisplayComponent,
49632
- TextComponent,
49633
- TitleComponent,
49634
- ], template: `
49635
- <ion-header>
49636
- <ion-toolbar>
49637
- <ion-buttons slot="end">
49638
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49639
- <strong>{{ t('close') }}</strong>
49640
- </ion-button>
49641
- </ion-buttons>
49642
- </ion-toolbar>
49643
- </ion-header>
49644
-
49645
- <ion-content class="ion-padding">
49646
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49647
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49648
-
49649
- @if (secret(); as s) {
49650
- <!-- Secreto recién creado: mostrar UNA vez -->
49651
- <div class="apikey-secret">
49652
- <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49653
- <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49654
- <div class="apikey-secret__box">
49655
- <code>{{ s.secret }}</code>
49656
- </div>
49657
- <div class="apikey-actions">
49658
- <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49659
- {{ copied() ? t('copied') : t('copy') }}
49660
- </ion-button>
49661
- <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49662
- {{ t('done') }}
49663
- </ion-button>
49664
- </div>
49665
- </div>
49666
- } @else {
49667
- @if (loading()) {
49668
- <ion-spinner name="dots" />
49669
- } @else {
49670
- <!-- Lista -->
49671
- @if (keys().length === 0) {
49672
- <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49673
- } @else {
49674
- <div class="apikey-list">
49675
- @for (k of keys(); track k.keyId) {
49676
- <div class="apikey-row">
49677
- <div class="apikey-row__main">
49678
- <span class="apikey-row__name">{{ k.name }}</span>
49679
- <span class="apikey-row__meta">
49680
- {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49681
- {{ k.lastUsedAt || t('never') }}
49682
- </span>
49683
- </div>
49684
- <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49685
- {{ t('revoke') }}
49686
- </ion-button>
49687
- </div>
49688
- }
49689
- </div>
49690
- }
49691
-
49692
- @if (errorMsg()) {
49693
- <p class="apikey-error">{{ errorMsg() }}</p>
49694
- }
49695
-
49696
- <!-- Crear -->
49697
- @if (showCreate()) {
49698
- <div class="apikey-create">
49699
- <ion-input
49700
- label="{{ t('nameLabel') }}"
49701
- labelPlacement="stacked"
49702
- [placeholder]="t('namePlaceholder')"
49703
- [(ngModel)]="name"
49704
- />
49705
- <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49706
- <div class="apikey-perms">
49707
- @for (p of availablePermissions(); track p) {
49708
- <ion-checkbox
49709
- labelPlacement="end"
49710
- [checked]="selectedPerms().has(p)"
49711
- (ionChange)="togglePerm(p, $event)"
49712
- >
49713
- {{ p }}
49714
- </ion-checkbox>
49715
- }
49716
- </div>
49717
- <ion-input
49718
- label="{{ t('expiresLabel') }}"
49719
- labelPlacement="stacked"
49720
- type="number"
49721
- [(ngModel)]="expiresInDays"
49722
- />
49723
- <div class="apikey-actions">
49724
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49725
- @if (submitting()) {
49726
- <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49727
- } @else {
49728
- {{ t('create') }}
49729
- }
49730
- </ion-button>
49731
- <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49732
- {{ t('cancel') }}
49733
- </ion-button>
49734
- </div>
49735
- </div>
49736
- } @else {
49737
- <div class="apikey-actions">
49738
- <ion-button expand="block" color="primary" (click)="openCreate()">
49739
- {{ t('newKey') }}
49740
- </ion-button>
49741
- </div>
49742
- }
49743
- }
49744
- }
49745
- </ion-content>
49746
- `, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.apikey-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.apikey-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;border:1.5px solid var(--ion-color-light)}.apikey-row__main{display:flex;flex-direction:column}.apikey-row__name{font-weight:600;color:var(--ion-color-dark)}.apikey-row__meta{font-size:12px;color:var(--ion-color-medium-shade, #6b6e76)}.apikey-create{margin-top:8px;padding:16px;border-radius:12px;background:var(--ion-color-light)}.apikey-create__label{margin:12px 0 4px;font-size:13px;font-weight:600;color:var(--ion-color-dark)}.apikey-perms{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}.apikey-actions{margin-top:16px;display:flex;flex-direction:column;gap:8px}.apikey-error{margin:8px 0;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.apikey-secret__warn{font-size:.875rem;color:var(--ion-color-warning-shade, #b88600);margin:0 0 12px}.apikey-secret__box{padding:12px;border-radius:8px;background:var(--ion-color-dark);color:#fff;word-break:break-all;font-family:monospace;font-size:13px}\n"] }]
49747
- }], ctorParameters: () => [], propDecorators: { _modalRef: [{
49748
- type: Input
49749
- }], i18nNamespace: [{
49750
- type: Input
49751
- }] } });
49752
-
49753
48834
  /**
49754
48835
  * Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
49755
48836
  * el constructor del componente si el consumer no proveyó el namespace
@@ -49805,8 +48886,6 @@ const ORGANIZATION_VIEW_I18N = {
49805
48886
  inviteCtaTitle: '¿Deseas agregar a alguien?',
49806
48887
  inviteCtaHint: 'Invita a usuarios a unirse a tu organización.',
49807
48888
  inviteOpen: 'Invitar',
49808
- importOpen: 'Importar',
49809
- apiKeysOpen: 'API Keys',
49810
48889
  viewMemberCta: 'Ver miembro',
49811
48890
  roleViewer: 'Visor',
49812
48891
  roleEditor: 'Editor',
@@ -49876,8 +48955,6 @@ const ORGANIZATION_VIEW_I18N = {
49876
48955
  inviteCtaTitle: 'Want to add someone?',
49877
48956
  inviteCtaHint: 'Invite users to join your organization.',
49878
48957
  inviteOpen: 'Invite',
49879
- importOpen: 'Import',
49880
- apiKeysOpen: 'API Keys',
49881
48958
  viewMemberCta: 'View member',
49882
48959
  roleViewer: 'Viewer',
49883
48960
  roleEditor: 'Editor',
@@ -49902,7 +48979,7 @@ const ORGANIZATION_VIEW_I18N = {
49902
48979
  },
49903
48980
  };
49904
48981
 
49905
- const DEFAULT_NAMESPACE$1 = 'Settings.Organization';
48982
+ const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
49906
48983
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
49907
48984
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
49908
48985
  /**
@@ -49959,7 +49036,7 @@ class OrganizationViewComponent {
49959
49036
  showInvite: merged.showInvite ?? true,
49960
49037
  showTransferOwnership: merged.showTransferOwnership ?? true,
49961
49038
  showLeave: merged.showLeave ?? true,
49962
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
49039
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
49963
49040
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
49964
49041
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
49965
49042
  onOrgEdited: merged.onOrgEdited,
@@ -49991,22 +49068,6 @@ class OrganizationViewComponent {
49991
49068
  this.members = signal([]);
49992
49069
  this.membersLoading = signal(false);
49993
49070
  this.membersLoadError = signal(null);
49994
- this.membersErrorState = computed(() => {
49995
- this.i18n.lang();
49996
- const err = this.membersLoadError();
49997
- if (!err)
49998
- return null;
49999
- return createErrorStateProps(err, {
50000
- title: { offline: this.tt('offlineTitle'), error: this.tt('errorTitle') },
50001
- description: {
50002
- offline: this.tt('offlineHint'),
50003
- error: interpretError(err).message,
50004
- },
50005
- retryLabel: this.tt('retry'),
50006
- onRetry: () => this.loadMembers(),
50007
- retrying: this.membersLoading(),
50008
- });
50009
- });
50010
49071
  this.membersNextToken = signal(null);
50011
49072
  this.loadingMoreMembers = signal(false);
50012
49073
  this.showAllMembers = signal(false);
@@ -50093,22 +49154,6 @@ class OrganizationViewComponent {
50093
49154
  shape: 'round',
50094
49155
  type: 'button',
50095
49156
  }));
50096
- this.openImportButtonProps = computed(() => ({
50097
- text: this.tt('importOpen'),
50098
- color: 'dark',
50099
- fill: 'outline',
50100
- size: 'default',
50101
- shape: 'round',
50102
- type: 'button',
50103
- }));
50104
- this.openApiKeysButtonProps = computed(() => ({
50105
- text: this.tt('apiKeysOpen'),
50106
- color: 'dark',
50107
- fill: 'clear',
50108
- size: 'default',
50109
- shape: 'round',
50110
- type: 'button',
50111
- }));
50112
49157
  /** Guarda para detectar el primer disparo del effect (evita doble carga). */
50113
49158
  this.lastLoadedOrgId = null;
50114
49159
  const ns = this.ns;
@@ -50209,36 +49254,6 @@ class OrganizationViewComponent {
50209
49254
  backdropDismiss: true,
50210
49255
  });
50211
49256
  }
50212
- onOpenImport() {
50213
- void this.modalService.open({
50214
- component: MemberImportModalComponent,
50215
- componentProps: {
50216
- orgId: this.activeOrgId(),
50217
- availableRoles: this.availableRoles().map(r => r.name),
50218
- onSuccess: () => {
50219
- this.loadMembers();
50220
- this.resolvedConfig().onMemberInvited?.();
50221
- },
50222
- },
50223
- breakpoints: {
50224
- breakpoints: [0, 0.92],
50225
- initialBreakpoint: 0.92,
50226
- showHandle: true,
50227
- },
50228
- backdropDismiss: true,
50229
- });
50230
- }
50231
- onOpenApiKeys() {
50232
- void this.modalService.open({
50233
- component: ApiKeysModalComponent,
50234
- breakpoints: {
50235
- breakpoints: [0, 0.92],
50236
- initialBreakpoint: 0.92,
50237
- showHandle: true,
50238
- },
50239
- backdropDismiss: true,
50240
- });
50241
- }
50242
49257
  onViewPermissions() {
50243
49258
  this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
50244
49259
  }
@@ -50320,7 +49335,6 @@ class OrganizationViewComponent {
50320
49335
  if (!orgId)
50321
49336
  return Promise.resolve();
50322
49337
  this.membersLoading.set(true);
50323
- this.membersLoadError.set(null);
50324
49338
  this.membersNextToken.set(null);
50325
49339
  this.showAllMembers.set(false);
50326
49340
  return new Promise(resolve => {
@@ -50531,8 +49545,6 @@ class OrganizationViewComponent {
50531
49545
  <div class="section-body">
50532
49546
  @if (membersLoading()) {
50533
49547
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50534
- } @else if (membersErrorState()) {
50535
- <val-empty-state [props]="membersErrorState()!" />
50536
49548
  } @else if (members().length === 0) {
50537
49549
  <val-text
50538
49550
  [props]="{
@@ -50625,8 +49637,6 @@ class OrganizationViewComponent {
50625
49637
  />
50626
49638
  </div>
50627
49639
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50628
- <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
50629
- <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50630
49640
  </div>
50631
49641
  </section>
50632
49642
  }
@@ -50735,8 +49745,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50735
49745
  ButtonComponent,
50736
49746
  MemberCardComponent,
50737
49747
  TransferOwnershipModalComponent,
50738
- MemberImportModalComponent,
50739
- ApiKeysModalComponent,
50740
49748
  ], template: `
50741
49749
  <div class="page">
50742
49750
  <header class="page-header">
@@ -50815,8 +49823,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50815
49823
  <div class="section-body">
50816
49824
  @if (membersLoading()) {
50817
49825
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50818
- } @else if (membersErrorState()) {
50819
- <val-empty-state [props]="membersErrorState()!" />
50820
49826
  } @else if (members().length === 0) {
50821
49827
  <val-text
50822
49828
  [props]="{
@@ -50909,8 +49915,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50909
49915
  />
50910
49916
  </div>
50911
49917
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50912
- <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
50913
- <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50914
49918
  </div>
50915
49919
  </section>
50916
49920
  }
@@ -51039,6 +50043,70 @@ function provideValtechOrganizationRoutes(opts) {
51039
50043
  ];
51040
50044
  }
51041
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
+
51042
50110
  /**
51043
50111
  * Refreshable Stream
51044
50112
  *
@@ -51247,7 +50315,7 @@ addIcons({
51247
50315
  openOutline,
51248
50316
  personOutline,
51249
50317
  });
51250
- const DEFAULT_NAMESPACE = 'Notifications';
50318
+ const DEFAULT_NAMESPACE$2 = 'Notifications';
51251
50319
  const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
51252
50320
  const DEFAULT_PAGE_SIZE = 10;
51253
50321
  const DEFAULT_MAX_LOAD = 50;
@@ -51307,7 +50375,7 @@ class NotificationsViewComponent {
51307
50375
  showPushOffCta: merged.showPushOffCta ?? true,
51308
50376
  enableMarkAll: merged.enableMarkAll ?? true,
51309
50377
  settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
51310
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
50378
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
51311
50379
  pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
51312
50380
  maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
51313
50381
  onNotificationOpen: merged.onNotificationOpen,
@@ -51975,6 +51043,1419 @@ function provideValtechNotificationsRoutes(opts) {
51975
51043
  ];
51976
51044
  }
51977
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
+
51978
52459
  /** Built-in label sets for the three platform locales. */
51979
52460
  const CALLOUT_LABELS = {
51980
52461
  es: {
@@ -62181,5 +62662,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
62181
62662
  * Generated bundle index. Do not edit.
62182
62663
  */
62183
62664
 
62184
- 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, ApiKeyService, ApiKeysModalComponent, 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 };
62185
62666
  //# sourceMappingURL=valtech-components.mjs.map