valtech-components 4.0.55 → 4.0.57
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.
- package/esm2022/lib/components/organisms/login/login.component.mjs +188 -24
- package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +140 -2
- package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +22 -4
- package/esm2022/lib/components/organisms/organization-view/types.mjs +1 -1
- package/esm2022/lib/services/auth/auth.service.mjs +20 -1
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/services/i18n/default-content.mjs +29 -1
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +393 -28
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/login/login.component.d.ts +17 -0
- package/lib/components/organisms/organization-view/organization-view.component.d.ts +15 -1
- package/lib/components/organisms/organization-view/types.d.ts +14 -0
- package/lib/services/auth/auth.service.d.ts +12 -1
- package/lib/services/auth/types.d.ts +28 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
59
|
+
const VERSION = '4.0.57';
|
|
60
60
|
|
|
61
61
|
// Control de estado de refresco (singleton a nivel de módulo)
|
|
62
62
|
let isRefreshing = false;
|
|
@@ -7341,6 +7341,20 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
7341
7341
|
mfaTOTP: 'Ingresa el código de tu app de autenticación',
|
|
7342
7342
|
mfaEmail: 'Ingresa el código enviado a tu correo',
|
|
7343
7343
|
mfaSMS: 'Ingresa el código enviado a tu teléfono',
|
|
7344
|
+
mfaBackupCode: 'Ingresa uno de tus códigos de respaldo',
|
|
7345
|
+
mfaUseBackupLink: '¿Perdiste tu dispositivo?',
|
|
7346
|
+
mfaUseBackupAction: 'Usa un código de respaldo',
|
|
7347
|
+
mfaUseTotpAction: 'Volver al código de la app',
|
|
7348
|
+
backupCodeMinLength: 'El código de respaldo tiene 8 caracteres',
|
|
7349
|
+
mfaLostBackupLink: '¿Sin acceso a tu segundo factor?',
|
|
7350
|
+
mfaResetAction: 'Restablecer MFA por correo',
|
|
7351
|
+
mfaResetTitle: 'Restablecer MFA',
|
|
7352
|
+
mfaResetRequestDescription: 'Te enviaremos un código a tu correo para deshabilitar la verificación en dos pasos.',
|
|
7353
|
+
mfaResetConfirmDescription: 'Ingresa el código que enviamos a tu correo.',
|
|
7354
|
+
mfaResetSubmit: 'Enviar código',
|
|
7355
|
+
mfaResetConfirmSubmit: 'Deshabilitar MFA',
|
|
7356
|
+
mfaResetCodeSent: 'Si la cuenta existe, enviamos un código a tu correo.',
|
|
7357
|
+
mfaResetDone: 'MFA deshabilitado. Inicia sesión nuevamente.',
|
|
7344
7358
|
// Forgot/Reset password
|
|
7345
7359
|
forgotTitle: 'Recuperar contraseña',
|
|
7346
7360
|
forgotDescription: 'Ingresa tu correo electrónico y te enviaremos un código.',
|
|
@@ -7526,6 +7540,20 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
7526
7540
|
mfaTOTP: 'Enter the code from your authenticator app',
|
|
7527
7541
|
mfaEmail: 'Enter the code sent to your email',
|
|
7528
7542
|
mfaSMS: 'Enter the code sent to your phone',
|
|
7543
|
+
mfaBackupCode: 'Enter one of your backup codes',
|
|
7544
|
+
mfaUseBackupLink: 'Lost your device?',
|
|
7545
|
+
mfaUseBackupAction: 'Use a backup code',
|
|
7546
|
+
mfaUseTotpAction: 'Back to authenticator code',
|
|
7547
|
+
backupCodeMinLength: 'The backup code is 8 characters',
|
|
7548
|
+
mfaLostBackupLink: 'No access to your second factor?',
|
|
7549
|
+
mfaResetAction: 'Reset MFA by email',
|
|
7550
|
+
mfaResetTitle: 'Reset MFA',
|
|
7551
|
+
mfaResetRequestDescription: 'We will email you a code to disable two-step verification.',
|
|
7552
|
+
mfaResetConfirmDescription: 'Enter the code we sent to your email.',
|
|
7553
|
+
mfaResetSubmit: 'Send code',
|
|
7554
|
+
mfaResetConfirmSubmit: 'Disable MFA',
|
|
7555
|
+
mfaResetCodeSent: 'If the account exists, we sent a code to your email.',
|
|
7556
|
+
mfaResetDone: 'MFA disabled. Please sign in again.',
|
|
7529
7557
|
// Forgot/Reset password
|
|
7530
7558
|
forgotTitle: 'Recover password',
|
|
7531
7559
|
forgotDescription: 'Enter your email and we will send you a code.',
|
|
@@ -8762,6 +8790,25 @@ class AuthService {
|
|
|
8762
8790
|
.post(`${this.baseUrl}/reset-password`, request)
|
|
8763
8791
|
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8764
8792
|
}
|
|
8793
|
+
/**
|
|
8794
|
+
* Inicia el reset de MFA por email cuando el user perdió su 2º factor
|
|
8795
|
+
* (TOTP + backup codes). Envía un código de 6 dígitos al email verificado.
|
|
8796
|
+
* Response siempre genérica (anti-enumeración).
|
|
8797
|
+
*/
|
|
8798
|
+
requestMFAReset(request) {
|
|
8799
|
+
return this.http
|
|
8800
|
+
.post(`${this.baseUrl}/mfa/reset-request`, request)
|
|
8801
|
+
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8802
|
+
}
|
|
8803
|
+
/**
|
|
8804
|
+
* Confirma el reset de MFA con el código recibido por email → deshabilita MFA.
|
|
8805
|
+
* Tras esto el user inicia sesión normalmente (sin 2º factor) y re-enrola.
|
|
8806
|
+
*/
|
|
8807
|
+
confirmMFAReset(request) {
|
|
8808
|
+
return this.http
|
|
8809
|
+
.post(`${this.baseUrl}/mfa/reset`, request)
|
|
8810
|
+
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8811
|
+
}
|
|
8765
8812
|
/**
|
|
8766
8813
|
* Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
|
|
8767
8814
|
* la contraseña con el token del enlace de email. En éxito el backend devuelve
|
|
@@ -40691,6 +40738,16 @@ class LoginComponent {
|
|
|
40691
40738
|
this._verifyFormSectionName = signal('');
|
|
40692
40739
|
this._mfaVerifyFormState = signal(ComponentStates.ENABLED);
|
|
40693
40740
|
this._mfaMethod = signal('TOTP');
|
|
40741
|
+
/** Método MFA pendiente — expuesto al template para gatear el link de backup (solo TOTP). */
|
|
40742
|
+
this.mfaMethod = this._mfaMethod.asReadonly();
|
|
40743
|
+
/** A: el user togglea a "código de respaldo" (8 chars alfanuméricos) cuando perdió el 2º factor. */
|
|
40744
|
+
this.mfaUseBackupCode = signal(false);
|
|
40745
|
+
// B: reset de MFA por email (cuando el user no tiene backup codes).
|
|
40746
|
+
this.isMFAResetModalOpen = false;
|
|
40747
|
+
this._mfaResetStep = signal('request');
|
|
40748
|
+
this.mfaResetStep = this._mfaResetStep.asReadonly();
|
|
40749
|
+
this._mfaResetFormState = signal(ComponentStates.ENABLED);
|
|
40750
|
+
this._mfaResetEmail = signal('');
|
|
40694
40751
|
this._forgotPasswordFormState = signal(ComponentStates.ENABLED);
|
|
40695
40752
|
this._resetPasswordFormState = signal(ComponentStates.ENABLED);
|
|
40696
40753
|
this._resetFormSectionName = signal('');
|
|
@@ -40857,7 +40914,51 @@ class LoginComponent {
|
|
|
40857
40914
|
// ==========================================
|
|
40858
40915
|
this.mfaVerifyFormProps = computed(() => {
|
|
40859
40916
|
const method = this._mfaMethod();
|
|
40860
|
-
const
|
|
40917
|
+
const useBackup = this.mfaUseBackupCode();
|
|
40918
|
+
const sectionName = useBackup
|
|
40919
|
+
? this.t('mfaBackupCode')
|
|
40920
|
+
: method === 'TOTP'
|
|
40921
|
+
? this.t('mfaTOTP')
|
|
40922
|
+
: method === 'EMAIL'
|
|
40923
|
+
? this.t('mfaEmail')
|
|
40924
|
+
: this.t('mfaSMS');
|
|
40925
|
+
// A: backup code = 8 chars alfanuméricos → input de texto (el PIN de 6
|
|
40926
|
+
// celdas numéricas no puede capturarlo). El backend rutea por longitud.
|
|
40927
|
+
const codeField = useBackup
|
|
40928
|
+
? {
|
|
40929
|
+
type: InputType.TEXT,
|
|
40930
|
+
label: '',
|
|
40931
|
+
name: 'code',
|
|
40932
|
+
token: 'mfa-backup',
|
|
40933
|
+
hint: '',
|
|
40934
|
+
placeholder: '',
|
|
40935
|
+
errorKeys: {
|
|
40936
|
+
required: 'codeRequired',
|
|
40937
|
+
minlength: 'backupCodeMinLength',
|
|
40938
|
+
},
|
|
40939
|
+
validators: [Validators.required, Validators.minLength(8)],
|
|
40940
|
+
order: 0,
|
|
40941
|
+
state: ComponentStates.ENABLED,
|
|
40942
|
+
autoFocus: true,
|
|
40943
|
+
}
|
|
40944
|
+
: {
|
|
40945
|
+
type: InputType.PIN_CODE,
|
|
40946
|
+
label: '',
|
|
40947
|
+
name: 'code',
|
|
40948
|
+
token: 'mfa-pin',
|
|
40949
|
+
hint: '',
|
|
40950
|
+
placeholder: '',
|
|
40951
|
+
errorKeys: {
|
|
40952
|
+
required: 'codeRequired',
|
|
40953
|
+
minlength: 'codeMinLength',
|
|
40954
|
+
},
|
|
40955
|
+
validators: [Validators.required, Validators.minLength(6)],
|
|
40956
|
+
order: 0,
|
|
40957
|
+
state: ComponentStates.ENABLED,
|
|
40958
|
+
length: 6,
|
|
40959
|
+
allowNumbersOnly: true,
|
|
40960
|
+
autoFocus: true,
|
|
40961
|
+
};
|
|
40861
40962
|
return this.i18nHelper.resolveForm({
|
|
40862
40963
|
nameKey: 'mfaTitle',
|
|
40863
40964
|
i18nNamespace: '_auth',
|
|
@@ -40865,26 +40966,7 @@ class LoginComponent {
|
|
|
40865
40966
|
{
|
|
40866
40967
|
name: sectionName,
|
|
40867
40968
|
order: 0,
|
|
40868
|
-
fields: [
|
|
40869
|
-
{
|
|
40870
|
-
type: InputType.PIN_CODE,
|
|
40871
|
-
label: '',
|
|
40872
|
-
name: 'code',
|
|
40873
|
-
token: 'mfa-pin',
|
|
40874
|
-
hint: '',
|
|
40875
|
-
placeholder: '',
|
|
40876
|
-
errorKeys: {
|
|
40877
|
-
required: 'codeRequired',
|
|
40878
|
-
minlength: 'codeMinLength',
|
|
40879
|
-
},
|
|
40880
|
-
validators: [Validators.required, Validators.minLength(6)],
|
|
40881
|
-
order: 0,
|
|
40882
|
-
state: ComponentStates.ENABLED,
|
|
40883
|
-
length: 6,
|
|
40884
|
-
allowNumbersOnly: true,
|
|
40885
|
-
autoFocus: true,
|
|
40886
|
-
},
|
|
40887
|
-
],
|
|
40969
|
+
fields: [codeField],
|
|
40888
40970
|
},
|
|
40889
40971
|
],
|
|
40890
40972
|
actions: {
|
|
@@ -40896,6 +40978,79 @@ class LoginComponent {
|
|
|
40896
40978
|
});
|
|
40897
40979
|
});
|
|
40898
40980
|
// ==========================================
|
|
40981
|
+
// MFA RESET FORM (computed) — B: recovery por email
|
|
40982
|
+
// ==========================================
|
|
40983
|
+
this.mfaResetRequestFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
40984
|
+
nameKey: 'mfaResetTitle',
|
|
40985
|
+
i18nNamespace: '_auth',
|
|
40986
|
+
sections: [
|
|
40987
|
+
{
|
|
40988
|
+
name: this.t('mfaResetRequestDescription'),
|
|
40989
|
+
order: 0,
|
|
40990
|
+
fields: [
|
|
40991
|
+
{
|
|
40992
|
+
type: InputType.EMAIL,
|
|
40993
|
+
label: '',
|
|
40994
|
+
name: 'email',
|
|
40995
|
+
token: 'mfa-reset-email',
|
|
40996
|
+
hint: '',
|
|
40997
|
+
placeholderKey: 'emailPlaceholder',
|
|
40998
|
+
value: this._mfaResetEmail(),
|
|
40999
|
+
errorKeys: {
|
|
41000
|
+
required: 'emailRequired',
|
|
41001
|
+
email: 'emailInvalid',
|
|
41002
|
+
},
|
|
41003
|
+
validators: [Validators.required, Validators.email],
|
|
41004
|
+
order: 0,
|
|
41005
|
+
state: ComponentStates.ENABLED,
|
|
41006
|
+
},
|
|
41007
|
+
],
|
|
41008
|
+
},
|
|
41009
|
+
],
|
|
41010
|
+
actions: {
|
|
41011
|
+
...button({ text: '', type: 'submit', fill: 'solid', expand: 'block' }),
|
|
41012
|
+
token: 'mfa-reset-request-submit',
|
|
41013
|
+
textKey: 'mfaResetSubmit',
|
|
41014
|
+
},
|
|
41015
|
+
state: this._mfaResetFormState(),
|
|
41016
|
+
}));
|
|
41017
|
+
this.mfaResetConfirmFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
41018
|
+
nameKey: 'mfaResetTitle',
|
|
41019
|
+
i18nNamespace: '_auth',
|
|
41020
|
+
sections: [
|
|
41021
|
+
{
|
|
41022
|
+
name: this.t('mfaResetConfirmDescription'),
|
|
41023
|
+
order: 0,
|
|
41024
|
+
fields: [
|
|
41025
|
+
{
|
|
41026
|
+
type: InputType.PIN_CODE,
|
|
41027
|
+
label: '',
|
|
41028
|
+
name: 'code',
|
|
41029
|
+
token: 'mfa-reset-pin',
|
|
41030
|
+
hint: '',
|
|
41031
|
+
placeholder: '',
|
|
41032
|
+
errorKeys: {
|
|
41033
|
+
required: 'codeRequired',
|
|
41034
|
+
minlength: 'codeMinLength',
|
|
41035
|
+
},
|
|
41036
|
+
validators: [Validators.required, Validators.minLength(6)],
|
|
41037
|
+
order: 0,
|
|
41038
|
+
state: ComponentStates.ENABLED,
|
|
41039
|
+
length: 6,
|
|
41040
|
+
allowNumbersOnly: true,
|
|
41041
|
+
autoFocus: true,
|
|
41042
|
+
},
|
|
41043
|
+
],
|
|
41044
|
+
},
|
|
41045
|
+
],
|
|
41046
|
+
actions: {
|
|
41047
|
+
...button({ text: '', type: 'submit', fill: 'solid', expand: 'block' }),
|
|
41048
|
+
token: 'mfa-reset-confirm-submit',
|
|
41049
|
+
textKey: 'mfaResetConfirmSubmit',
|
|
41050
|
+
},
|
|
41051
|
+
state: this._mfaResetFormState(),
|
|
41052
|
+
}));
|
|
41053
|
+
// ==========================================
|
|
40899
41054
|
// FORGOT PASSWORD FORM (computed)
|
|
40900
41055
|
// ==========================================
|
|
40901
41056
|
this.forgotPasswordFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
@@ -41026,6 +41181,7 @@ class LoginComponent {
|
|
|
41026
41181
|
next: () => {
|
|
41027
41182
|
this._loginFormState.set(ComponentStates.ENABLED);
|
|
41028
41183
|
if (this.authService.mfaPending().required) {
|
|
41184
|
+
this._mfaResetEmail.set(email); // B: prefill del reset por email
|
|
41029
41185
|
this.openMFAVerifyModal();
|
|
41030
41186
|
return;
|
|
41031
41187
|
}
|
|
@@ -41154,6 +41310,7 @@ class LoginComponent {
|
|
|
41154
41310
|
openMFAVerifyModal() {
|
|
41155
41311
|
const method = this.authService.mfaPending().method;
|
|
41156
41312
|
this._mfaMethod.set(method);
|
|
41313
|
+
this.mfaUseBackupCode.set(false);
|
|
41157
41314
|
this._mfaVerifyFormState.set(ComponentStates.ENABLED);
|
|
41158
41315
|
this.isMFAVerifyModalOpen = true;
|
|
41159
41316
|
this.onMFARequired.emit({ method });
|
|
@@ -41161,6 +41318,60 @@ class LoginComponent {
|
|
|
41161
41318
|
closeMFAVerifyModal() {
|
|
41162
41319
|
this.isMFAVerifyModalOpen = false;
|
|
41163
41320
|
}
|
|
41321
|
+
/** A: alterna entre el código TOTP (6 díg) y un código de respaldo (8 chars). */
|
|
41322
|
+
toggleMFABackupCode() {
|
|
41323
|
+
this.mfaUseBackupCode.update(v => !v);
|
|
41324
|
+
}
|
|
41325
|
+
// ==========================================
|
|
41326
|
+
// MFA RESET HANDLERS — B: recovery por email
|
|
41327
|
+
// ==========================================
|
|
41328
|
+
openMFAResetModal() {
|
|
41329
|
+
this._mfaResetStep.set('request');
|
|
41330
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41331
|
+
this.isMFAResetModalOpen = true;
|
|
41332
|
+
}
|
|
41333
|
+
closeMFAResetModal() {
|
|
41334
|
+
this.isMFAResetModalOpen = false;
|
|
41335
|
+
this._mfaResetStep.set('request');
|
|
41336
|
+
}
|
|
41337
|
+
mfaResetRequestHandler(event) {
|
|
41338
|
+
const email = event.fields['email'];
|
|
41339
|
+
if (!email) {
|
|
41340
|
+
this.showToast(this.t('enterEmail'));
|
|
41341
|
+
return;
|
|
41342
|
+
}
|
|
41343
|
+
this._mfaResetFormState.set(ComponentStates.WORKING);
|
|
41344
|
+
this._mfaResetEmail.set(email);
|
|
41345
|
+
this.authService.requestMFAReset({ email }).subscribe({
|
|
41346
|
+
next: () => {
|
|
41347
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41348
|
+
this._mfaResetStep.set('confirm');
|
|
41349
|
+
this.showToast(this.t('mfaResetCodeSent'));
|
|
41350
|
+
},
|
|
41351
|
+
error: err => {
|
|
41352
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41353
|
+
this.handleError(err, 'mfa');
|
|
41354
|
+
},
|
|
41355
|
+
});
|
|
41356
|
+
}
|
|
41357
|
+
mfaResetConfirmHandler(event) {
|
|
41358
|
+
const code = event.fields['code'];
|
|
41359
|
+
this._mfaResetFormState.set(ComponentStates.WORKING);
|
|
41360
|
+
this.authService.confirmMFAReset({ email: this._mfaResetEmail(), code }).subscribe({
|
|
41361
|
+
next: () => {
|
|
41362
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41363
|
+
// MFA deshabilitado → cerramos ambos modales; el user reinicia el login
|
|
41364
|
+
// (ahora sin 2º factor) y re-enrola TOTP desde ajustes.
|
|
41365
|
+
this.closeMFAResetModal();
|
|
41366
|
+
this.closeMFAVerifyModal();
|
|
41367
|
+
this.showToast(this.t('mfaResetDone'));
|
|
41368
|
+
},
|
|
41369
|
+
error: err => {
|
|
41370
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41371
|
+
this.handleError(err, 'mfa');
|
|
41372
|
+
},
|
|
41373
|
+
});
|
|
41374
|
+
}
|
|
41164
41375
|
verifyMFAHandler(event) {
|
|
41165
41376
|
const code = event.fields['code'];
|
|
41166
41377
|
this._mfaVerifyFormState.set(ComponentStates.WORKING);
|
|
@@ -41377,7 +41588,7 @@ class LoginComponent {
|
|
|
41377
41588
|
this.stopResetResendCooldown();
|
|
41378
41589
|
}
|
|
41379
41590
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
41380
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginComponent, isStandalone: true, selector: "val-login", inputs: { props: "props" }, outputs: { onSuccess: "onSuccess", onError: "onError", onMFARequired: "onMFARequired" }, ngImport: i0, template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: ImageComponent, selector: "val-image", inputs: ["props"] }] }); }
|
|
41591
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginComponent, isStandalone: true, selector: "val-login", inputs: { props: "props" }, outputs: { onSuccess: "onSuccess", onError: "onError", onMFARequired: "onMFARequired" }, ngImport: i0, template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n @if (mfaMethod() === 'TOTP') {\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n @if (mfaUseBackupCode()) {\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseTotpAction') }}</a>\n } @else { {{ t('mfaUseBackupLink') }}\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseBackupAction') }}</a>\n }\n </ion-text>\n </div>\n }\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('mfaLostBackupLink') }}\n <a (click)=\"openMFAResetModal()\">{{ t('mfaResetAction') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Reset Modal (B) \u2014 recovery por email cuando no hay backup codes -->\n<ion-modal [isOpen]=\"isMFAResetModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"closeMFAResetModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAResetModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n @if (mfaResetStep() === 'request') {\n <val-form [props]=\"mfaResetRequestFormProps()\" (onSubmit)=\"mfaResetRequestHandler($event)\" />\n } @else {\n <val-form [props]=\"mfaResetConfirmFormProps()\" (onSubmit)=\"mfaResetConfirmHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: ImageComponent, selector: "val-image", inputs: ["props"] }] }); }
|
|
41381
41592
|
}
|
|
41382
41593
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, decorators: [{
|
|
41383
41594
|
type: Component,
|
|
@@ -41393,7 +41604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
41393
41604
|
IonToolbar,
|
|
41394
41605
|
FormComponent,
|
|
41395
41606
|
ImageComponent,
|
|
41396
|
-
], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"] }]
|
|
41607
|
+
], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n @if (mfaMethod() === 'TOTP') {\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n @if (mfaUseBackupCode()) {\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseTotpAction') }}</a>\n } @else { {{ t('mfaUseBackupLink') }}\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseBackupAction') }}</a>\n }\n </ion-text>\n </div>\n }\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('mfaLostBackupLink') }}\n <a (click)=\"openMFAResetModal()\">{{ t('mfaResetAction') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Reset Modal (B) \u2014 recovery por email cuando no hay backup codes -->\n<ion-modal [isOpen]=\"isMFAResetModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"closeMFAResetModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAResetModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n @if (mfaResetStep() === 'request') {\n <val-form [props]=\"mfaResetRequestFormProps()\" (onSubmit)=\"mfaResetRequestHandler($event)\" />\n } @else {\n <val-form [props]=\"mfaResetConfirmFormProps()\" (onSubmit)=\"mfaResetConfirmHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"] }]
|
|
41397
41608
|
}], propDecorators: { props: [{
|
|
41398
41609
|
type: Input
|
|
41399
41610
|
}], onSuccess: [{
|
|
@@ -52178,7 +52389,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52178
52389
|
inviteSuccess: 'Invitación enviada.',
|
|
52179
52390
|
inviteError: 'No se pudo enviar la invitación.',
|
|
52180
52391
|
membersError: 'No se pudieron cargar los miembros.',
|
|
52181
|
-
leaveTitle: 'Salir de la organización',
|
|
52392
|
+
leaveTitle: '⚠️ Salir de la organización',
|
|
52182
52393
|
leaveHint: 'Dejarás de tener acceso a esta organización.',
|
|
52183
52394
|
leaveCta: 'Salir',
|
|
52184
52395
|
leaveOwnerHint: 'Eres el propietario. Transfiere la propiedad antes de salir.',
|
|
@@ -52194,7 +52405,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52194
52405
|
retry: 'Reintentar',
|
|
52195
52406
|
saveError: 'No se pudo guardar. Intenta de nuevo.',
|
|
52196
52407
|
leaveError: 'No se pudo salir. Intenta de nuevo.',
|
|
52197
|
-
transferTitle: 'Transferir propiedad',
|
|
52408
|
+
transferTitle: '⚠️ Transferir propiedad',
|
|
52198
52409
|
transferHint: 'Elige un miembro para transferirle la propiedad de la organización.',
|
|
52199
52410
|
transferSelect: 'Seleccionar nuevo propietario',
|
|
52200
52411
|
transferCta: 'Transferir',
|
|
@@ -52245,6 +52456,15 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52245
52456
|
permDelete: 'Eliminar',
|
|
52246
52457
|
permManage: 'Gestionar',
|
|
52247
52458
|
membersShowMore: 'Ver más',
|
|
52459
|
+
orgsNewQuestion: '¿Necesitas otra organización?',
|
|
52460
|
+
orgsNewHint: 'Crea un espacio separado para otro equipo, empresa o proyecto.',
|
|
52461
|
+
orgsNewCta: 'Nueva organización',
|
|
52462
|
+
deleteTitle: '⚠️ Eliminar organización',
|
|
52463
|
+
deleteHint: 'Esta acción es permanente e irreversible. Todos los miembros perderán el acceso.',
|
|
52464
|
+
deleteCta: 'Eliminar organización',
|
|
52465
|
+
deleteConfirm: '¿Estás seguro? Esta acción eliminará permanentemente la organización y no se puede deshacer.',
|
|
52466
|
+
deleteSuccess: 'Organización eliminada.',
|
|
52467
|
+
deleteError: 'No se pudo eliminar la organización. Intenta de nuevo.',
|
|
52248
52468
|
},
|
|
52249
52469
|
en: {
|
|
52250
52470
|
pageTitle: 'Organization',
|
|
@@ -52283,7 +52503,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52283
52503
|
retry: 'Retry',
|
|
52284
52504
|
saveError: 'Could not save. Please try again.',
|
|
52285
52505
|
leaveError: 'Could not leave. Please try again.',
|
|
52286
|
-
transferTitle: 'Transfer ownership',
|
|
52506
|
+
transferTitle: '⚠️ Transfer ownership',
|
|
52287
52507
|
transferHint: 'Choose a member to transfer ownership of this organization.',
|
|
52288
52508
|
transferSelect: 'Select new owner',
|
|
52289
52509
|
transferCta: 'Transfer',
|
|
@@ -52334,6 +52554,15 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52334
52554
|
permDelete: 'Delete',
|
|
52335
52555
|
permManage: 'Manage',
|
|
52336
52556
|
membersShowMore: 'Show more',
|
|
52557
|
+
orgsNewQuestion: 'Need another organization?',
|
|
52558
|
+
orgsNewHint: 'Create a separate space for another team, company or project.',
|
|
52559
|
+
orgsNewCta: 'New organization',
|
|
52560
|
+
deleteTitle: '⚠️ Delete organization',
|
|
52561
|
+
deleteHint: 'This action is permanent and irreversible. All members will lose access.',
|
|
52562
|
+
deleteCta: 'Delete organization',
|
|
52563
|
+
deleteConfirm: 'Are you sure? This action will permanently delete the organization and cannot be undone.',
|
|
52564
|
+
deleteSuccess: 'Organization deleted.',
|
|
52565
|
+
deleteError: 'Could not delete organization. Please try again.',
|
|
52337
52566
|
},
|
|
52338
52567
|
};
|
|
52339
52568
|
|
|
@@ -52394,8 +52623,10 @@ class OrganizationViewComponent {
|
|
|
52394
52623
|
showMembers: merged.showMembers ?? true,
|
|
52395
52624
|
showRoles: merged.showRoles ?? true,
|
|
52396
52625
|
showInvite: merged.showInvite ?? true,
|
|
52626
|
+
showNewOrgCta: merged.showNewOrgCta ?? false,
|
|
52397
52627
|
showTransferOwnership: merged.showTransferOwnership ?? true,
|
|
52398
52628
|
showLeave: merged.showLeave ?? true,
|
|
52629
|
+
showDeleteOrg: merged.showDeleteOrg ?? true,
|
|
52399
52630
|
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$6,
|
|
52400
52631
|
viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
|
|
52401
52632
|
apiKeysRoute: merged.apiKeysRoute ?? DEFAULT_API_KEYS_ROUTE,
|
|
@@ -52404,6 +52635,8 @@ class OrganizationViewComponent {
|
|
|
52404
52635
|
onMemberInvited: merged.onMemberInvited,
|
|
52405
52636
|
onOwnershipTransferred: merged.onOwnershipTransferred,
|
|
52406
52637
|
onLeftOrg: merged.onLeftOrg,
|
|
52638
|
+
onOrgDeleted: merged.onOrgDeleted,
|
|
52639
|
+
onOrgCreated: merged.onOrgCreated,
|
|
52407
52640
|
};
|
|
52408
52641
|
});
|
|
52409
52642
|
this.org = signal(null);
|
|
@@ -52412,6 +52645,7 @@ class OrganizationViewComponent {
|
|
|
52412
52645
|
this.loading = signal(false);
|
|
52413
52646
|
this.leaving = signal(false);
|
|
52414
52647
|
this.permissionsOpen = signal(false);
|
|
52648
|
+
this.createOrgModalOpen = signal(false);
|
|
52415
52649
|
this.orgLoadError = signal(null);
|
|
52416
52650
|
this.orgErrorState = computed(() => {
|
|
52417
52651
|
this.i18n.lang();
|
|
@@ -52453,6 +52687,7 @@ class OrganizationViewComponent {
|
|
|
52453
52687
|
this.showAllMembers = signal(false);
|
|
52454
52688
|
this.visibleMembers = computed(() => (this.showAllMembers() ? this.members() : this.members().slice(0, 3)));
|
|
52455
52689
|
this.transferring = signal(false);
|
|
52690
|
+
this.deleting = signal(false);
|
|
52456
52691
|
this.availableRoles = signal(SYSTEM_ROLE_DEFAULTS);
|
|
52457
52692
|
this.activeOrgId = computed(() => {
|
|
52458
52693
|
const u = this.auth.user();
|
|
@@ -52546,6 +52781,38 @@ class OrganizationViewComponent {
|
|
|
52546
52781
|
shape: 'round',
|
|
52547
52782
|
type: 'button',
|
|
52548
52783
|
}));
|
|
52784
|
+
this.deleteButtonProps = computed(() => ({
|
|
52785
|
+
text: this.tt('deleteCta'),
|
|
52786
|
+
token: 'org-delete',
|
|
52787
|
+
color: 'dark',
|
|
52788
|
+
fill: 'outline',
|
|
52789
|
+
size: 'default',
|
|
52790
|
+
shape: 'round',
|
|
52791
|
+
type: 'button',
|
|
52792
|
+
state: this.deleting() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
|
|
52793
|
+
}));
|
|
52794
|
+
this.newOrgCtaProps = computed(() => ({
|
|
52795
|
+
title: this.tt('orgsNewQuestion'),
|
|
52796
|
+
description: this.tt('orgsNewHint'),
|
|
52797
|
+
padding: '16px',
|
|
52798
|
+
borderRadius: '14px',
|
|
52799
|
+
actions: {
|
|
52800
|
+
position: 'right',
|
|
52801
|
+
columned: false,
|
|
52802
|
+
buttons: [
|
|
52803
|
+
{
|
|
52804
|
+
text: this.tt('orgsNewCta'),
|
|
52805
|
+
color: 'primary',
|
|
52806
|
+
fill: 'solid',
|
|
52807
|
+
shape: 'round',
|
|
52808
|
+
size: 'default',
|
|
52809
|
+
type: 'button',
|
|
52810
|
+
state: 'ENABLED',
|
|
52811
|
+
token: 'org-new',
|
|
52812
|
+
},
|
|
52813
|
+
],
|
|
52814
|
+
},
|
|
52815
|
+
}));
|
|
52549
52816
|
/** Guarda para detectar el primer disparo del effect (evita doble carga). */
|
|
52550
52817
|
this.lastLoadedOrgId = null;
|
|
52551
52818
|
const ns = this.ns;
|
|
@@ -52622,6 +52889,46 @@ class OrganizationViewComponent {
|
|
|
52622
52889
|
this.leaving.set(false);
|
|
52623
52890
|
}
|
|
52624
52891
|
}
|
|
52892
|
+
async onDelete() {
|
|
52893
|
+
if (!this.isOwner() || this.deleting())
|
|
52894
|
+
return;
|
|
52895
|
+
const result = await this.confirmDialog.confirmDestructive({
|
|
52896
|
+
title: this.tt('deleteTitle'),
|
|
52897
|
+
message: this.tt('deleteConfirm'),
|
|
52898
|
+
confirmButton: { text: this.tt('deleteCta') },
|
|
52899
|
+
cancelButton: { text: this.tt('cancelCta') },
|
|
52900
|
+
});
|
|
52901
|
+
if (!result.confirmed)
|
|
52902
|
+
return;
|
|
52903
|
+
const orgId = this.activeOrgId();
|
|
52904
|
+
if (!orgId)
|
|
52905
|
+
return;
|
|
52906
|
+
this.deleting.set(true);
|
|
52907
|
+
try {
|
|
52908
|
+
await firstValueFrom(this.orgService.deleteOrg(orgId));
|
|
52909
|
+
this.toast.show({ message: this.tt('deleteSuccess'), color: 'dark', duration: 3500 });
|
|
52910
|
+
const cfg = this.resolvedConfig();
|
|
52911
|
+
this.nav.navigateByUrl(cfg.leaveRedirectRoute);
|
|
52912
|
+
cfg.onOrgDeleted?.();
|
|
52913
|
+
}
|
|
52914
|
+
catch (err) {
|
|
52915
|
+
this.errors.handle(err, {
|
|
52916
|
+
context: 'organization.delete',
|
|
52917
|
+
fallbackKey: 'deleteError',
|
|
52918
|
+
i18nNamespace: this.ns,
|
|
52919
|
+
});
|
|
52920
|
+
}
|
|
52921
|
+
finally {
|
|
52922
|
+
this.deleting.set(false);
|
|
52923
|
+
}
|
|
52924
|
+
}
|
|
52925
|
+
onNewOrg() {
|
|
52926
|
+
this.createOrgModalOpen.set(true);
|
|
52927
|
+
}
|
|
52928
|
+
onOrgCreated(newOrg) {
|
|
52929
|
+
this.createOrgModalOpen.set(false);
|
|
52930
|
+
this.resolvedConfig().onOrgCreated?.(newOrg);
|
|
52931
|
+
}
|
|
52625
52932
|
onOpenInvite() {
|
|
52626
52933
|
void this.modalService.openAdaptive({
|
|
52627
52934
|
component: InviteMemberModalComponent,
|
|
@@ -53066,6 +53373,13 @@ class OrganizationViewComponent {
|
|
|
53066
53373
|
</section>
|
|
53067
53374
|
}
|
|
53068
53375
|
|
|
53376
|
+
@if (resolvedConfig().showNewOrgCta) {
|
|
53377
|
+
<!-- Nueva org CTA -->
|
|
53378
|
+
<section class="settings-section">
|
|
53379
|
+
<val-cta-card [props]="newOrgCtaProps()" (onAction)="onNewOrg()" />
|
|
53380
|
+
</section>
|
|
53381
|
+
}
|
|
53382
|
+
|
|
53069
53383
|
@if (resolvedConfig().showInvite) {
|
|
53070
53384
|
<!-- Invite: solo admin+ (gating RBAC interno) -->
|
|
53071
53385
|
<section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
|
|
@@ -53169,6 +53483,22 @@ class OrganizationViewComponent {
|
|
|
53169
53483
|
</section>
|
|
53170
53484
|
}
|
|
53171
53485
|
|
|
53486
|
+
@if (resolvedConfig().showDeleteOrg && isOwner()) {
|
|
53487
|
+
<!-- Delete org: solo owner, acción más destructiva -->
|
|
53488
|
+
<section class="settings-section" data-testid="org-delete-section">
|
|
53489
|
+
<div class="section-title-danger">
|
|
53490
|
+
<ion-icon name="warning-outline" class="danger-icon" aria-hidden="true"></ion-icon>
|
|
53491
|
+
<val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('deleteTitle') }" />
|
|
53492
|
+
</div>
|
|
53493
|
+
<div class="section-body">
|
|
53494
|
+
<val-text [props]="{ content: tt('deleteHint'), size: 'medium', color: 'dark', bold: false }" />
|
|
53495
|
+
<div class="row-actions">
|
|
53496
|
+
<val-button [props]="deleteButtonProps()" (click)="onDelete()" />
|
|
53497
|
+
</div>
|
|
53498
|
+
</div>
|
|
53499
|
+
</section>
|
|
53500
|
+
}
|
|
53501
|
+
|
|
53172
53502
|
@if (isDebug) {
|
|
53173
53503
|
<section class="settings-section rbac-debug">
|
|
53174
53504
|
<div class="section-header-row">
|
|
@@ -53198,12 +53528,19 @@ class OrganizationViewComponent {
|
|
|
53198
53528
|
[config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
|
|
53199
53529
|
(dismissed)="permissionsOpen.set(false)"
|
|
53200
53530
|
/>
|
|
53531
|
+
<val-create-org-modal
|
|
53532
|
+
[isOpen]="createOrgModalOpen()"
|
|
53533
|
+
(dismissed)="createOrgModalOpen.set(false)"
|
|
53534
|
+
(created)="onOrgCreated($event)"
|
|
53535
|
+
/>
|
|
53201
53536
|
</div>
|
|
53202
|
-
`, 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}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ 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"] }] }); }
|
|
53537
|
+
`, 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}.section-title-danger val-title{display:block}.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"] }] }); }
|
|
53203
53538
|
}
|
|
53204
53539
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
|
|
53205
53540
|
type: Component,
|
|
53206
53541
|
args: [{ selector: 'val-organization-view', standalone: true, imports: [
|
|
53542
|
+
CreateOrgModalComponent,
|
|
53543
|
+
CtaCardComponent,
|
|
53207
53544
|
DisplayComponent,
|
|
53208
53545
|
EmptyStateComponent,
|
|
53209
53546
|
HasPermissionDirective,
|
|
@@ -53374,6 +53711,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
53374
53711
|
</section>
|
|
53375
53712
|
}
|
|
53376
53713
|
|
|
53714
|
+
@if (resolvedConfig().showNewOrgCta) {
|
|
53715
|
+
<!-- Nueva org CTA -->
|
|
53716
|
+
<section class="settings-section">
|
|
53717
|
+
<val-cta-card [props]="newOrgCtaProps()" (onAction)="onNewOrg()" />
|
|
53718
|
+
</section>
|
|
53719
|
+
}
|
|
53720
|
+
|
|
53377
53721
|
@if (resolvedConfig().showInvite) {
|
|
53378
53722
|
<!-- Invite: solo admin+ (gating RBAC interno) -->
|
|
53379
53723
|
<section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
|
|
@@ -53477,6 +53821,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
53477
53821
|
</section>
|
|
53478
53822
|
}
|
|
53479
53823
|
|
|
53824
|
+
@if (resolvedConfig().showDeleteOrg && isOwner()) {
|
|
53825
|
+
<!-- Delete org: solo owner, acción más destructiva -->
|
|
53826
|
+
<section class="settings-section" data-testid="org-delete-section">
|
|
53827
|
+
<div class="section-title-danger">
|
|
53828
|
+
<ion-icon name="warning-outline" class="danger-icon" aria-hidden="true"></ion-icon>
|
|
53829
|
+
<val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('deleteTitle') }" />
|
|
53830
|
+
</div>
|
|
53831
|
+
<div class="section-body">
|
|
53832
|
+
<val-text [props]="{ content: tt('deleteHint'), size: 'medium', color: 'dark', bold: false }" />
|
|
53833
|
+
<div class="row-actions">
|
|
53834
|
+
<val-button [props]="deleteButtonProps()" (click)="onDelete()" />
|
|
53835
|
+
</div>
|
|
53836
|
+
</div>
|
|
53837
|
+
</section>
|
|
53838
|
+
}
|
|
53839
|
+
|
|
53480
53840
|
@if (isDebug) {
|
|
53481
53841
|
<section class="settings-section rbac-debug">
|
|
53482
53842
|
<div class="section-header-row">
|
|
@@ -53506,6 +53866,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
53506
53866
|
[config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
|
|
53507
53867
|
(dismissed)="permissionsOpen.set(false)"
|
|
53508
53868
|
/>
|
|
53869
|
+
<val-create-org-modal
|
|
53870
|
+
[isOpen]="createOrgModalOpen()"
|
|
53871
|
+
(dismissed)="createOrgModalOpen.set(false)"
|
|
53872
|
+
(created)="onOrgCreated($event)"
|
|
53873
|
+
/>
|
|
53509
53874
|
</div>
|
|
53510
53875
|
`, 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}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"] }]
|
|
53511
53876
|
}], ctorParameters: () => [], propDecorators: { config: [{
|