valtech-components 4.0.174 → 4.0.176

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.
@@ -1,7 +1,7 @@
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, SecurityContext, NgZone, ViewChild, ElementRef, ChangeDetectorRef, output, ContentChild, model, ViewEncapsulation, untracked, viewChild, Injector, isSignal } 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, forkJoin, race, timer, shareReplay as shareReplay$1 } from 'rxjs';
4
- import { catchError, switchMap, finalize, filter, take, timeout, map as map$1, tap, shareReplay, distinctUntilChanged as distinctUntilChanged$1, retry, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
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, forkJoin, race, timer, shareReplay } from 'rxjs';
4
+ import { catchError, switchMap, finalize, filter, take, timeout, map as map$1, tap, distinctUntilChanged as distinctUntilChanged$1, retry, 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';
7
7
  import * as i1$1 from '@angular/fire/firestore';
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.174';
59
+ const VERSION = '4.0.176';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -2204,7 +2204,10 @@ class AuthStateService {
2204
2204
  * Resetea todo el estado a valores iniciales.
2205
2205
  */
2206
2206
  reset() {
2207
- this._state.set(INITIAL_AUTH_STATE);
2207
+ // isLoading: false — al resetear (logout) ya no hay bootstrap en curso;
2208
+ // INITIAL_AUTH_STATE tiene isLoading:true porque es el estado del arranque frío.
2209
+ // El guestGuard espera isLoading=false para dejar pasar a /login.
2210
+ this._state.set({ ...INITIAL_AUTH_STATE, isLoading: false });
2208
2211
  this._mfaPending.set(INITIAL_MFA_STATE);
2209
2212
  }
2210
2213
  /**
@@ -8349,13 +8352,6 @@ class AuthService {
8349
8352
  this.refreshTimerId = null;
8350
8353
  this.syncSubscription = null;
8351
8354
  this.firestoreRBACUnsubscribe = null;
8352
- // Single-flight refresh: un solo POST /refresh en vuelo a la vez. Todos los
8353
- // callers (bootstrap, reestablecer Firebase, interceptor 401, timer proactivo)
8354
- // comparten el MISMO observable. Sin esto, refreshes concurrentes chocan contra
8355
- // la rotación de refresh token del backend: el primero rota el token y el
8356
- // segundo llega con el token ya invalidado → 401 → logout espurio (el usuario
8357
- // cae a la landing al actualizar). Ver project_pwa-login-freeze.
8358
- this.inFlightRefresh$ = null;
8359
8355
  // =============================================
8360
8356
  // ESTADO PÚBLICO (Signals readonly)
8361
8357
  // =============================================
@@ -8766,20 +8762,13 @@ class AuthService {
8766
8762
  * que el cliente debe guardar para el próximo refresh.
8767
8763
  */
8768
8764
  refreshAccessToken() {
8769
- // Single-flight: si ya hay un /refresh en vuelo, devolvemos el MISMO
8770
- // observable (multicast vía shareReplay). Evita la carrera contra la rotación
8771
- // de refresh token del backend, que producía 401 espurios → logout al
8772
- // actualizar. Ver inFlightRefresh$ y project_pwa-login-freeze.
8773
- if (this.inFlightRefresh$) {
8774
- return this.inFlightRefresh$;
8775
- }
8776
8765
  // H-05 same-site-aware: en same-site (prod) el refresh va SOLO por la cookie
8777
8766
  // HttpOnly (body vacío), no se persiste en localStorage → un XSS no lo roba.
8778
8767
  // En cross-site (dev) la cookie de terceros se bloquea (Safari ITP / Chrome
8779
8768
  // 3rd-party phase-out) → se manda en el body + se persiste (fallback).
8780
8769
  // withCredentials adjunta la cookie cuando el browser la permite.
8781
8770
  const body = this.storageService.cookieOnly ? {} : { refreshToken: this.state().refreshToken };
8782
- this.inFlightRefresh$ = this.http
8771
+ return this.http
8783
8772
  .post(`${this.baseUrl}/refresh`, body, { withCredentials: true })
8784
8773
  .pipe(
8785
8774
  // Nunca dejar el refresh colgado: si la red/Lambda no responde a tiempo,
@@ -8809,17 +8798,7 @@ class AuthService {
8809
8798
  // initialize() only clears state (no redirect). Auto-navigating to
8810
8799
  // /login from this method redirects even users on public routes.
8811
8800
  return throwError(() => error);
8812
- }),
8813
- // Liberar el slot al terminar (éxito, error o timeout) para que el
8814
- // próximo refresh real arranque uno nuevo.
8815
- finalize(() => {
8816
- this.inFlightRefresh$ = null;
8817
- }),
8818
- // Multicast: los side-effects del tap corren UNA vez y todos los callers
8819
- // concurrentes reciben la misma respuesta/error. refCount:false → que un
8820
- // caller se desuscribe temprano NO cancela la rotación de token en vuelo.
8821
- shareReplay({ bufferSize: 1, refCount: false }));
8822
- return this.inFlightRefresh$;
8801
+ }));
8823
8802
  }
8824
8803
  /**
8825
8804
  * Maneja autenticación exitosa desde fuentes externas (OAuth, etc).
@@ -28372,13 +28351,6 @@ class UpdateBannerComponent {
28372
28351
  dismissAction: this.tr('dismissAction'),
28373
28352
  };
28374
28353
  });
28375
- /**
28376
- * Feedback visual mientras se aplica el update. El botón pasa a estado
28377
- * WORKING (spinner) y se bloquean nuevos taps. `applyUpdate()` siempre
28378
- * recarga la página al terminar (try/finally), así que NO reseteamos este
28379
- * flag: queda en true hasta que el reload reemplaza la app.
28380
- */
28381
- this.applying = signal(false);
28382
28354
  if (!this.i18n.hasNamespace(UPDATE_BANNER_I18N_NAMESPACE)) {
28383
28355
  this.i18n.registerContent(UPDATE_BANNER_I18N_NAMESPACE, {
28384
28356
  es: UPDATE_BANNER_DEFAULT_CONTENT.es,
@@ -28388,11 +28360,6 @@ class UpdateBannerComponent {
28388
28360
  }
28389
28361
  /** Aplica la actualización (activa el SW si existe + recarga). */
28390
28362
  onUpdate() {
28391
- // Anti multi-tap: el botón WORKING no queda `disabled` a nivel DOM, así que
28392
- // sin este guard un usuario impaciente dispararía applyUpdate() N veces.
28393
- if (this.applying())
28394
- return;
28395
- this.applying.set(true);
28396
28363
  void this.version.applyUpdate();
28397
28364
  }
28398
28365
  /** Descarta el banner opcional. No-op en modo obligatorio. */
@@ -28462,7 +28429,7 @@ class UpdateBannerComponent {
28462
28429
  color: 'primary',
28463
28430
  fill: 'solid',
28464
28431
  size: 'small',
28465
- state: applying() ? 'WORKING' : 'ENABLED',
28432
+ state: 'ENABLED',
28466
28433
  type: 'button',
28467
28434
  }"
28468
28435
  (onClick)="onUpdate()"
@@ -28474,7 +28441,7 @@ class UpdateBannerComponent {
28474
28441
  color: 'dark',
28475
28442
  fill: 'clear',
28476
28443
  size: 'small',
28477
- state: applying() ? 'DISABLED' : 'ENABLED',
28444
+ state: 'ENABLED',
28478
28445
  type: 'button',
28479
28446
  }"
28480
28447
  (onClick)="onDismiss()"
@@ -28537,7 +28504,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28537
28504
  color: 'primary',
28538
28505
  fill: 'solid',
28539
28506
  size: 'small',
28540
- state: applying() ? 'WORKING' : 'ENABLED',
28507
+ state: 'ENABLED',
28541
28508
  type: 'button',
28542
28509
  }"
28543
28510
  (onClick)="onUpdate()"
@@ -28549,7 +28516,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28549
28516
  color: 'dark',
28550
28517
  fill: 'clear',
28551
28518
  size: 'small',
28552
- state: applying() ? 'DISABLED' : 'ENABLED',
28519
+ state: 'ENABLED',
28553
28520
  type: 'button',
28554
28521
  }"
28555
28522
  (onClick)="onDismiss()"
@@ -49841,384 +49808,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49841
49808
  type: Input
49842
49809
  }] } });
49843
49810
 
49844
- /**
49845
- * Defaults i18n (es/en) embebidos en `val-switch-org-modal`. Auto-registrados en
49846
- * el constructor del componente si el consumer no proveyó el namespace
49847
- * (`Settings.SwitchOrg` por default). SOLO incluye las keys que el modal usa.
49848
- */
49849
- const SWITCH_ORG_MODAL_I18N = {
49850
- es: {
49851
- title: 'Cambiar de cuenta',
49852
- subtitle: 'Selecciona la organización a la que quieres cambiar.',
49853
- searchPlaceholder: 'Buscar organización...',
49854
- noResults: 'No se encontraron organizaciones.',
49855
- current: 'Actual',
49856
- close: 'Cerrar',
49857
- switchError: 'No se pudo cambiar de organización.',
49858
- profile: 'Mi perfil',
49859
- newOrg: 'Nueva organización',
49860
- },
49861
- en: {
49862
- title: 'Switch account',
49863
- subtitle: 'Select the organization you want to switch to.',
49864
- searchPlaceholder: 'Search organization...',
49865
- noResults: 'No organizations found.',
49866
- current: 'Active',
49867
- close: 'Close',
49868
- switchError: 'Could not switch organization.',
49869
- profile: 'My profile',
49870
- newOrg: 'New organization',
49871
- },
49872
- };
49873
-
49874
- const DEFAULT_NAMESPACE$e = 'Settings.SwitchOrg';
49875
- addIcons({ addOutline, businessOutline, checkmarkCircleOutline });
49876
- /**
49877
- * `val-switch-org-modal` — modal de cambio de organización activa (organism
49878
- * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
49879
- * la vista Cuenta (y futuras vistas org-aware).
49880
- *
49881
- * Lista las organizaciones del user (vía `OrgService.listMyOrgs`), permite buscar
49882
- * por nombre, y cambia la org activa con `OrgSwitchService.switchTo`. La org
49883
- * actual queda primera y deshabilitada.
49884
- *
49885
- * Abierto vía `ModalService` — recibe `_modalRef` por `componentProps` + callback
49886
- * `onSuccess`. Header canónico (Regla #5): sin `ion-title`, botón Cerrar texto en
49887
- * `slot=end`, título/subtítulo en el body con `val-display`/`val-title`.
49888
- *
49889
- * Auto-registra sus defaults i18n (es/en) si el consumer no proveyó el namespace
49890
- * `Settings.SwitchOrg`.
49891
- */
49892
- class SwitchOrgModalComponent {
49893
- onLogoError(orgId) {
49894
- const next = new Set(this.failedLogos());
49895
- next.add(orgId);
49896
- this.failedLogos.set(next);
49897
- }
49898
- constructor() {
49899
- this.i18n = inject(I18nService);
49900
- this.auth = inject(AuthService);
49901
- this.orgService = inject(OrgService);
49902
- this.orgSwitch = inject(OrgSwitchService);
49903
- this.errors = inject(ValtechErrorService);
49904
- /** Namespace i18n con que la vista resuelve sus textos. */
49905
- this.i18nNamespace = DEFAULT_NAMESPACE$e;
49906
- this.orgs = signal([]);
49907
- this.loading = signal(false);
49908
- this.query = signal('');
49909
- this.switchingId = signal(null);
49910
- this.user = computed(() => this.auth.user());
49911
- /** Avatar de usuario que falló al cargar → cae a iniciales. */
49912
- this.userAvatarFailed = signal(false);
49913
- /** IDs de orgs cuyo logo falló al cargar → caen al ícono business-outline. */
49914
- this.failedLogos = signal(new Set());
49915
- this.userInitials = computed(() => {
49916
- const name = this.user()?.name ?? this.user()?.email ?? '?';
49917
- return name.slice(0, 2).toUpperCase();
49918
- });
49919
- this.activeOrgId = computed(() => {
49920
- const u = this.auth.user();
49921
- return u?.activeOrgId ?? u?.activeOrg ?? '';
49922
- });
49923
- this.filteredOrgs = computed(() => {
49924
- const q = this.query().trim().toLowerCase();
49925
- const list = this.orgs();
49926
- if (!q)
49927
- return list;
49928
- return list.filter(o => o.name.toLowerCase().includes(q));
49929
- });
49930
- this.i18n.registerDefaults(DEFAULT_NAMESPACE$e, SWITCH_ORG_MODAL_I18N);
49931
- this.loadOrgs();
49932
- }
49933
- onQueryChange(value) {
49934
- this.query.set(value ?? '');
49935
- }
49936
- async onSelect(org) {
49937
- if (this.switchingId() || org.id === this.activeOrgId())
49938
- return;
49939
- this.switchingId.set(org.id);
49940
- try {
49941
- await this.orgSwitch.switchTo(org.id);
49942
- this.onSuccess?.();
49943
- this.dismiss();
49944
- }
49945
- catch (err) {
49946
- this.errors.handle(err, {
49947
- context: 'account.switchOrg',
49948
- fallbackKey: 'switchError',
49949
- i18nNamespace: this.i18nNamespace,
49950
- });
49951
- this.switchingId.set(null);
49952
- }
49953
- }
49954
- loadOrgs() {
49955
- this.loading.set(true);
49956
- this.orgService
49957
- .listMyOrgs()
49958
- .pipe(take(1))
49959
- .subscribe({
49960
- next: orgs => {
49961
- const activeId = this.activeOrgId();
49962
- this.orgs.set([...orgs].sort((a, b) => (a.id === activeId ? -1 : b.id === activeId ? 1 : 0)));
49963
- this.loading.set(false);
49964
- },
49965
- error: () => this.loading.set(false),
49966
- });
49967
- }
49968
- onProfile() {
49969
- this.dismiss();
49970
- this.onProfileClick?.();
49971
- }
49972
- onCreateNew() {
49973
- this.dismiss();
49974
- this.onCreateOrg?.();
49975
- }
49976
- dismiss() {
49977
- this._modalRef?.dismiss(null, 'cancel');
49978
- }
49979
- t(key) {
49980
- return this.i18n.t(key, this.i18nNamespace);
49981
- }
49982
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SwitchOrgModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49983
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SwitchOrgModalComponent, isStandalone: true, selector: "val-switch-org-modal", inputs: { _modalRef: "_modalRef", onSuccess: "onSuccess", onProfileClick: "onProfileClick", onCreateOrg: "onCreateOrg", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
49984
- <ion-header>
49985
- <ion-toolbar>
49986
- <ion-buttons slot="end">
49987
- <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
49988
- <strong>{{ t('close') }}</strong>
49989
- </ion-button>
49990
- </ion-buttons>
49991
- </ion-toolbar>
49992
- </ion-header>
49993
-
49994
- <ion-content class="ion-padding">
49995
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49996
- <val-title
49997
- [props]="{
49998
- content: t('subtitle'),
49999
- size: 'large',
50000
- color: '',
50001
- bold: false,
50002
- }"
50003
- />
50004
-
50005
- <div class="user-strip">
50006
- <div class="user-strip__avatar">
50007
- @if (user()?.avatarUrl && !userAvatarFailed()) {
50008
- <img [src]="user()!.avatarUrl!" [alt]="user()?.name ?? ''" (error)="userAvatarFailed.set(true)" />
50009
- } @else {
50010
- <span>{{ userInitials() }}</span>
50011
- }
50012
- </div>
50013
- <div class="user-strip__info">
50014
- <span class="user-strip__name">{{ user()?.name || user()?.email || '' }}</span>
50015
- <span class="user-strip__email">{{ user()?.email || '' }}</span>
50016
- </div>
50017
- <button class="user-strip__profile-btn" type="button" (click)="onProfile()">
50018
- {{ t('profile') }}
50019
- </button>
50020
- </div>
50021
-
50022
- <div class="switch-section">
50023
- <val-searchbar
50024
- [props]="{
50025
- placeholder: t('searchPlaceholder'),
50026
- debounce: 200,
50027
- showCancelButton: 'never',
50028
- }"
50029
- (filterEvent)="onQueryChange($event)"
50030
- />
50031
-
50032
- @if (loading()) {
50033
- <div class="switch-state">
50034
- <ion-spinner name="crescent" />
50035
- </div>
50036
- } @else if (filteredOrgs().length === 0) {
50037
- <val-empty-state
50038
- [props]="{
50039
- variant: 'empty',
50040
- title: t('noResults'),
50041
- icon: 'business-outline',
50042
- }"
50043
- />
50044
- } @else {
50045
- <div class="orgs-list">
50046
- @for (org of filteredOrgs(); track org.id) {
50047
- <button
50048
- type="button"
50049
- class="org-row"
50050
- [class.org-row--active]="org.id === activeOrgId()"
50051
- [disabled]="!!switchingId() || org.id === activeOrgId()"
50052
- (click)="onSelect(org)"
50053
- >
50054
- <div class="org-row__icon">
50055
- @if (org.logoUrl && !failedLogos().has(org.id)) {
50056
- <img [src]="org.logoUrl" [alt]="org.name" (error)="onLogoError(org.id)" />
50057
- } @else {
50058
- <ion-icon name="business-outline" />
50059
- }
50060
- </div>
50061
- <div class="org-row__body">
50062
- <span class="org-row__name">{{ org.name }}</span>
50063
- <span class="org-row__type">{{ org.type }}</span>
50064
- </div>
50065
- <div class="org-row__end">
50066
- @if (switchingId() === org.id) {
50067
- <ion-spinner name="crescent" class="org-row__spinner" />
50068
- } @else if (org.id === activeOrgId()) {
50069
- <span class="org-row__current">{{ t('current') }}</span>
50070
- <ion-icon name="checkmark-circle-outline" class="org-row__check" />
50071
- }
50072
- </div>
50073
- </button>
50074
- }
50075
- </div>
50076
- }
50077
- </div>
50078
-
50079
- @if (onCreateOrg) {
50080
- <div class="new-org-cta">
50081
- <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="onCreateNew()">
50082
- <ion-icon slot="start" name="add-outline" />
50083
- {{ t('newOrg') }}
50084
- </ion-button>
50085
- </div>
50086
- }
50087
- </ion-content>
50088
- `, isInline: true, styles: [".switch-section{margin-top:16px;display:flex;flex-direction:column;gap:12px}.switch-state{display:flex;justify-content:center;padding:24px 0}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-row{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border:1px solid rgba(var(--ion-color-dark-rgb),.14);border-radius:12px;background:var(--ion-card-background, var(--ion-background-color));cursor:pointer;text-align:left;transition:border-color .15s ease,opacity .15s ease}.org-row:not(:disabled):active{opacity:.7}.org-row:disabled{cursor:default}.org-row--active{border-color:var(--ion-color-dark)}.org-row__icon{width:36px;height:36px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--ion-color-dark);background:rgba(var(--ion-color-dark-rgb),.08)}.org-row__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-row__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-row__type{font-size:.8rem;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__end{display:flex;align-items:center;gap:6px;flex-shrink:0}.org-row__current{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__check{font-size:1.1rem;color:var(--ion-color-dark)}.org-row__spinner{width:18px;height:18px;color:var(--ion-color-dark)}.org-row__icon img{width:100%;height:100%;object-fit:cover;border-radius:50%}.user-strip{display:flex;align-items:center;gap:12px;padding:12px 0 4px;margin-bottom:8px;border-bottom:1px solid rgba(var(--ion-color-dark-rgb),.1)}.user-strip__avatar{width:44px;height:44px;border-radius:50%;flex-shrink:0;overflow:hidden;background:rgba(var(--ion-color-dark-rgb),.1);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;color:var(--ion-color-dark)}.user-strip__avatar img{width:100%;height:100%;object-fit:cover}.user-strip__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.user-strip__name{font-weight:600;font-size:.9rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__email{font-size:.78rem;color:rgba(var(--ion-color-dark-rgb),.6);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__profile-btn{background:none;border:none;padding:6px 0;font-size:.82rem;font-weight:600;color:var(--ion-color-dark);cursor:pointer;white-space:nowrap;text-decoration:underline;text-underline-offset:3px}.new-org-cta{margin-top:16px;padding-top:16px;border-top:1px solid rgba(var(--ion-color-dark-rgb),.1)}\n"], dependencies: [{ 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
50089
- }
50090
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SwitchOrgModalComponent, decorators: [{
50091
- type: Component,
50092
- args: [{ selector: 'val-switch-org-modal', standalone: true, imports: [
50093
- IonHeader,
50094
- IonToolbar,
50095
- IonButtons,
50096
- IonButton,
50097
- IonContent,
50098
- IonSpinner,
50099
- IonIcon,
50100
- DisplayComponent,
50101
- EmptyStateComponent,
50102
- SearchbarComponent,
50103
- TitleComponent,
50104
- ], template: `
50105
- <ion-header>
50106
- <ion-toolbar>
50107
- <ion-buttons slot="end">
50108
- <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
50109
- <strong>{{ t('close') }}</strong>
50110
- </ion-button>
50111
- </ion-buttons>
50112
- </ion-toolbar>
50113
- </ion-header>
50114
-
50115
- <ion-content class="ion-padding">
50116
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
50117
- <val-title
50118
- [props]="{
50119
- content: t('subtitle'),
50120
- size: 'large',
50121
- color: '',
50122
- bold: false,
50123
- }"
50124
- />
50125
-
50126
- <div class="user-strip">
50127
- <div class="user-strip__avatar">
50128
- @if (user()?.avatarUrl && !userAvatarFailed()) {
50129
- <img [src]="user()!.avatarUrl!" [alt]="user()?.name ?? ''" (error)="userAvatarFailed.set(true)" />
50130
- } @else {
50131
- <span>{{ userInitials() }}</span>
50132
- }
50133
- </div>
50134
- <div class="user-strip__info">
50135
- <span class="user-strip__name">{{ user()?.name || user()?.email || '' }}</span>
50136
- <span class="user-strip__email">{{ user()?.email || '' }}</span>
50137
- </div>
50138
- <button class="user-strip__profile-btn" type="button" (click)="onProfile()">
50139
- {{ t('profile') }}
50140
- </button>
50141
- </div>
50142
-
50143
- <div class="switch-section">
50144
- <val-searchbar
50145
- [props]="{
50146
- placeholder: t('searchPlaceholder'),
50147
- debounce: 200,
50148
- showCancelButton: 'never',
50149
- }"
50150
- (filterEvent)="onQueryChange($event)"
50151
- />
50152
-
50153
- @if (loading()) {
50154
- <div class="switch-state">
50155
- <ion-spinner name="crescent" />
50156
- </div>
50157
- } @else if (filteredOrgs().length === 0) {
50158
- <val-empty-state
50159
- [props]="{
50160
- variant: 'empty',
50161
- title: t('noResults'),
50162
- icon: 'business-outline',
50163
- }"
50164
- />
50165
- } @else {
50166
- <div class="orgs-list">
50167
- @for (org of filteredOrgs(); track org.id) {
50168
- <button
50169
- type="button"
50170
- class="org-row"
50171
- [class.org-row--active]="org.id === activeOrgId()"
50172
- [disabled]="!!switchingId() || org.id === activeOrgId()"
50173
- (click)="onSelect(org)"
50174
- >
50175
- <div class="org-row__icon">
50176
- @if (org.logoUrl && !failedLogos().has(org.id)) {
50177
- <img [src]="org.logoUrl" [alt]="org.name" (error)="onLogoError(org.id)" />
50178
- } @else {
50179
- <ion-icon name="business-outline" />
50180
- }
50181
- </div>
50182
- <div class="org-row__body">
50183
- <span class="org-row__name">{{ org.name }}</span>
50184
- <span class="org-row__type">{{ org.type }}</span>
50185
- </div>
50186
- <div class="org-row__end">
50187
- @if (switchingId() === org.id) {
50188
- <ion-spinner name="crescent" class="org-row__spinner" />
50189
- } @else if (org.id === activeOrgId()) {
50190
- <span class="org-row__current">{{ t('current') }}</span>
50191
- <ion-icon name="checkmark-circle-outline" class="org-row__check" />
50192
- }
50193
- </div>
50194
- </button>
50195
- }
50196
- </div>
50197
- }
50198
- </div>
50199
-
50200
- @if (onCreateOrg) {
50201
- <div class="new-org-cta">
50202
- <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="onCreateNew()">
50203
- <ion-icon slot="start" name="add-outline" />
50204
- {{ t('newOrg') }}
50205
- </ion-button>
50206
- </div>
50207
- }
50208
- </ion-content>
50209
- `, styles: [".switch-section{margin-top:16px;display:flex;flex-direction:column;gap:12px}.switch-state{display:flex;justify-content:center;padding:24px 0}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-row{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border:1px solid rgba(var(--ion-color-dark-rgb),.14);border-radius:12px;background:var(--ion-card-background, var(--ion-background-color));cursor:pointer;text-align:left;transition:border-color .15s ease,opacity .15s ease}.org-row:not(:disabled):active{opacity:.7}.org-row:disabled{cursor:default}.org-row--active{border-color:var(--ion-color-dark)}.org-row__icon{width:36px;height:36px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--ion-color-dark);background:rgba(var(--ion-color-dark-rgb),.08)}.org-row__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-row__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-row__type{font-size:.8rem;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__end{display:flex;align-items:center;gap:6px;flex-shrink:0}.org-row__current{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__check{font-size:1.1rem;color:var(--ion-color-dark)}.org-row__spinner{width:18px;height:18px;color:var(--ion-color-dark)}.org-row__icon img{width:100%;height:100%;object-fit:cover;border-radius:50%}.user-strip{display:flex;align-items:center;gap:12px;padding:12px 0 4px;margin-bottom:8px;border-bottom:1px solid rgba(var(--ion-color-dark-rgb),.1)}.user-strip__avatar{width:44px;height:44px;border-radius:50%;flex-shrink:0;overflow:hidden;background:rgba(var(--ion-color-dark-rgb),.1);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;color:var(--ion-color-dark)}.user-strip__avatar img{width:100%;height:100%;object-fit:cover}.user-strip__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.user-strip__name{font-weight:600;font-size:.9rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__email{font-size:.78rem;color:rgba(var(--ion-color-dark-rgb),.6);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__profile-btn{background:none;border:none;padding:6px 0;font-size:.82rem;font-weight:600;color:var(--ion-color-dark);cursor:pointer;white-space:nowrap;text-decoration:underline;text-underline-offset:3px}.new-org-cta{margin-top:16px;padding-top:16px;border-top:1px solid rgba(var(--ion-color-dark-rgb),.1)}\n"] }]
50210
- }], ctorParameters: () => [], propDecorators: { _modalRef: [{
50211
- type: Input
50212
- }], onSuccess: [{
50213
- type: Input
50214
- }], onProfileClick: [{
50215
- type: Input
50216
- }], onCreateOrg: [{
50217
- type: Input
50218
- }], i18nNamespace: [{
50219
- type: Input
50220
- }] } });
50221
-
50222
49811
  /**
50223
49812
  * Defaults i18n (es/en) embebidos en `val-account-view`. Auto-registrados en el
50224
49813
  * constructor del componente si el consumer no proveyó el namespace
@@ -50320,7 +49909,7 @@ const ACCOUNT_VIEW_I18N = {
50320
49909
  };
50321
49910
 
50322
49911
  addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline, warningOutline });
50323
- const DEFAULT_NAMESPACE$d = 'Settings.Account';
49912
+ const DEFAULT_NAMESPACE$e = 'Settings.Account';
50324
49913
  const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
50325
49914
  /**
50326
49915
  * `val-account-view` — vista Cuenta full-feature autocontenida (organism).
@@ -50361,7 +49950,7 @@ class AccountViewComponent {
50361
49950
  showNewOrgCta: merged.showNewOrgCta ?? false,
50362
49951
  showLogout: merged.showLogout ?? true,
50363
49952
  showDeleteAccount: merged.showDeleteAccount ?? true,
50364
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
49953
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
50365
49954
  manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
50366
49955
  onOrgSwitched: merged.onOrgSwitched,
50367
49956
  onOrgCreated: merged.onOrgCreated,
@@ -50459,15 +50048,6 @@ class AccountViewComponent {
50459
50048
  type: 'button',
50460
50049
  token: 'account-delete',
50461
50050
  }));
50462
- this.switchAccountButtonProps = computed(() => ({
50463
- text: this.tt('switchAccountCta'),
50464
- color: 'dark',
50465
- fill: 'outline',
50466
- size: 'default',
50467
- shape: 'round',
50468
- type: 'button',
50469
- token: 'account-switch',
50470
- }));
50471
50051
  const ns = this.ns;
50472
50052
  this.i18n.registerDefaults(ns, ACCOUNT_VIEW_I18N);
50473
50053
  this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
@@ -50486,25 +50066,6 @@ class AccountViewComponent {
50486
50066
  };
50487
50067
  return map[type] ?? type;
50488
50068
  }
50489
- onSwitchAccount() {
50490
- void this.modalService.open({
50491
- component: SwitchOrgModalComponent,
50492
- componentProps: {
50493
- i18nNamespace: 'Settings.SwitchOrg',
50494
- onSuccess: () => {
50495
- this.loadOrgs();
50496
- this.loadPendingInvites();
50497
- this.resolvedConfig().onOrgSwitched?.();
50498
- },
50499
- },
50500
- breakpoints: {
50501
- breakpoints: [0, 0.85],
50502
- initialBreakpoint: 0.85,
50503
- showHandle: true,
50504
- },
50505
- backdropDismiss: true,
50506
- });
50507
- }
50508
50069
  onDeleteAccount() {
50509
50070
  this.deleteAccountOpen.set(true);
50510
50071
  this.resolvedConfig().onAccountDeleted?.();
@@ -50691,11 +50252,6 @@ class AccountViewComponent {
50691
50252
  content: orgsActiveTitle(),
50692
50253
  }"
50693
50254
  />
50694
- @if (orgs().length > 1) {
50695
- <ion-button fill="outline" color="dark" shape="round" size="small" (click)="onSwitchAccount()">
50696
- {{ tt('orgsSwitch') }}
50697
- </ion-button>
50698
- }
50699
50255
  </div>
50700
50256
  <div class="section-body">
50701
50257
  @if (orgsLoading()) {
@@ -50895,11 +50451,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50895
50451
  content: orgsActiveTitle(),
50896
50452
  }"
50897
50453
  />
50898
- @if (orgs().length > 1) {
50899
- <ion-button fill="outline" color="dark" shape="round" size="small" (click)="onSwitchAccount()">
50900
- {{ tt('orgsSwitch') }}
50901
- </ion-button>
50902
- }
50903
50454
  </div>
50904
50455
  <div class="section-body">
50905
50456
  @if (orgsLoading()) {
@@ -51173,7 +50724,7 @@ const EDIT_ORG_MODAL_I18N = {
51173
50724
  },
51174
50725
  };
51175
50726
 
51176
- const DEFAULT_NAMESPACE$c = 'EditOrgModal';
50727
+ const DEFAULT_NAMESPACE$d = 'EditOrgModal';
51177
50728
  /**
51178
50729
  * `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
51179
50730
  * Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
@@ -51200,7 +50751,7 @@ class EditOrgModalComponent {
51200
50751
  /** Organización a editar (pasada por `componentProps`). */
51201
50752
  this.org = null;
51202
50753
  /** Namespace i18n con que la vista resuelve sus textos. */
51203
- this.i18nNamespace = DEFAULT_NAMESPACE$c;
50754
+ this.i18nNamespace = DEFAULT_NAMESPACE$d;
51204
50755
  this.saving = signal(false);
51205
50756
  this.logoUrl = signal(null);
51206
50757
  this.removingLogo = signal(false);
@@ -51208,7 +50759,7 @@ class EditOrgModalComponent {
51208
50759
  const name = this.org?.name ?? '';
51209
50760
  return name.slice(0, 2).toUpperCase() || '?';
51210
50761
  });
51211
- this.i18n.registerDefaults(DEFAULT_NAMESPACE$c, EDIT_ORG_MODAL_I18N);
50762
+ this.i18n.registerDefaults(DEFAULT_NAMESPACE$d, EDIT_ORG_MODAL_I18N);
51212
50763
  this.formMeta = signal(this.buildFormMeta());
51213
50764
  effect(() => {
51214
50765
  this.i18n.lang();
@@ -51532,7 +51083,7 @@ const INVITE_MEMBER_MODAL_I18N = {
51532
51083
  },
51533
51084
  };
51534
51085
 
51535
- const DEFAULT_NAMESPACE$b = 'InviteModal';
51086
+ const DEFAULT_NAMESPACE$c = 'InviteModal';
51536
51087
  addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
51537
51088
  /**
51538
51089
  * `val-invite-member-modal` — invitación de miembros a una organización.
@@ -51563,7 +51114,7 @@ class InviteMemberModalComponent {
51563
51114
  /** Ids de miembros ya existentes (excluidos de los resultados). */
51564
51115
  this.existingMemberIds = [];
51565
51116
  /** Namespace i18n con que la vista resuelve sus textos. */
51566
- this.i18nNamespace = DEFAULT_NAMESPACE$b;
51117
+ this.i18nNamespace = DEFAULT_NAMESPACE$c;
51567
51118
  this.query = signal('');
51568
51119
  this.searching = signal(false);
51569
51120
  this.searchResults = signal([]);
@@ -51608,7 +51159,7 @@ class InviteMemberModalComponent {
51608
51159
  state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
51609
51160
  }));
51610
51161
  this.searchSubject = new Subject();
51611
- this.i18n.registerDefaults(DEFAULT_NAMESPACE$b, INVITE_MEMBER_MODAL_I18N);
51162
+ this.i18n.registerDefaults(DEFAULT_NAMESPACE$c, INVITE_MEMBER_MODAL_I18N);
51612
51163
  this.roleSub = this.roleControl.valueChanges.subscribe(v => {
51613
51164
  if (v)
51614
51165
  this.selectedRole.set(v);
@@ -52301,7 +51852,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
52301
51852
  },
52302
51853
  };
52303
51854
 
52304
- const DEFAULT_NAMESPACE$a = 'Settings.MemberDetail';
51855
+ const DEFAULT_NAMESPACE$b = 'Settings.MemberDetail';
52305
51856
  /**
52306
51857
  * `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
52307
51858
  * permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -52351,7 +51902,7 @@ class MemberDetailModalComponent {
52351
51902
  { id: 'admin', name: 'admin' },
52352
51903
  ];
52353
51904
  /** Namespace i18n con que la vista resuelve sus textos. */
52354
- this.i18nNamespace = DEFAULT_NAMESPACE$a;
51905
+ this.i18nNamespace = DEFAULT_NAMESPACE$b;
52355
51906
  this.working = signal(false);
52356
51907
  this.currentRole = signal('');
52357
51908
  this.roleControl = new FormControl('');
@@ -52417,7 +51968,7 @@ class MemberDetailModalComponent {
52417
51968
  type: 'button',
52418
51969
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
52419
51970
  }));
52420
- this.i18n.registerDefaults(DEFAULT_NAMESPACE$a, MEMBER_DETAIL_MODAL_I18N);
51971
+ this.i18n.registerDefaults(DEFAULT_NAMESPACE$b, MEMBER_DETAIL_MODAL_I18N);
52421
51972
  }
52422
51973
  ngOnInit() {
52423
51974
  this.i18n.registerDefaults(this.i18nNamespace, MEMBER_DETAIL_MODAL_I18N);
@@ -52876,7 +52427,7 @@ const PERMISSIONS_VIEW_I18N = {
52876
52427
  },
52877
52428
  };
52878
52429
 
52879
- const DEFAULT_NAMESPACE$9 = 'Settings.Permissions';
52430
+ const DEFAULT_NAMESPACE$a = 'Settings.Permissions';
52880
52431
  /**
52881
52432
  * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
52882
52433
  * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
@@ -52921,7 +52472,7 @@ class PermissionsViewComponent {
52921
52472
  return {
52922
52473
  showSystemRoles: merged.showSystemRoles ?? true,
52923
52474
  showCustomRoles: merged.showCustomRoles ?? true,
52924
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$9,
52475
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$a,
52925
52476
  };
52926
52477
  });
52927
52478
  this.loading = signal(false);
@@ -53425,7 +52976,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
53425
52976
  },
53426
52977
  };
53427
52978
 
53428
- const DEFAULT_NAMESPACE$8 = 'Settings.TransferModal';
52979
+ const DEFAULT_NAMESPACE$9 = 'Settings.TransferModal';
53429
52980
  /**
53430
52981
  * `val-transfer-ownership-modal` — selector de miembro para transferir la
53431
52982
  * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
@@ -53449,7 +53000,7 @@ class TransferOwnershipModalComponent {
53449
53000
  /** Lista de miembros transferibles (todos excepto el owner actual). */
53450
53001
  this.members = [];
53451
53002
  /** Namespace i18n con que la vista resuelve sus textos. */
53452
- this.i18nNamespace = DEFAULT_NAMESPACE$8;
53003
+ this.i18nNamespace = DEFAULT_NAMESPACE$9;
53453
53004
  this.query = signal('');
53454
53005
  this.selectedId = signal('');
53455
53006
  this.filtered = computed(() => {
@@ -53459,7 +53010,7 @@ class TransferOwnershipModalComponent {
53459
53010
  return this.members.filter(m => m.name?.toLowerCase().includes(q) || m.email?.toLowerCase().includes(q) || m.userId.toLowerCase().includes(q));
53460
53011
  });
53461
53012
  this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
53462
- this.i18n.registerDefaults(DEFAULT_NAMESPACE$8, TRANSFER_OWNERSHIP_MODAL_I18N);
53013
+ this.i18n.registerDefaults(DEFAULT_NAMESPACE$9, TRANSFER_OWNERSHIP_MODAL_I18N);
53463
53014
  }
53464
53015
  ngOnInit() {
53465
53016
  this.i18n.registerDefaults(this.i18nNamespace, TRANSFER_OWNERSHIP_MODAL_I18N);
@@ -53732,7 +53283,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
53732
53283
  },
53733
53284
  };
53734
53285
 
53735
- const DEFAULT_NAMESPACE$7 = 'Settings.ImportModal';
53286
+ const DEFAULT_NAMESPACE$8 = 'Settings.ImportModal';
53736
53287
  addIcons({ cloudUploadOutline, documentTextOutline });
53737
53288
  /**
53738
53289
  * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
@@ -53752,7 +53303,7 @@ class MemberImportModalComponent {
53752
53303
  /** Nombres de roles disponibles (para el hint). Opcional. */
53753
53304
  this.availableRoles = [];
53754
53305
  /** Namespace i18n. */
53755
- this.i18nNamespace = DEFAULT_NAMESPACE$7;
53306
+ this.i18nNamespace = DEFAULT_NAMESPACE$8;
53756
53307
  this.csv = '';
53757
53308
  this.importMode = signal('file');
53758
53309
  this.onConflictControl = new FormControl('assignRole');
@@ -53794,7 +53345,7 @@ class MemberImportModalComponent {
53794
53345
  return this.t('summary');
53795
53346
  return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
53796
53347
  });
53797
- this.i18n.registerDefaults(DEFAULT_NAMESPACE$7, MEMBER_IMPORT_MODAL_I18N);
53348
+ this.i18n.registerDefaults(DEFAULT_NAMESPACE$8, MEMBER_IMPORT_MODAL_I18N);
53798
53349
  }
53799
53350
  ngOnInit() {
53800
53351
  this.i18n.registerDefaults(this.i18nNamespace, MEMBER_IMPORT_MODAL_I18N);
@@ -54195,6 +53746,384 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
54195
53746
  type: Input
54196
53747
  }] } });
54197
53748
 
53749
+ /**
53750
+ * Defaults i18n (es/en) embebidos en `val-switch-org-modal`. Auto-registrados en
53751
+ * el constructor del componente si el consumer no proveyó el namespace
53752
+ * (`Settings.SwitchOrg` por default). SOLO incluye las keys que el modal usa.
53753
+ */
53754
+ const SWITCH_ORG_MODAL_I18N = {
53755
+ es: {
53756
+ title: 'Cambiar de cuenta',
53757
+ subtitle: 'Selecciona la organización a la que quieres cambiar.',
53758
+ searchPlaceholder: 'Buscar organización...',
53759
+ noResults: 'No se encontraron organizaciones.',
53760
+ current: 'Actual',
53761
+ close: 'Cerrar',
53762
+ switchError: 'No se pudo cambiar de organización.',
53763
+ profile: 'Mi perfil',
53764
+ newOrg: 'Nueva organización',
53765
+ },
53766
+ en: {
53767
+ title: 'Switch account',
53768
+ subtitle: 'Select the organization you want to switch to.',
53769
+ searchPlaceholder: 'Search organization...',
53770
+ noResults: 'No organizations found.',
53771
+ current: 'Active',
53772
+ close: 'Close',
53773
+ switchError: 'Could not switch organization.',
53774
+ profile: 'My profile',
53775
+ newOrg: 'New organization',
53776
+ },
53777
+ };
53778
+
53779
+ const DEFAULT_NAMESPACE$7 = 'Settings.SwitchOrg';
53780
+ addIcons({ addOutline, businessOutline, checkmarkCircleOutline });
53781
+ /**
53782
+ * `val-switch-org-modal` — modal de cambio de organización activa (organism
53783
+ * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
53784
+ * la vista Cuenta (y futuras vistas org-aware).
53785
+ *
53786
+ * Lista las organizaciones del user (vía `OrgService.listMyOrgs`), permite buscar
53787
+ * por nombre, y cambia la org activa con `OrgSwitchService.switchTo`. La org
53788
+ * actual queda primera y deshabilitada.
53789
+ *
53790
+ * Abierto vía `ModalService` — recibe `_modalRef` por `componentProps` + callback
53791
+ * `onSuccess`. Header canónico (Regla #5): sin `ion-title`, botón Cerrar texto en
53792
+ * `slot=end`, título/subtítulo en el body con `val-display`/`val-title`.
53793
+ *
53794
+ * Auto-registra sus defaults i18n (es/en) si el consumer no proveyó el namespace
53795
+ * `Settings.SwitchOrg`.
53796
+ */
53797
+ class SwitchOrgModalComponent {
53798
+ onLogoError(orgId) {
53799
+ const next = new Set(this.failedLogos());
53800
+ next.add(orgId);
53801
+ this.failedLogos.set(next);
53802
+ }
53803
+ constructor() {
53804
+ this.i18n = inject(I18nService);
53805
+ this.auth = inject(AuthService);
53806
+ this.orgService = inject(OrgService);
53807
+ this.orgSwitch = inject(OrgSwitchService);
53808
+ this.errors = inject(ValtechErrorService);
53809
+ /** Namespace i18n con que la vista resuelve sus textos. */
53810
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
53811
+ this.orgs = signal([]);
53812
+ this.loading = signal(false);
53813
+ this.query = signal('');
53814
+ this.switchingId = signal(null);
53815
+ this.user = computed(() => this.auth.user());
53816
+ /** Avatar de usuario que falló al cargar → cae a iniciales. */
53817
+ this.userAvatarFailed = signal(false);
53818
+ /** IDs de orgs cuyo logo falló al cargar → caen al ícono business-outline. */
53819
+ this.failedLogos = signal(new Set());
53820
+ this.userInitials = computed(() => {
53821
+ const name = this.user()?.name ?? this.user()?.email ?? '?';
53822
+ return name.slice(0, 2).toUpperCase();
53823
+ });
53824
+ this.activeOrgId = computed(() => {
53825
+ const u = this.auth.user();
53826
+ return u?.activeOrgId ?? u?.activeOrg ?? '';
53827
+ });
53828
+ this.filteredOrgs = computed(() => {
53829
+ const q = this.query().trim().toLowerCase();
53830
+ const list = this.orgs();
53831
+ if (!q)
53832
+ return list;
53833
+ return list.filter(o => o.name.toLowerCase().includes(q));
53834
+ });
53835
+ this.i18n.registerDefaults(DEFAULT_NAMESPACE$7, SWITCH_ORG_MODAL_I18N);
53836
+ this.loadOrgs();
53837
+ }
53838
+ onQueryChange(value) {
53839
+ this.query.set(value ?? '');
53840
+ }
53841
+ async onSelect(org) {
53842
+ if (this.switchingId() || org.id === this.activeOrgId())
53843
+ return;
53844
+ this.switchingId.set(org.id);
53845
+ try {
53846
+ await this.orgSwitch.switchTo(org.id);
53847
+ this.onSuccess?.();
53848
+ this.dismiss();
53849
+ }
53850
+ catch (err) {
53851
+ this.errors.handle(err, {
53852
+ context: 'account.switchOrg',
53853
+ fallbackKey: 'switchError',
53854
+ i18nNamespace: this.i18nNamespace,
53855
+ });
53856
+ this.switchingId.set(null);
53857
+ }
53858
+ }
53859
+ loadOrgs() {
53860
+ this.loading.set(true);
53861
+ this.orgService
53862
+ .listMyOrgs()
53863
+ .pipe(take(1))
53864
+ .subscribe({
53865
+ next: orgs => {
53866
+ const activeId = this.activeOrgId();
53867
+ this.orgs.set([...orgs].sort((a, b) => (a.id === activeId ? -1 : b.id === activeId ? 1 : 0)));
53868
+ this.loading.set(false);
53869
+ },
53870
+ error: () => this.loading.set(false),
53871
+ });
53872
+ }
53873
+ onProfile() {
53874
+ this.dismiss();
53875
+ this.onProfileClick?.();
53876
+ }
53877
+ onCreateNew() {
53878
+ this.dismiss();
53879
+ this.onCreateOrg?.();
53880
+ }
53881
+ dismiss() {
53882
+ this._modalRef?.dismiss(null, 'cancel');
53883
+ }
53884
+ t(key) {
53885
+ return this.i18n.t(key, this.i18nNamespace);
53886
+ }
53887
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SwitchOrgModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53888
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SwitchOrgModalComponent, isStandalone: true, selector: "val-switch-org-modal", inputs: { _modalRef: "_modalRef", onSuccess: "onSuccess", onProfileClick: "onProfileClick", onCreateOrg: "onCreateOrg", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
53889
+ <ion-header>
53890
+ <ion-toolbar>
53891
+ <ion-buttons slot="end">
53892
+ <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
53893
+ <strong>{{ t('close') }}</strong>
53894
+ </ion-button>
53895
+ </ion-buttons>
53896
+ </ion-toolbar>
53897
+ </ion-header>
53898
+
53899
+ <ion-content class="ion-padding">
53900
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
53901
+ <val-title
53902
+ [props]="{
53903
+ content: t('subtitle'),
53904
+ size: 'large',
53905
+ color: '',
53906
+ bold: false,
53907
+ }"
53908
+ />
53909
+
53910
+ <div class="user-strip">
53911
+ <div class="user-strip__avatar">
53912
+ @if (user()?.avatarUrl && !userAvatarFailed()) {
53913
+ <img [src]="user()!.avatarUrl!" [alt]="user()?.name ?? ''" (error)="userAvatarFailed.set(true)" />
53914
+ } @else {
53915
+ <span>{{ userInitials() }}</span>
53916
+ }
53917
+ </div>
53918
+ <div class="user-strip__info">
53919
+ <span class="user-strip__name">{{ user()?.name || user()?.email || '' }}</span>
53920
+ <span class="user-strip__email">{{ user()?.email || '' }}</span>
53921
+ </div>
53922
+ <button class="user-strip__profile-btn" type="button" (click)="onProfile()">
53923
+ {{ t('profile') }}
53924
+ </button>
53925
+ </div>
53926
+
53927
+ <div class="switch-section">
53928
+ <val-searchbar
53929
+ [props]="{
53930
+ placeholder: t('searchPlaceholder'),
53931
+ debounce: 200,
53932
+ showCancelButton: 'never',
53933
+ }"
53934
+ (filterEvent)="onQueryChange($event)"
53935
+ />
53936
+
53937
+ @if (loading()) {
53938
+ <div class="switch-state">
53939
+ <ion-spinner name="crescent" />
53940
+ </div>
53941
+ } @else if (filteredOrgs().length === 0) {
53942
+ <val-empty-state
53943
+ [props]="{
53944
+ variant: 'empty',
53945
+ title: t('noResults'),
53946
+ icon: 'business-outline',
53947
+ }"
53948
+ />
53949
+ } @else {
53950
+ <div class="orgs-list">
53951
+ @for (org of filteredOrgs(); track org.id) {
53952
+ <button
53953
+ type="button"
53954
+ class="org-row"
53955
+ [class.org-row--active]="org.id === activeOrgId()"
53956
+ [disabled]="!!switchingId() || org.id === activeOrgId()"
53957
+ (click)="onSelect(org)"
53958
+ >
53959
+ <div class="org-row__icon">
53960
+ @if (org.logoUrl && !failedLogos().has(org.id)) {
53961
+ <img [src]="org.logoUrl" [alt]="org.name" (error)="onLogoError(org.id)" />
53962
+ } @else {
53963
+ <ion-icon name="business-outline" />
53964
+ }
53965
+ </div>
53966
+ <div class="org-row__body">
53967
+ <span class="org-row__name">{{ org.name }}</span>
53968
+ <span class="org-row__type">{{ org.type }}</span>
53969
+ </div>
53970
+ <div class="org-row__end">
53971
+ @if (switchingId() === org.id) {
53972
+ <ion-spinner name="crescent" class="org-row__spinner" />
53973
+ } @else if (org.id === activeOrgId()) {
53974
+ <span class="org-row__current">{{ t('current') }}</span>
53975
+ <ion-icon name="checkmark-circle-outline" class="org-row__check" />
53976
+ }
53977
+ </div>
53978
+ </button>
53979
+ }
53980
+ </div>
53981
+ }
53982
+ </div>
53983
+
53984
+ @if (onCreateOrg) {
53985
+ <div class="new-org-cta">
53986
+ <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="onCreateNew()">
53987
+ <ion-icon slot="start" name="add-outline" />
53988
+ {{ t('newOrg') }}
53989
+ </ion-button>
53990
+ </div>
53991
+ }
53992
+ </ion-content>
53993
+ `, isInline: true, styles: [".switch-section{margin-top:16px;display:flex;flex-direction:column;gap:12px}.switch-state{display:flex;justify-content:center;padding:24px 0}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-row{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border:1px solid rgba(var(--ion-color-dark-rgb),.14);border-radius:12px;background:var(--ion-card-background, var(--ion-background-color));cursor:pointer;text-align:left;transition:border-color .15s ease,opacity .15s ease}.org-row:not(:disabled):active{opacity:.7}.org-row:disabled{cursor:default}.org-row--active{border-color:var(--ion-color-dark)}.org-row__icon{width:36px;height:36px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--ion-color-dark);background:rgba(var(--ion-color-dark-rgb),.08)}.org-row__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-row__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-row__type{font-size:.8rem;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__end{display:flex;align-items:center;gap:6px;flex-shrink:0}.org-row__current{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__check{font-size:1.1rem;color:var(--ion-color-dark)}.org-row__spinner{width:18px;height:18px;color:var(--ion-color-dark)}.org-row__icon img{width:100%;height:100%;object-fit:cover;border-radius:50%}.user-strip{display:flex;align-items:center;gap:12px;padding:12px 0 4px;margin-bottom:8px;border-bottom:1px solid rgba(var(--ion-color-dark-rgb),.1)}.user-strip__avatar{width:44px;height:44px;border-radius:50%;flex-shrink:0;overflow:hidden;background:rgba(var(--ion-color-dark-rgb),.1);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;color:var(--ion-color-dark)}.user-strip__avatar img{width:100%;height:100%;object-fit:cover}.user-strip__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.user-strip__name{font-weight:600;font-size:.9rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__email{font-size:.78rem;color:rgba(var(--ion-color-dark-rgb),.6);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__profile-btn{background:none;border:none;padding:6px 0;font-size:.82rem;font-weight:600;color:var(--ion-color-dark);cursor:pointer;white-space:nowrap;text-decoration:underline;text-underline-offset:3px}.new-org-cta{margin-top:16px;padding-top:16px;border-top:1px solid rgba(var(--ion-color-dark-rgb),.1)}\n"], dependencies: [{ 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
53994
+ }
53995
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SwitchOrgModalComponent, decorators: [{
53996
+ type: Component,
53997
+ args: [{ selector: 'val-switch-org-modal', standalone: true, imports: [
53998
+ IonHeader,
53999
+ IonToolbar,
54000
+ IonButtons,
54001
+ IonButton,
54002
+ IonContent,
54003
+ IonSpinner,
54004
+ IonIcon,
54005
+ DisplayComponent,
54006
+ EmptyStateComponent,
54007
+ SearchbarComponent,
54008
+ TitleComponent,
54009
+ ], template: `
54010
+ <ion-header>
54011
+ <ion-toolbar>
54012
+ <ion-buttons slot="end">
54013
+ <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
54014
+ <strong>{{ t('close') }}</strong>
54015
+ </ion-button>
54016
+ </ion-buttons>
54017
+ </ion-toolbar>
54018
+ </ion-header>
54019
+
54020
+ <ion-content class="ion-padding">
54021
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
54022
+ <val-title
54023
+ [props]="{
54024
+ content: t('subtitle'),
54025
+ size: 'large',
54026
+ color: '',
54027
+ bold: false,
54028
+ }"
54029
+ />
54030
+
54031
+ <div class="user-strip">
54032
+ <div class="user-strip__avatar">
54033
+ @if (user()?.avatarUrl && !userAvatarFailed()) {
54034
+ <img [src]="user()!.avatarUrl!" [alt]="user()?.name ?? ''" (error)="userAvatarFailed.set(true)" />
54035
+ } @else {
54036
+ <span>{{ userInitials() }}</span>
54037
+ }
54038
+ </div>
54039
+ <div class="user-strip__info">
54040
+ <span class="user-strip__name">{{ user()?.name || user()?.email || '' }}</span>
54041
+ <span class="user-strip__email">{{ user()?.email || '' }}</span>
54042
+ </div>
54043
+ <button class="user-strip__profile-btn" type="button" (click)="onProfile()">
54044
+ {{ t('profile') }}
54045
+ </button>
54046
+ </div>
54047
+
54048
+ <div class="switch-section">
54049
+ <val-searchbar
54050
+ [props]="{
54051
+ placeholder: t('searchPlaceholder'),
54052
+ debounce: 200,
54053
+ showCancelButton: 'never',
54054
+ }"
54055
+ (filterEvent)="onQueryChange($event)"
54056
+ />
54057
+
54058
+ @if (loading()) {
54059
+ <div class="switch-state">
54060
+ <ion-spinner name="crescent" />
54061
+ </div>
54062
+ } @else if (filteredOrgs().length === 0) {
54063
+ <val-empty-state
54064
+ [props]="{
54065
+ variant: 'empty',
54066
+ title: t('noResults'),
54067
+ icon: 'business-outline',
54068
+ }"
54069
+ />
54070
+ } @else {
54071
+ <div class="orgs-list">
54072
+ @for (org of filteredOrgs(); track org.id) {
54073
+ <button
54074
+ type="button"
54075
+ class="org-row"
54076
+ [class.org-row--active]="org.id === activeOrgId()"
54077
+ [disabled]="!!switchingId() || org.id === activeOrgId()"
54078
+ (click)="onSelect(org)"
54079
+ >
54080
+ <div class="org-row__icon">
54081
+ @if (org.logoUrl && !failedLogos().has(org.id)) {
54082
+ <img [src]="org.logoUrl" [alt]="org.name" (error)="onLogoError(org.id)" />
54083
+ } @else {
54084
+ <ion-icon name="business-outline" />
54085
+ }
54086
+ </div>
54087
+ <div class="org-row__body">
54088
+ <span class="org-row__name">{{ org.name }}</span>
54089
+ <span class="org-row__type">{{ org.type }}</span>
54090
+ </div>
54091
+ <div class="org-row__end">
54092
+ @if (switchingId() === org.id) {
54093
+ <ion-spinner name="crescent" class="org-row__spinner" />
54094
+ } @else if (org.id === activeOrgId()) {
54095
+ <span class="org-row__current">{{ t('current') }}</span>
54096
+ <ion-icon name="checkmark-circle-outline" class="org-row__check" />
54097
+ }
54098
+ </div>
54099
+ </button>
54100
+ }
54101
+ </div>
54102
+ }
54103
+ </div>
54104
+
54105
+ @if (onCreateOrg) {
54106
+ <div class="new-org-cta">
54107
+ <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="onCreateNew()">
54108
+ <ion-icon slot="start" name="add-outline" />
54109
+ {{ t('newOrg') }}
54110
+ </ion-button>
54111
+ </div>
54112
+ }
54113
+ </ion-content>
54114
+ `, styles: [".switch-section{margin-top:16px;display:flex;flex-direction:column;gap:12px}.switch-state{display:flex;justify-content:center;padding:24px 0}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-row{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border:1px solid rgba(var(--ion-color-dark-rgb),.14);border-radius:12px;background:var(--ion-card-background, var(--ion-background-color));cursor:pointer;text-align:left;transition:border-color .15s ease,opacity .15s ease}.org-row:not(:disabled):active{opacity:.7}.org-row:disabled{cursor:default}.org-row--active{border-color:var(--ion-color-dark)}.org-row__icon{width:36px;height:36px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--ion-color-dark);background:rgba(var(--ion-color-dark-rgb),.08)}.org-row__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-row__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-row__type{font-size:.8rem;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__end{display:flex;align-items:center;gap:6px;flex-shrink:0}.org-row__current{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:rgba(var(--ion-color-dark-rgb),.7)}.org-row__check{font-size:1.1rem;color:var(--ion-color-dark)}.org-row__spinner{width:18px;height:18px;color:var(--ion-color-dark)}.org-row__icon img{width:100%;height:100%;object-fit:cover;border-radius:50%}.user-strip{display:flex;align-items:center;gap:12px;padding:12px 0 4px;margin-bottom:8px;border-bottom:1px solid rgba(var(--ion-color-dark-rgb),.1)}.user-strip__avatar{width:44px;height:44px;border-radius:50%;flex-shrink:0;overflow:hidden;background:rgba(var(--ion-color-dark-rgb),.1);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;color:var(--ion-color-dark)}.user-strip__avatar img{width:100%;height:100%;object-fit:cover}.user-strip__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.user-strip__name{font-weight:600;font-size:.9rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__email{font-size:.78rem;color:rgba(var(--ion-color-dark-rgb),.6);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-strip__profile-btn{background:none;border:none;padding:6px 0;font-size:.82rem;font-weight:600;color:var(--ion-color-dark);cursor:pointer;white-space:nowrap;text-decoration:underline;text-underline-offset:3px}.new-org-cta{margin-top:16px;padding-top:16px;border-top:1px solid rgba(var(--ion-color-dark-rgb),.1)}\n"] }]
54115
+ }], ctorParameters: () => [], propDecorators: { _modalRef: [{
54116
+ type: Input
54117
+ }], onSuccess: [{
54118
+ type: Input
54119
+ }], onProfileClick: [{
54120
+ type: Input
54121
+ }], onCreateOrg: [{
54122
+ type: Input
54123
+ }], i18nNamespace: [{
54124
+ type: Input
54125
+ }] } });
54126
+
54198
54127
  /**
54199
54128
  * Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
54200
54129
  * el constructor del componente si el consumer no proveyó el namespace
@@ -54265,6 +54194,7 @@ const ORGANIZATION_VIEW_I18N = {
54265
54194
  apiKeysFeaturePermsDesc: 'Cada key lleva solo los permisos que tú le asignes — concede el mínimo.',
54266
54195
  apiKeysFeatureSecretTitle: 'Secreto único',
54267
54196
  apiKeysFeatureSecretDesc: 'El secreto se muestra una sola vez. Guárdalo en un gestor de secretos.',
54197
+ switchOrgCta: 'Cambiar organización',
54268
54198
  orgsMoreInfo: 'Aprender más sobre organizaciones',
54269
54199
  orgsInfoTitle: '¿Qué son las organizaciones?',
54270
54200
  orgsInfoSubtitle: 'Todo lo que necesitas saber',
@@ -54363,6 +54293,7 @@ const ORGANIZATION_VIEW_I18N = {
54363
54293
  apiKeysFeaturePermsDesc: 'Each key carries only the permissions you assign — grant the minimum.',
54364
54294
  apiKeysFeatureSecretTitle: 'One-time secret',
54365
54295
  apiKeysFeatureSecretDesc: 'The secret is shown only once. Store it in a secret manager.',
54296
+ switchOrgCta: 'Switch organization',
54366
54297
  orgsMoreInfo: 'Learn more about organizations',
54367
54298
  orgsInfoTitle: 'What are organizations?',
54368
54299
  orgsInfoSubtitle: 'Everything you need to know',
@@ -54797,6 +54728,16 @@ class OrganizationViewComponent {
54797
54728
  }
54798
54729
  // "Más información" sobre organizaciones — mismo sheet explicativo que muestra
54799
54730
  // account-view sin org, pero disponible siempre (todo user tiene org por defecto).
54731
+ onSwitchOrg() {
54732
+ void this.modalService.open({
54733
+ component: SwitchOrgModalComponent,
54734
+ componentProps: {
54735
+ i18nNamespace: 'Settings.SwitchOrg',
54736
+ },
54737
+ breakpoints: { breakpoints: [0, 0.85], initialBreakpoint: 0.85, showHandle: true },
54738
+ backdropDismiss: true,
54739
+ });
54740
+ }
54800
54741
  onOrgInfo() {
54801
54742
  void this.modalService.open({
54802
54743
  component: OrgInfoSheetComponent,
@@ -55066,6 +55007,12 @@ class OrganizationViewComponent {
55066
55007
  <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
55067
55008
  {{ tt('orgsMoreInfo') }}
55068
55009
  </button>
55010
+ <!-- Cambiar de organización activa (solo si hay más de una). -->
55011
+ <div class="org-switch-row">
55012
+ <ion-button fill="outline" color="dark" shape="round" size="small" (click)="onSwitchOrg()">
55013
+ {{ tt('switchOrgCta') }}
55014
+ </ion-button>
55015
+ </div>
55069
55016
  </header>
55070
55017
 
55071
55018
  @if (resolvedConfig().showInfo) {
@@ -55367,7 +55314,7 @@ class OrganizationViewComponent {
55367
55314
  (created)="onOrgCreated($event)"
55368
55315
  />
55369
55316
  </div>
55370
- `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px;margin-bottom:12px}.section-title-danger val-title{display:block;margin:0}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { 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"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
55317
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px;margin-bottom:12px}.section-title-danger val-title{display:block;margin:0}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { 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: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { 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"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
55371
55318
  }
55372
55319
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
55373
55320
  type: Component,
@@ -55377,6 +55324,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55377
55324
  DisplayComponent,
55378
55325
  EmptyStateComponent,
55379
55326
  HasPermissionDirective,
55327
+ IonButton,
55380
55328
  IonIcon,
55381
55329
  SkeletonLayoutComponent,
55382
55330
  TitleComponent,
@@ -55387,6 +55335,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55387
55335
  TransferOwnershipModalComponent,
55388
55336
  MemberImportModalComponent,
55389
55337
  OrgInfoSheetComponent,
55338
+ SwitchOrgModalComponent,
55390
55339
  ], template: `
55391
55340
  <div class="page">
55392
55341
  <header class="page-header">
@@ -55404,6 +55353,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55404
55353
  <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
55405
55354
  {{ tt('orgsMoreInfo') }}
55406
55355
  </button>
55356
+ <!-- Cambiar de organización activa (solo si hay más de una). -->
55357
+ <div class="org-switch-row">
55358
+ <ion-button fill="outline" color="dark" shape="round" size="small" (click)="onSwitchOrg()">
55359
+ {{ tt('switchOrgCta') }}
55360
+ </ion-button>
55361
+ </div>
55407
55362
  </header>
55408
55363
 
55409
55364
  @if (resolvedConfig().showInfo) {
@@ -61944,7 +61899,7 @@ class LegalContentService {
61944
61899
  const stream = fallback && fallback !== locale
61945
61900
  ? primary.pipe(catchError$1(() => this.loadOne(slug, fallback, options.basePath)))
61946
61901
  : primary;
61947
- const shared = stream.pipe(shareReplay$1({ bufferSize: 1, refCount: false }));
61902
+ const shared = stream.pipe(shareReplay({ bufferSize: 1, refCount: false }));
61948
61903
  this.cache.set(key, shared);
61949
61904
  return shared;
61950
61905
  }