valtech-components 4.0.27 → 4.0.28
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/account-view/account-view.component.mjs +34 -30
- package/esm2022/lib/components/organisms/change-password-modal/change-password-modal.component.mjs +3 -3
- package/esm2022/lib/components/organisms/create-org-modal/create-org-modal.component.mjs +91 -65
- package/esm2022/lib/components/organisms/delete-account-modal/delete-account-modal.component.mjs +106 -84
- package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +16 -3
- package/esm2022/lib/components/organisms/permissions-view/permissions-modal.component.mjs +70 -0
- package/esm2022/lib/components/organisms/permissions-view/permissions-view.component.mjs +16 -5
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/valtech-components.mjs +1954 -1814
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/account-view/account-view.component.d.ts +3 -0
- package/lib/components/organisms/create-org-modal/create-org-modal.component.d.ts +8 -8
- package/lib/components/organisms/delete-account-modal/delete-account-modal.component.d.ts +7 -8
- package/lib/components/organisms/organization-view/organization-view.component.d.ts +1 -0
- package/lib/components/organisms/permissions-view/permissions-modal.component.d.ts +13 -0
- package/lib/components/organisms/permissions-view/permissions-view.component.d.ts +9 -2
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
|
@@ -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.28';
|
|
60
60
|
|
|
61
61
|
// Control de estado de refresco (singleton a nivel de módulo)
|
|
62
62
|
let isRefreshing = false;
|
|
@@ -33564,11 +33564,11 @@ class ChangePasswordModalComponent {
|
|
|
33564
33564
|
this.toast.show({ message, duration: 3500 });
|
|
33565
33565
|
}
|
|
33566
33566
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
33567
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-modal", inputs: { isOpen: "isOpen" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
|
|
33567
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-modal", inputs: { isOpen: "isOpen" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\" cssClass=\"val-modal-adaptive\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
|
|
33568
33568
|
}
|
|
33569
33569
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, decorators: [{
|
|
33570
33570
|
type: Component,
|
|
33571
|
-
args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonModal, IonSpinner, IonToolbar, FormComponent], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"] }]
|
|
33571
|
+
args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonModal, IonSpinner, IonToolbar, FormComponent], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\" cssClass=\"val-modal-adaptive\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"] }]
|
|
33572
33572
|
}], propDecorators: { isOpen: [{
|
|
33573
33573
|
type: Input
|
|
33574
33574
|
}], changed: [{
|
|
@@ -46678,6 +46678,16 @@ const DEFAULT_NAMESPACE$h = 'CreateOrgModal';
|
|
|
46678
46678
|
* `CreateOrgModal`.
|
|
46679
46679
|
*/
|
|
46680
46680
|
class CreateOrgModalComponent {
|
|
46681
|
+
set isOpen(value) {
|
|
46682
|
+
const opening = value && !this._isOpen;
|
|
46683
|
+
this._isOpen = value;
|
|
46684
|
+
if (opening) {
|
|
46685
|
+
this._busy.set(false);
|
|
46686
|
+
}
|
|
46687
|
+
}
|
|
46688
|
+
get isOpen() {
|
|
46689
|
+
return this._isOpen;
|
|
46690
|
+
}
|
|
46681
46691
|
constructor() {
|
|
46682
46692
|
this.i18n = inject(I18nService);
|
|
46683
46693
|
this.orgService = inject(OrgService);
|
|
@@ -46686,6 +46696,9 @@ class CreateOrgModalComponent {
|
|
|
46686
46696
|
this.errors = inject(ValtechErrorService);
|
|
46687
46697
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
46688
46698
|
this.i18nNamespace = DEFAULT_NAMESPACE$h;
|
|
46699
|
+
this._isOpen = false;
|
|
46700
|
+
this.dismissed = new EventEmitter();
|
|
46701
|
+
this.created = new EventEmitter();
|
|
46689
46702
|
this._busy = signal(false);
|
|
46690
46703
|
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$h)) {
|
|
46691
46704
|
this.i18n.registerContent(DEFAULT_NAMESPACE$h, CREATE_ORG_MODAL_I18N);
|
|
@@ -46731,8 +46744,8 @@ class CreateOrgModalComponent {
|
|
|
46731
46744
|
duration: 3500,
|
|
46732
46745
|
});
|
|
46733
46746
|
await this.orgSwitch.switchTo(newOrg.id);
|
|
46734
|
-
this.
|
|
46735
|
-
this.
|
|
46747
|
+
this.created.emit(newOrg);
|
|
46748
|
+
this.dismissed.emit();
|
|
46736
46749
|
}
|
|
46737
46750
|
catch (err) {
|
|
46738
46751
|
this.errors.handle(err, { i18nNamespace: this.i18nNamespace });
|
|
@@ -46742,7 +46755,10 @@ class CreateOrgModalComponent {
|
|
|
46742
46755
|
}
|
|
46743
46756
|
}
|
|
46744
46757
|
dismiss() {
|
|
46745
|
-
this.
|
|
46758
|
+
this.dismissed.emit();
|
|
46759
|
+
}
|
|
46760
|
+
close() {
|
|
46761
|
+
this.dismissed.emit();
|
|
46746
46762
|
}
|
|
46747
46763
|
buildFormMeta() {
|
|
46748
46764
|
const nameField = {
|
|
@@ -46822,71 +46838,81 @@ class CreateOrgModalComponent {
|
|
|
46822
46838
|
return this.i18n.t(key, this.i18nNamespace);
|
|
46823
46839
|
}
|
|
46824
46840
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CreateOrgModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
46825
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: CreateOrgModalComponent, isStandalone: true, selector: "val-create-org-modal", inputs: {
|
|
46826
|
-
<ion-
|
|
46827
|
-
<
|
|
46828
|
-
<ion-
|
|
46829
|
-
<ion-
|
|
46830
|
-
<
|
|
46831
|
-
|
|
46832
|
-
|
|
46833
|
-
|
|
46834
|
-
|
|
46835
|
-
|
|
46836
|
-
|
|
46837
|
-
|
|
46838
|
-
|
|
46839
|
-
|
|
46840
|
-
|
|
46841
|
-
|
|
46842
|
-
|
|
46843
|
-
|
|
46844
|
-
|
|
46845
|
-
|
|
46846
|
-
|
|
46847
|
-
|
|
46848
|
-
|
|
46849
|
-
|
|
46850
|
-
|
|
46851
|
-
|
|
46852
|
-
|
|
46841
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: CreateOrgModalComponent, isStandalone: true, selector: "val-create-org-modal", inputs: { i18nNamespace: "i18nNamespace", isOpen: "isOpen" }, outputs: { dismissed: "dismissed", created: "created" }, ngImport: i0, template: `
|
|
46842
|
+
<ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
|
|
46843
|
+
<ng-template>
|
|
46844
|
+
<ion-header>
|
|
46845
|
+
<ion-toolbar>
|
|
46846
|
+
<ion-buttons slot="end">
|
|
46847
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
|
|
46848
|
+
<strong>{{ t('close') }}</strong>
|
|
46849
|
+
</ion-button>
|
|
46850
|
+
</ion-buttons>
|
|
46851
|
+
</ion-toolbar>
|
|
46852
|
+
</ion-header>
|
|
46853
|
+
<ion-content class="ion-padding">
|
|
46854
|
+
<val-display [props]="{ content: t('pageTitle'), size: 'small', color: 'dark' }" />
|
|
46855
|
+
<val-title
|
|
46856
|
+
[props]="{
|
|
46857
|
+
content: t('pageDescription'),
|
|
46858
|
+
size: 'large',
|
|
46859
|
+
color: '',
|
|
46860
|
+
bold: false,
|
|
46861
|
+
}"
|
|
46862
|
+
/>
|
|
46863
|
+
<val-form [props]="formMeta()" (onSubmit)="onSubmit($event)" />
|
|
46864
|
+
<div class="modal-close-bottom">
|
|
46865
|
+
<ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
|
|
46866
|
+
{{ t('close') }}
|
|
46867
|
+
</ion-button>
|
|
46868
|
+
</div>
|
|
46869
|
+
</ion-content>
|
|
46870
|
+
</ng-template>
|
|
46871
|
+
</ion-modal>
|
|
46872
|
+
`, isInline: true, styles: [".modal-close-bottom{margin-top:16px}\n"], dependencies: [{ kind: "component", type: IonModal, selector: "ion-modal" }, { 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: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
|
|
46853
46873
|
}
|
|
46854
46874
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CreateOrgModalComponent, decorators: [{
|
|
46855
46875
|
type: Component,
|
|
46856
|
-
args: [{ selector: 'val-create-org-modal', standalone: true, imports: [IonHeader, IonToolbar, IonButtons, IonButton, IonContent, FormComponent, DisplayComponent, TitleComponent], template: `
|
|
46857
|
-
<ion-
|
|
46858
|
-
<
|
|
46859
|
-
<ion-
|
|
46860
|
-
<ion-
|
|
46861
|
-
<
|
|
46862
|
-
|
|
46863
|
-
|
|
46864
|
-
|
|
46865
|
-
|
|
46866
|
-
|
|
46867
|
-
|
|
46868
|
-
|
|
46869
|
-
|
|
46870
|
-
|
|
46871
|
-
|
|
46872
|
-
|
|
46873
|
-
|
|
46874
|
-
|
|
46875
|
-
|
|
46876
|
-
|
|
46877
|
-
|
|
46878
|
-
|
|
46879
|
-
|
|
46880
|
-
|
|
46881
|
-
|
|
46882
|
-
|
|
46876
|
+
args: [{ selector: 'val-create-org-modal', standalone: true, imports: [IonModal, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, FormComponent, DisplayComponent, TitleComponent], template: `
|
|
46877
|
+
<ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
|
|
46878
|
+
<ng-template>
|
|
46879
|
+
<ion-header>
|
|
46880
|
+
<ion-toolbar>
|
|
46881
|
+
<ion-buttons slot="end">
|
|
46882
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
|
|
46883
|
+
<strong>{{ t('close') }}</strong>
|
|
46884
|
+
</ion-button>
|
|
46885
|
+
</ion-buttons>
|
|
46886
|
+
</ion-toolbar>
|
|
46887
|
+
</ion-header>
|
|
46888
|
+
<ion-content class="ion-padding">
|
|
46889
|
+
<val-display [props]="{ content: t('pageTitle'), size: 'small', color: 'dark' }" />
|
|
46890
|
+
<val-title
|
|
46891
|
+
[props]="{
|
|
46892
|
+
content: t('pageDescription'),
|
|
46893
|
+
size: 'large',
|
|
46894
|
+
color: '',
|
|
46895
|
+
bold: false,
|
|
46896
|
+
}"
|
|
46897
|
+
/>
|
|
46898
|
+
<val-form [props]="formMeta()" (onSubmit)="onSubmit($event)" />
|
|
46899
|
+
<div class="modal-close-bottom">
|
|
46900
|
+
<ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
|
|
46901
|
+
{{ t('close') }}
|
|
46902
|
+
</ion-button>
|
|
46903
|
+
</div>
|
|
46904
|
+
</ion-content>
|
|
46905
|
+
</ng-template>
|
|
46906
|
+
</ion-modal>
|
|
46883
46907
|
`, styles: [".modal-close-bottom{margin-top:16px}\n"] }]
|
|
46884
|
-
}], ctorParameters: () => [], propDecorators: {
|
|
46885
|
-
type: Input
|
|
46886
|
-
}], onSuccess: [{
|
|
46908
|
+
}], ctorParameters: () => [], propDecorators: { i18nNamespace: [{
|
|
46887
46909
|
type: Input
|
|
46888
|
-
}],
|
|
46910
|
+
}], isOpen: [{
|
|
46889
46911
|
type: Input
|
|
46912
|
+
}], dismissed: [{
|
|
46913
|
+
type: Output
|
|
46914
|
+
}], created: [{
|
|
46915
|
+
type: Output
|
|
46890
46916
|
}] } });
|
|
46891
46917
|
|
|
46892
46918
|
/**
|
|
@@ -46949,6 +46975,21 @@ const DEFAULT_NAMESPACE$g = 'Settings.DeleteAccount';
|
|
|
46949
46975
|
* proveyó el namespace `Settings.DeleteAccount`.
|
|
46950
46976
|
*/
|
|
46951
46977
|
class DeleteAccountModalComponent {
|
|
46978
|
+
set isOpen(value) {
|
|
46979
|
+
const opening = value && !this._isOpen;
|
|
46980
|
+
this._isOpen = value;
|
|
46981
|
+
if (opening) {
|
|
46982
|
+
this.hasPassword.set(null);
|
|
46983
|
+
this.codeSent.set(false);
|
|
46984
|
+
this.formState.set(ComponentStates.ENABLED);
|
|
46985
|
+
firstValueFrom(this.auth.getProfile())
|
|
46986
|
+
.then(profile => this.hasPassword.set(profile.hasPassword ?? true))
|
|
46987
|
+
.catch(() => this.hasPassword.set(true));
|
|
46988
|
+
}
|
|
46989
|
+
}
|
|
46990
|
+
get isOpen() {
|
|
46991
|
+
return this._isOpen;
|
|
46992
|
+
}
|
|
46952
46993
|
constructor() {
|
|
46953
46994
|
this.auth = inject(AuthService);
|
|
46954
46995
|
this.errors = inject(ValtechErrorService);
|
|
@@ -46956,6 +46997,8 @@ class DeleteAccountModalComponent {
|
|
|
46956
46997
|
this.toast = inject(ToastService);
|
|
46957
46998
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
46958
46999
|
this.i18nNamespace = DEFAULT_NAMESPACE$g;
|
|
47000
|
+
this._isOpen = false;
|
|
47001
|
+
this.dismissed = new EventEmitter();
|
|
46959
47002
|
this.hasPassword = signal(null);
|
|
46960
47003
|
this.codeSent = signal(false);
|
|
46961
47004
|
this.formState = signal(ComponentStates.ENABLED);
|
|
@@ -47026,11 +47069,6 @@ class DeleteAccountModalComponent {
|
|
|
47026
47069
|
this.i18n.registerContent(DEFAULT_NAMESPACE$g, DELETE_ACCOUNT_MODAL_I18N);
|
|
47027
47070
|
}
|
|
47028
47071
|
}
|
|
47029
|
-
ngOnInit() {
|
|
47030
|
-
firstValueFrom(this.auth.getProfile())
|
|
47031
|
-
.then(profile => this.hasPassword.set(profile.hasPassword ?? true))
|
|
47032
|
-
.catch(() => this.hasPassword.set(true));
|
|
47033
|
-
}
|
|
47034
47072
|
submitBtn(token, textKey) {
|
|
47035
47073
|
return {
|
|
47036
47074
|
text: this.t(textKey),
|
|
@@ -47045,7 +47083,7 @@ class DeleteAccountModalComponent {
|
|
|
47045
47083
|
};
|
|
47046
47084
|
}
|
|
47047
47085
|
close() {
|
|
47048
|
-
this.
|
|
47086
|
+
this.dismissed.emit();
|
|
47049
47087
|
}
|
|
47050
47088
|
async onPasswordSubmit(event) {
|
|
47051
47089
|
const password = event.fields['password'];
|
|
@@ -47105,89 +47143,99 @@ class DeleteAccountModalComponent {
|
|
|
47105
47143
|
}
|
|
47106
47144
|
}
|
|
47107
47145
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeleteAccountModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
47108
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DeleteAccountModalComponent, isStandalone: true, selector: "val-delete-account-modal", inputs: {
|
|
47109
|
-
<ion-
|
|
47110
|
-
<
|
|
47111
|
-
<ion-
|
|
47112
|
-
<ion-
|
|
47113
|
-
<
|
|
47114
|
-
|
|
47115
|
-
|
|
47116
|
-
|
|
47117
|
-
|
|
47146
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DeleteAccountModalComponent, isStandalone: true, selector: "val-delete-account-modal", inputs: { i18nNamespace: "i18nNamespace", isOpen: "isOpen" }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
|
|
47147
|
+
<ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
|
|
47148
|
+
<ng-template>
|
|
47149
|
+
<ion-header>
|
|
47150
|
+
<ion-toolbar>
|
|
47151
|
+
<ion-buttons slot="end">
|
|
47152
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="close()">
|
|
47153
|
+
<strong>{{ t('close') }}</strong>
|
|
47154
|
+
</ion-button>
|
|
47155
|
+
</ion-buttons>
|
|
47156
|
+
</ion-toolbar>
|
|
47157
|
+
</ion-header>
|
|
47118
47158
|
|
|
47119
|
-
|
|
47120
|
-
|
|
47121
|
-
|
|
47122
|
-
|
|
47123
|
-
|
|
47124
|
-
|
|
47125
|
-
|
|
47126
|
-
|
|
47127
|
-
|
|
47128
|
-
|
|
47129
|
-
|
|
47130
|
-
|
|
47131
|
-
|
|
47132
|
-
|
|
47133
|
-
|
|
47134
|
-
|
|
47135
|
-
|
|
47136
|
-
|
|
47137
|
-
|
|
47138
|
-
|
|
47139
|
-
|
|
47140
|
-
|
|
47141
|
-
|
|
47142
|
-
|
|
47143
|
-
|
|
47144
|
-
|
|
47145
|
-
|
|
47159
|
+
<ion-content class="ion-padding">
|
|
47160
|
+
<section class="modal-form-section">
|
|
47161
|
+
@if (hasPassword() === null) {
|
|
47162
|
+
<div class="spinner-center">
|
|
47163
|
+
<ion-spinner name="crescent" />
|
|
47164
|
+
</div>
|
|
47165
|
+
} @else if (hasPassword()) {
|
|
47166
|
+
<val-form [props]="passwordFormProps()" (onSubmit)="onPasswordSubmit($event)" />
|
|
47167
|
+
} @else if (!codeSent()) {
|
|
47168
|
+
<val-form [props]="sendCodeFormProps()" (onSubmit)="onSendCode()" />
|
|
47169
|
+
} @else {
|
|
47170
|
+
<val-form [props]="codeFormProps()" (onSubmit)="onCodeSubmit($event)" />
|
|
47171
|
+
<div class="resend-row">
|
|
47172
|
+
<ion-button
|
|
47173
|
+
fill="clear"
|
|
47174
|
+
color="medium"
|
|
47175
|
+
size="small"
|
|
47176
|
+
[disabled]="formState() === 'WORKING'"
|
|
47177
|
+
(click)="onResendCode()"
|
|
47178
|
+
>
|
|
47179
|
+
{{ t('resend') }}
|
|
47180
|
+
</ion-button>
|
|
47181
|
+
</div>
|
|
47182
|
+
}
|
|
47183
|
+
</section>
|
|
47184
|
+
</ion-content>
|
|
47185
|
+
</ng-template>
|
|
47186
|
+
</ion-modal>
|
|
47187
|
+
`, isInline: true, styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.resend-row{display:flex;justify-content:center;margin-top:-8px}.spinner-center{display:flex;justify-content:center;padding-top:32px}\n"], dependencies: [{ kind: "component", type: IonModal, selector: "ion-modal" }, { 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: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
|
|
47146
47188
|
}
|
|
47147
47189
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeleteAccountModalComponent, decorators: [{
|
|
47148
47190
|
type: Component,
|
|
47149
|
-
args: [{ selector: 'val-delete-account-modal', standalone: true, imports: [IonHeader, IonToolbar, IonButtons, IonButton, IonContent, IonSpinner, FormComponent], template: `
|
|
47150
|
-
<ion-
|
|
47151
|
-
<
|
|
47152
|
-
<ion-
|
|
47153
|
-
<ion-
|
|
47154
|
-
<
|
|
47155
|
-
|
|
47156
|
-
|
|
47157
|
-
|
|
47158
|
-
|
|
47191
|
+
args: [{ selector: 'val-delete-account-modal', standalone: true, imports: [IonModal, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, IonSpinner, FormComponent], template: `
|
|
47192
|
+
<ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
|
|
47193
|
+
<ng-template>
|
|
47194
|
+
<ion-header>
|
|
47195
|
+
<ion-toolbar>
|
|
47196
|
+
<ion-buttons slot="end">
|
|
47197
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="close()">
|
|
47198
|
+
<strong>{{ t('close') }}</strong>
|
|
47199
|
+
</ion-button>
|
|
47200
|
+
</ion-buttons>
|
|
47201
|
+
</ion-toolbar>
|
|
47202
|
+
</ion-header>
|
|
47159
47203
|
|
|
47160
|
-
|
|
47161
|
-
|
|
47162
|
-
|
|
47163
|
-
|
|
47164
|
-
|
|
47165
|
-
|
|
47166
|
-
|
|
47167
|
-
|
|
47168
|
-
|
|
47169
|
-
|
|
47170
|
-
|
|
47171
|
-
|
|
47172
|
-
|
|
47173
|
-
|
|
47174
|
-
|
|
47175
|
-
|
|
47176
|
-
|
|
47177
|
-
|
|
47178
|
-
|
|
47179
|
-
|
|
47180
|
-
|
|
47181
|
-
|
|
47182
|
-
|
|
47183
|
-
|
|
47184
|
-
|
|
47185
|
-
|
|
47204
|
+
<ion-content class="ion-padding">
|
|
47205
|
+
<section class="modal-form-section">
|
|
47206
|
+
@if (hasPassword() === null) {
|
|
47207
|
+
<div class="spinner-center">
|
|
47208
|
+
<ion-spinner name="crescent" />
|
|
47209
|
+
</div>
|
|
47210
|
+
} @else if (hasPassword()) {
|
|
47211
|
+
<val-form [props]="passwordFormProps()" (onSubmit)="onPasswordSubmit($event)" />
|
|
47212
|
+
} @else if (!codeSent()) {
|
|
47213
|
+
<val-form [props]="sendCodeFormProps()" (onSubmit)="onSendCode()" />
|
|
47214
|
+
} @else {
|
|
47215
|
+
<val-form [props]="codeFormProps()" (onSubmit)="onCodeSubmit($event)" />
|
|
47216
|
+
<div class="resend-row">
|
|
47217
|
+
<ion-button
|
|
47218
|
+
fill="clear"
|
|
47219
|
+
color="medium"
|
|
47220
|
+
size="small"
|
|
47221
|
+
[disabled]="formState() === 'WORKING'"
|
|
47222
|
+
(click)="onResendCode()"
|
|
47223
|
+
>
|
|
47224
|
+
{{ t('resend') }}
|
|
47225
|
+
</ion-button>
|
|
47226
|
+
</div>
|
|
47227
|
+
}
|
|
47228
|
+
</section>
|
|
47229
|
+
</ion-content>
|
|
47230
|
+
</ng-template>
|
|
47231
|
+
</ion-modal>
|
|
47186
47232
|
`, styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.resend-row{display:flex;justify-content:center;margin-top:-8px}.spinner-center{display:flex;justify-content:center;padding-top:32px}\n"] }]
|
|
47187
|
-
}], ctorParameters: () => [], propDecorators: {
|
|
47233
|
+
}], ctorParameters: () => [], propDecorators: { i18nNamespace: [{
|
|
47188
47234
|
type: Input
|
|
47189
|
-
}],
|
|
47235
|
+
}], isOpen: [{
|
|
47190
47236
|
type: Input
|
|
47237
|
+
}], dismissed: [{
|
|
47238
|
+
type: Output
|
|
47191
47239
|
}] } });
|
|
47192
47240
|
|
|
47193
47241
|
/**
|
|
@@ -47878,6 +47926,8 @@ class AccountViewComponent {
|
|
|
47878
47926
|
retrying: this.orgsLoading(),
|
|
47879
47927
|
});
|
|
47880
47928
|
});
|
|
47929
|
+
this.createOrgOpen = signal(false);
|
|
47930
|
+
this.deleteAccountOpen = signal(false);
|
|
47881
47931
|
this.pendingInvites = signal([]);
|
|
47882
47932
|
this.pendingInvitesLoading = signal(false);
|
|
47883
47933
|
this.inviteAccepting = signal(null);
|
|
@@ -47991,40 +48041,18 @@ class AccountViewComponent {
|
|
|
47991
48041
|
});
|
|
47992
48042
|
}
|
|
47993
48043
|
onDeleteAccount() {
|
|
47994
|
-
|
|
47995
|
-
component: DeleteAccountModalComponent,
|
|
47996
|
-
componentProps: {
|
|
47997
|
-
i18nNamespace: 'Settings.DeleteAccount',
|
|
47998
|
-
},
|
|
47999
|
-
breakpoints: {
|
|
48000
|
-
breakpoints: [0, 0.85],
|
|
48001
|
-
initialBreakpoint: 0.85,
|
|
48002
|
-
showHandle: true,
|
|
48003
|
-
},
|
|
48004
|
-
backdropDismiss: false,
|
|
48005
|
-
});
|
|
48044
|
+
this.deleteAccountOpen.set(true);
|
|
48006
48045
|
this.resolvedConfig().onAccountDeleted?.();
|
|
48007
48046
|
}
|
|
48008
48047
|
onManageOrg() {
|
|
48009
48048
|
this.nav.navigateByUrl(this.resolvedConfig().manageOrgRoute);
|
|
48010
48049
|
}
|
|
48011
48050
|
onNewOrg() {
|
|
48012
|
-
|
|
48013
|
-
|
|
48014
|
-
|
|
48015
|
-
|
|
48016
|
-
|
|
48017
|
-
this.loadOrgs();
|
|
48018
|
-
this.resolvedConfig().onOrgCreated?.(newOrg);
|
|
48019
|
-
},
|
|
48020
|
-
},
|
|
48021
|
-
breakpoints: {
|
|
48022
|
-
breakpoints: [0, 0.92],
|
|
48023
|
-
initialBreakpoint: 0.92,
|
|
48024
|
-
showHandle: true,
|
|
48025
|
-
},
|
|
48026
|
-
backdropDismiss: true,
|
|
48027
|
-
});
|
|
48051
|
+
this.createOrgOpen.set(true);
|
|
48052
|
+
}
|
|
48053
|
+
onOrgCreated(newOrg) {
|
|
48054
|
+
this.loadOrgs();
|
|
48055
|
+
this.resolvedConfig().onOrgCreated?.(newOrg);
|
|
48028
48056
|
}
|
|
48029
48057
|
onMoreInfo() {
|
|
48030
48058
|
void this.modalService.open({
|
|
@@ -48314,8 +48342,19 @@ class AccountViewComponent {
|
|
|
48314
48342
|
</div>
|
|
48315
48343
|
</section>
|
|
48316
48344
|
}
|
|
48345
|
+
<val-create-org-modal
|
|
48346
|
+
[isOpen]="createOrgOpen()"
|
|
48347
|
+
[i18nNamespace]="'CreateOrgModal'"
|
|
48348
|
+
(dismissed)="createOrgOpen.set(false)"
|
|
48349
|
+
(created)="onOrgCreated($event)"
|
|
48350
|
+
/>
|
|
48351
|
+
<val-delete-account-modal
|
|
48352
|
+
[isOpen]="deleteAccountOpen()"
|
|
48353
|
+
[i18nNamespace]="'Settings.DeleteAccount'"
|
|
48354
|
+
(dismissed)="deleteAccountOpen.set(false)"
|
|
48355
|
+
/>
|
|
48317
48356
|
</div>
|
|
48318
|
-
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.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))}.settings-section val-title{display:block;margin-bottom:12px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;flex-wrap:wrap;gap:8px}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.org-new-cta-card{display:block;margin-top:12px}.orgs-empty-card{background:var(--ion-color-secondary-tint, rgba(var(--ion-color-secondary-rgb, 130, 101, 208), .2));border-radius:18px;padding:18px 16px 12px;display:flex;flex-direction:column;gap:8px}.orgs-empty-card__main{display:flex;align-items:center;gap:10px}.orgs-empty-card__icon{font-size:20px;flex-shrink:0;color:var(--ion-color-dark)}.orgs-empty-card__text{font-size:.875rem;color:var(--ion-color-dark)}.orgs-empty-card__link{background:none;border:none;padding:0;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);text-align:left;text-decoration:underline;text-underline-offset:2px;font-family:inherit}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1.5px solid transparent;cursor:pointer;transition:background .15s,border-color .15s}.org-card:active{opacity:.75}.org-card--active{border-color:var(--ion-color-primary);background:color-mix(in srgb,var(--ion-color-primary) 8%,transparent)}:host-context(body.dark) .org-card,:host-context(html.ion-palette-dark) .org-card,:host-context([data-theme=\"dark\"]) .org-card{background:#ffffff0d}:host-context(body.dark) .org-card--active,:host-context(html.ion-palette-dark) .org-card--active,:host-context([data-theme=\"dark\"]) .org-card--active{background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent)}.org-card__icon{width:40px;height:40px;border-radius:50%;background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20px;color:var(--ion-color-primary)}.org-card__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-card__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-card__meta{display:flex;flex-direction:column;gap:1px;margin-top:3px}.org-card__meta-item{font-size:.78rem;color:var(--ion-color-dark)}.org-card__meta-label{font-weight:600;color:var(--ion-color-dark);margin-right:4px}.org-card__end{display:flex;align-items:center;gap:8px;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: "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: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: InvitationCardComponent, selector: "val-invitation-card", inputs: ["props"], outputs: ["onAccept", "onDecline"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { 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: "pipe", type: TitleCasePipe, name: "titlecase" }] }); }
|
|
48357
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.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))}.settings-section val-title{display:block;margin-bottom:12px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;flex-wrap:wrap;gap:8px}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.org-new-cta-card{display:block;margin-top:12px}.orgs-empty-card{background:var(--ion-color-secondary-tint, rgba(var(--ion-color-secondary-rgb, 130, 101, 208), .2));border-radius:18px;padding:18px 16px 12px;display:flex;flex-direction:column;gap:8px}.orgs-empty-card__main{display:flex;align-items:center;gap:10px}.orgs-empty-card__icon{font-size:20px;flex-shrink:0;color:var(--ion-color-dark)}.orgs-empty-card__text{font-size:.875rem;color:var(--ion-color-dark)}.orgs-empty-card__link{background:none;border:none;padding:0;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);text-align:left;text-decoration:underline;text-underline-offset:2px;font-family:inherit}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1.5px solid transparent;cursor:pointer;transition:background .15s,border-color .15s}.org-card:active{opacity:.75}.org-card--active{border-color:var(--ion-color-primary);background:color-mix(in srgb,var(--ion-color-primary) 8%,transparent)}:host-context(body.dark) .org-card,:host-context(html.ion-palette-dark) .org-card,:host-context([data-theme=\"dark\"]) .org-card{background:#ffffff0d}:host-context(body.dark) .org-card--active,:host-context(html.ion-palette-dark) .org-card--active,:host-context([data-theme=\"dark\"]) .org-card--active{background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent)}.org-card__icon{width:40px;height:40px;border-radius:50%;background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20px;color:var(--ion-color-primary)}.org-card__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-card__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-card__meta{display:flex;flex-direction:column;gap:1px;margin-top:3px}.org-card__meta-item{font-size:.78rem;color:var(--ion-color-dark)}.org-card__meta-label{font-weight:600;color:var(--ion-color-dark);margin-right:4px}.org-card__end{display:flex;align-items:center;gap:8px;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: "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: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: InvitationCardComponent, selector: "val-invitation-card", inputs: ["props"], outputs: ["onAccept", "onDecline"] }, { kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: DeleteAccountModalComponent, selector: "val-delete-account-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { 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: "pipe", type: TitleCasePipe, name: "titlecase" }] }); }
|
|
48319
48358
|
}
|
|
48320
48359
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AccountViewComponent, decorators: [{
|
|
48321
48360
|
type: Component,
|
|
@@ -48328,6 +48367,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
48328
48367
|
ButtonComponent,
|
|
48329
48368
|
CtaCardComponent,
|
|
48330
48369
|
InvitationCardComponent,
|
|
48370
|
+
CreateOrgModalComponent,
|
|
48371
|
+
DeleteAccountModalComponent,
|
|
48331
48372
|
IonIcon,
|
|
48332
48373
|
IonButton,
|
|
48333
48374
|
TitleCasePipe,
|
|
@@ -48501,6 +48542,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
48501
48542
|
</div>
|
|
48502
48543
|
</section>
|
|
48503
48544
|
}
|
|
48545
|
+
<val-create-org-modal
|
|
48546
|
+
[isOpen]="createOrgOpen()"
|
|
48547
|
+
[i18nNamespace]="'CreateOrgModal'"
|
|
48548
|
+
(dismissed)="createOrgOpen.set(false)"
|
|
48549
|
+
(created)="onOrgCreated($event)"
|
|
48550
|
+
/>
|
|
48551
|
+
<val-delete-account-modal
|
|
48552
|
+
[isOpen]="deleteAccountOpen()"
|
|
48553
|
+
[i18nNamespace]="'Settings.DeleteAccount'"
|
|
48554
|
+
(dismissed)="deleteAccountOpen.set(false)"
|
|
48555
|
+
/>
|
|
48504
48556
|
</div>
|
|
48505
48557
|
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.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))}.settings-section val-title{display:block;margin-bottom:12px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;flex-wrap:wrap;gap:8px}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.org-new-cta-card{display:block;margin-top:12px}.orgs-empty-card{background:var(--ion-color-secondary-tint, rgba(var(--ion-color-secondary-rgb, 130, 101, 208), .2));border-radius:18px;padding:18px 16px 12px;display:flex;flex-direction:column;gap:8px}.orgs-empty-card__main{display:flex;align-items:center;gap:10px}.orgs-empty-card__icon{font-size:20px;flex-shrink:0;color:var(--ion-color-dark)}.orgs-empty-card__text{font-size:.875rem;color:var(--ion-color-dark)}.orgs-empty-card__link{background:none;border:none;padding:0;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);text-align:left;text-decoration:underline;text-underline-offset:2px;font-family:inherit}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1.5px solid transparent;cursor:pointer;transition:background .15s,border-color .15s}.org-card:active{opacity:.75}.org-card--active{border-color:var(--ion-color-primary);background:color-mix(in srgb,var(--ion-color-primary) 8%,transparent)}:host-context(body.dark) .org-card,:host-context(html.ion-palette-dark) .org-card,:host-context([data-theme=\"dark\"]) .org-card{background:#ffffff0d}:host-context(body.dark) .org-card--active,:host-context(html.ion-palette-dark) .org-card--active,:host-context([data-theme=\"dark\"]) .org-card--active{background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent)}.org-card__icon{width:40px;height:40px;border-radius:50%;background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20px;color:var(--ion-color-primary)}.org-card__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-card__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-card__meta{display:flex;flex-direction:column;gap:1px;margin-top:3px}.org-card__meta-item{font-size:.78rem;color:var(--ion-color-dark)}.org-card__meta-label{font-weight:600;color:var(--ion-color-dark);margin-right:4px}.org-card__end{display:flex;align-items:center;gap:8px;flex-shrink:0}\n"] }]
|
|
48506
48558
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
@@ -50004,246 +50056,938 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
50004
50056
|
}] } });
|
|
50005
50057
|
|
|
50006
50058
|
/**
|
|
50007
|
-
* Defaults i18n (es/en) embebidos en `val-
|
|
50008
|
-
*
|
|
50009
|
-
*
|
|
50010
|
-
*
|
|
50059
|
+
* Defaults i18n (es/en) embebidos en `val-permissions-view`. Auto-registrados en
|
|
50060
|
+
* el constructor del componente si el consumer no proveyó el namespace
|
|
50061
|
+
* (`Settings.Permissions` por default). SOLO incluye las keys que la vista usa.
|
|
50062
|
+
*
|
|
50063
|
+
* Portado EXACTO de `PERMISSIONS_I18N` (showcase) bajo el proceso de ADR-021.
|
|
50011
50064
|
*/
|
|
50012
|
-
const
|
|
50065
|
+
const PERMISSIONS_VIEW_I18N = {
|
|
50013
50066
|
es: {
|
|
50014
|
-
|
|
50015
|
-
|
|
50016
|
-
|
|
50017
|
-
|
|
50018
|
-
|
|
50019
|
-
|
|
50020
|
-
|
|
50067
|
+
pageTitle: 'Permisos',
|
|
50068
|
+
pageDescription: 'Roles y permisos de esta app en la organización activa',
|
|
50069
|
+
systemRolesTitle: 'Roles del sistema',
|
|
50070
|
+
systemRolesHint: 'Predefinidos por la plataforma. No editables.',
|
|
50071
|
+
customRolesTitle: 'Roles personalizados',
|
|
50072
|
+
customRolesHint: 'Definidos por tu organización.',
|
|
50073
|
+
systemBadge: 'Sistema',
|
|
50074
|
+
noCustomRoles: 'Esta organización no tiene roles personalizados.',
|
|
50075
|
+
noPermissions: 'Sin permisos asignados.',
|
|
50076
|
+
// Grupos de alcance
|
|
50077
|
+
groupApp: 'Permisos de la app',
|
|
50078
|
+
groupOrg: 'Permisos de la organización',
|
|
50079
|
+
groupPlatform: 'Permisos de plataforma',
|
|
50080
|
+
groupOther: 'Otras apps',
|
|
50081
|
+
// Roles del sistema
|
|
50082
|
+
roleViewer: 'Visor',
|
|
50083
|
+
roleEditor: 'Editor',
|
|
50084
|
+
roleAdmin: 'Administrador',
|
|
50085
|
+
roleSuperAdmin: 'Super Administrador',
|
|
50086
|
+
roleBingoAdmin: 'Bingo · Administrador',
|
|
50087
|
+
roleBingoTesorero: 'Bingo · Tesorero',
|
|
50088
|
+
roleBingoTaquilla: 'Bingo · Taquilla',
|
|
50089
|
+
roleBingoOperator: 'Bingo · Operador',
|
|
50090
|
+
// Apps
|
|
50091
|
+
app_showcase: 'Showcase',
|
|
50092
|
+
app_bingo: 'Bingo',
|
|
50093
|
+
// Recursos
|
|
50094
|
+
permUsers: 'Usuarios',
|
|
50095
|
+
permRoles: 'Roles',
|
|
50096
|
+
permRbac: 'Control de acceso',
|
|
50097
|
+
permApps: 'Apps',
|
|
50098
|
+
'permApi-keys': 'API keys',
|
|
50099
|
+
'permEmail-logs': 'Logs de correo',
|
|
50100
|
+
'permSms-logs': 'Logs de SMS',
|
|
50101
|
+
permDocuments: 'Documentos',
|
|
50102
|
+
permMedia: 'Media',
|
|
50103
|
+
permTemplates: 'Plantillas',
|
|
50104
|
+
permRequests: 'Solicitudes',
|
|
50105
|
+
permBingo: 'Bingo',
|
|
50106
|
+
// Acciones
|
|
50107
|
+
permRead: 'Leer',
|
|
50108
|
+
permWrite: 'Escribir',
|
|
50109
|
+
permCreate: 'Crear',
|
|
50110
|
+
permDelete: 'Eliminar',
|
|
50111
|
+
permManage: 'Gestionar',
|
|
50112
|
+
permEdit: 'Editar',
|
|
50113
|
+
permSell: 'Vender',
|
|
50114
|
+
permCheckin: 'Canjear',
|
|
50115
|
+
permSettle: 'Rendir',
|
|
50116
|
+
permExport: 'Exportar',
|
|
50117
|
+
permOperate: 'Operar',
|
|
50118
|
+
permAll: 'Todo',
|
|
50119
|
+
// Loading / error
|
|
50120
|
+
errorTitle: 'Error al cargar permisos',
|
|
50121
|
+
retry: 'Reintentar',
|
|
50021
50122
|
},
|
|
50022
50123
|
en: {
|
|
50023
|
-
|
|
50024
|
-
|
|
50025
|
-
|
|
50026
|
-
|
|
50027
|
-
|
|
50028
|
-
|
|
50029
|
-
|
|
50124
|
+
pageTitle: 'Permissions',
|
|
50125
|
+
pageDescription: 'Roles and permissions of this app in the active organization',
|
|
50126
|
+
systemRolesTitle: 'System roles',
|
|
50127
|
+
systemRolesHint: 'Defined by the platform. Not editable.',
|
|
50128
|
+
customRolesTitle: 'Custom roles',
|
|
50129
|
+
customRolesHint: 'Defined by your organization.',
|
|
50130
|
+
systemBadge: 'System',
|
|
50131
|
+
noCustomRoles: 'This organization has no custom roles.',
|
|
50132
|
+
noPermissions: 'No permissions assigned.',
|
|
50133
|
+
groupApp: 'App permissions',
|
|
50134
|
+
groupOrg: 'Organization permissions',
|
|
50135
|
+
groupPlatform: 'Platform permissions',
|
|
50136
|
+
groupOther: 'Other apps',
|
|
50137
|
+
roleViewer: 'Viewer',
|
|
50138
|
+
roleEditor: 'Editor',
|
|
50139
|
+
roleAdmin: 'Administrator',
|
|
50140
|
+
roleSuperAdmin: 'Super Administrator',
|
|
50141
|
+
roleBingoAdmin: 'Bingo · Administrator',
|
|
50142
|
+
roleBingoTesorero: 'Bingo · Treasurer',
|
|
50143
|
+
roleBingoTaquilla: 'Bingo · Box office',
|
|
50144
|
+
roleBingoOperator: 'Bingo · Operator',
|
|
50145
|
+
app_showcase: 'Showcase',
|
|
50146
|
+
app_bingo: 'Bingo',
|
|
50147
|
+
permUsers: 'Users',
|
|
50148
|
+
permRoles: 'Roles',
|
|
50149
|
+
permRbac: 'Access control',
|
|
50150
|
+
permApps: 'Apps',
|
|
50151
|
+
'permApi-keys': 'API keys',
|
|
50152
|
+
'permEmail-logs': 'Email logs',
|
|
50153
|
+
'permSms-logs': 'SMS logs',
|
|
50154
|
+
permDocuments: 'Documents',
|
|
50155
|
+
permMedia: 'Media',
|
|
50156
|
+
permTemplates: 'Templates',
|
|
50157
|
+
permRequests: 'Requests',
|
|
50158
|
+
permBingo: 'Bingo',
|
|
50159
|
+
permRead: 'Read',
|
|
50160
|
+
permWrite: 'Write',
|
|
50161
|
+
permCreate: 'Create',
|
|
50162
|
+
permDelete: 'Delete',
|
|
50163
|
+
permManage: 'Manage',
|
|
50164
|
+
permEdit: 'Edit',
|
|
50165
|
+
permSell: 'Sell',
|
|
50166
|
+
permCheckin: 'Check-in',
|
|
50167
|
+
permSettle: 'Settle',
|
|
50168
|
+
permExport: 'Export',
|
|
50169
|
+
permOperate: 'Operate',
|
|
50170
|
+
permAll: 'All',
|
|
50171
|
+
errorTitle: 'Error loading permissions',
|
|
50172
|
+
retry: 'Retry',
|
|
50030
50173
|
},
|
|
50031
50174
|
};
|
|
50032
50175
|
|
|
50033
|
-
const DEFAULT_NAMESPACE$9 = 'Settings.
|
|
50176
|
+
const DEFAULT_NAMESPACE$9 = 'Settings.Permissions';
|
|
50034
50177
|
/**
|
|
50035
|
-
* `val-
|
|
50036
|
-
*
|
|
50037
|
-
*
|
|
50178
|
+
* `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
|
|
50179
|
+
* Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
|
|
50180
|
+
* roles de plataforma) y los roles personalizados de la org activa, con el
|
|
50181
|
+
* catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
|
|
50182
|
+
* Promovida desde `showcase` bajo el proceso de ADR-021.
|
|
50038
50183
|
*
|
|
50039
|
-
*
|
|
50040
|
-
*
|
|
50041
|
-
*
|
|
50042
|
-
*
|
|
50184
|
+
* **RBAC interno** (como `organization-view`): lee la org activa vía
|
|
50185
|
+
* `AuthService.user().activeOrgId`. La vista NO muta nada — el control de quién
|
|
50186
|
+
* puede verla es responsabilidad del guard de ruta del consumer; no hay gating
|
|
50187
|
+
* fino interno ni se expone como config.
|
|
50043
50188
|
*
|
|
50044
|
-
*
|
|
50045
|
-
*
|
|
50046
|
-
*
|
|
50189
|
+
* Datos: `forkJoin` de `OrgService.listOrgRoles(orgId)` (roles de la org) +
|
|
50190
|
+
* `PermissionCatalogService.getCatalog(orgId)` (catálogo + appId actual). El
|
|
50191
|
+
* catálogo define el alcance (`scope`/`appId`) de cada recurso para agrupar.
|
|
50047
50192
|
*
|
|
50048
|
-
*
|
|
50049
|
-
* `
|
|
50193
|
+
* NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
|
|
50194
|
+
* inyecta `{ optional: true }` solo para leer el route data de config. Auto-registra
|
|
50195
|
+
* sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
|
|
50196
|
+
* `Settings.Permissions`).
|
|
50050
50197
|
*/
|
|
50051
|
-
class
|
|
50198
|
+
class PermissionsViewComponent {
|
|
50199
|
+
/** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
|
|
50200
|
+
get ns() {
|
|
50201
|
+
return this.resolvedConfig().i18nNamespace;
|
|
50202
|
+
}
|
|
50052
50203
|
constructor() {
|
|
50053
50204
|
this.i18n = inject(I18nService);
|
|
50054
|
-
|
|
50055
|
-
this.
|
|
50056
|
-
|
|
50057
|
-
this.
|
|
50058
|
-
this.
|
|
50059
|
-
|
|
50060
|
-
|
|
50061
|
-
|
|
50062
|
-
|
|
50063
|
-
|
|
50064
|
-
|
|
50205
|
+
this.orgService = inject(OrgService);
|
|
50206
|
+
this.catalog = inject(PermissionCatalogService);
|
|
50207
|
+
this.auth = inject(AuthService);
|
|
50208
|
+
this.nav = inject(NavigationService);
|
|
50209
|
+
this.route = inject(ActivatedRoute, { optional: true });
|
|
50210
|
+
/**
|
|
50211
|
+
* Cuando `true`, la vista está embebida en un modal — suprime el back-header
|
|
50212
|
+
* (lo gestiona el modal padre). Default `false` (uso como ruta).
|
|
50213
|
+
*/
|
|
50214
|
+
this.isModal = false;
|
|
50215
|
+
this.resolvedConfig = computed(() => {
|
|
50216
|
+
const fromRoute = (this.route?.snapshot.data['permissionsConfig'] ?? {});
|
|
50217
|
+
const merged = { ...fromRoute, ...(this.config ?? {}) };
|
|
50218
|
+
return {
|
|
50219
|
+
showSystemRoles: merged.showSystemRoles ?? true,
|
|
50220
|
+
showCustomRoles: merged.showCustomRoles ?? true,
|
|
50221
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$9,
|
|
50222
|
+
};
|
|
50065
50223
|
});
|
|
50066
|
-
this.
|
|
50067
|
-
|
|
50068
|
-
|
|
50224
|
+
this.loading = signal(false);
|
|
50225
|
+
this.loadError = signal(null);
|
|
50226
|
+
this.roles = signal([]);
|
|
50227
|
+
this.systemRoles = computed(() => this.roles().filter(r => r.isSystem));
|
|
50228
|
+
this.customRoles = computed(() => this.roles().filter(r => !r.isSystem));
|
|
50229
|
+
this.activeOrgId = computed(() => {
|
|
50230
|
+
const u = this.auth.user();
|
|
50231
|
+
return u?.activeOrgId ?? u?.activeOrg ?? '';
|
|
50232
|
+
});
|
|
50233
|
+
this.retryButtonProps = computed(() => ({
|
|
50234
|
+
text: this.tt('retry'),
|
|
50235
|
+
color: 'dark',
|
|
50236
|
+
fill: 'outline',
|
|
50237
|
+
size: 'default',
|
|
50238
|
+
type: 'button',
|
|
50239
|
+
state: this.loading() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
|
|
50240
|
+
}));
|
|
50241
|
+
const ns = this.ns;
|
|
50242
|
+
if (!this.i18n.hasNamespace(ns)) {
|
|
50243
|
+
this.i18n.registerContent(ns, PERMISSIONS_VIEW_I18N);
|
|
50069
50244
|
}
|
|
50245
|
+
this.load();
|
|
50246
|
+
connectPageRefresh(() => this.load());
|
|
50070
50247
|
}
|
|
50071
50248
|
ngOnInit() {
|
|
50072
|
-
if (!this.
|
|
50073
|
-
this.
|
|
50249
|
+
if (!this.isModal) {
|
|
50250
|
+
this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
|
|
50074
50251
|
}
|
|
50075
50252
|
}
|
|
50076
|
-
|
|
50077
|
-
const
|
|
50078
|
-
if (!
|
|
50253
|
+
load() {
|
|
50254
|
+
const orgId = this.activeOrgId();
|
|
50255
|
+
if (!orgId)
|
|
50079
50256
|
return;
|
|
50080
|
-
this.
|
|
50081
|
-
this.
|
|
50082
|
-
|
|
50083
|
-
|
|
50084
|
-
|
|
50085
|
-
|
|
50086
|
-
|
|
50087
|
-
|
|
50257
|
+
this.loading.set(true);
|
|
50258
|
+
this.loadError.set(null);
|
|
50259
|
+
forkJoin({
|
|
50260
|
+
roles: this.orgService.listOrgRoles(orgId),
|
|
50261
|
+
catalog: this.catalog.getCatalog(orgId),
|
|
50262
|
+
}).subscribe({
|
|
50263
|
+
next: ({ roles: rawRoles, catalog }) => {
|
|
50264
|
+
const byResource = new Map();
|
|
50265
|
+
for (const r of catalog.permissions)
|
|
50266
|
+
byResource.set(r.resource, r);
|
|
50267
|
+
const currentAppId = catalog.appId;
|
|
50268
|
+
// Labeler con la SSOT del backend (labels del catálogo) — reemplaza el
|
|
50269
|
+
// mapa hardcodeado `perm*`. Locale del momento de carga (igual que antes:
|
|
50270
|
+
// los labels se hornean en el RoleView; cambio de idioma requiere reload).
|
|
50271
|
+
const labeler = createPermissionLabeler(catalog, this.i18n.lang(), {
|
|
50272
|
+
allLabel: this.tt('permAll'),
|
|
50273
|
+
});
|
|
50274
|
+
this.roles.set((rawRoles ?? []).map((r) => this.buildRoleView({
|
|
50275
|
+
id: r.id,
|
|
50276
|
+
name: r.name,
|
|
50277
|
+
description: r.description,
|
|
50278
|
+
permissions: r.permissions ?? [],
|
|
50279
|
+
isSystem: r.isSystem ?? false,
|
|
50280
|
+
}, byResource, currentAppId, labeler)));
|
|
50281
|
+
this.loading.set(false);
|
|
50282
|
+
},
|
|
50283
|
+
error: err => {
|
|
50284
|
+
this.loadError.set(err);
|
|
50285
|
+
this.loading.set(false);
|
|
50286
|
+
},
|
|
50287
|
+
});
|
|
50088
50288
|
}
|
|
50089
|
-
|
|
50090
|
-
|
|
50091
|
-
|
|
50092
|
-
|
|
50093
|
-
|
|
50094
|
-
|
|
50095
|
-
|
|
50096
|
-
|
|
50097
|
-
|
|
50098
|
-
|
|
50099
|
-
|
|
50100
|
-
|
|
50101
|
-
|
|
50102
|
-
|
|
50103
|
-
|
|
50104
|
-
|
|
50105
|
-
|
|
50106
|
-
|
|
50107
|
-
|
|
50108
|
-
|
|
50109
|
-
|
|
50110
|
-
|
|
50111
|
-
|
|
50112
|
-
|
|
50113
|
-
|
|
50114
|
-
|
|
50115
|
-
|
|
50116
|
-
|
|
50117
|
-
|
|
50118
|
-
|
|
50119
|
-
|
|
50120
|
-
|
|
50121
|
-
|
|
50122
|
-
|
|
50123
|
-
|
|
50124
|
-
|
|
50125
|
-
|
|
50126
|
-
|
|
50127
|
-
|
|
50289
|
+
buildRoleView(role, byResource, currentAppId, labeler) {
|
|
50290
|
+
const appPerms = [];
|
|
50291
|
+
const orgPerms = [];
|
|
50292
|
+
const otherByApp = new Map();
|
|
50293
|
+
for (const perm of role.permissions) {
|
|
50294
|
+
const [resource] = perm.split(':');
|
|
50295
|
+
const entry = byResource.get(resource);
|
|
50296
|
+
// Sin entrada en el catálogo (el backend omite los recursos internal para
|
|
50297
|
+
// no-platform-admins) o entrada internal → NUNCA se muestra a nivel de
|
|
50298
|
+
// producto. Defensa en profundidad junto al filtro del backend (ADR-024).
|
|
50299
|
+
// Nota: el rol puede traer el permiso aunque el recurso esté filtrado del
|
|
50300
|
+
// catálogo (ej. admin tiene templates:*); por eso se descarta lo no resuelto.
|
|
50301
|
+
if (!entry || entry.scope === 'internal') {
|
|
50302
|
+
continue;
|
|
50303
|
+
}
|
|
50304
|
+
const label = labeler(perm);
|
|
50305
|
+
if (entry.scope === 'app') {
|
|
50306
|
+
if (entry.appId === currentAppId) {
|
|
50307
|
+
appPerms.push(label);
|
|
50308
|
+
}
|
|
50309
|
+
else {
|
|
50310
|
+
const appId = entry.appId ?? '?';
|
|
50311
|
+
const arr = otherByApp.get(appId) ?? [];
|
|
50312
|
+
arr.push(label);
|
|
50313
|
+
otherByApp.set(appId, arr);
|
|
50314
|
+
}
|
|
50315
|
+
}
|
|
50316
|
+
else {
|
|
50317
|
+
// scope === 'org' → gestión de la organización del cliente
|
|
50318
|
+
orgPerms.push(label);
|
|
50319
|
+
}
|
|
50320
|
+
}
|
|
50321
|
+
const groups = [];
|
|
50322
|
+
if (appPerms.length) {
|
|
50323
|
+
groups.push({ key: 'app', label: this.tt('groupApp'), perms: appPerms });
|
|
50324
|
+
}
|
|
50325
|
+
if (orgPerms.length) {
|
|
50326
|
+
groups.push({
|
|
50327
|
+
key: 'org',
|
|
50328
|
+
label: this.tt('groupOrg'),
|
|
50329
|
+
perms: orgPerms,
|
|
50330
|
+
});
|
|
50331
|
+
}
|
|
50332
|
+
for (const [appId, perms] of otherByApp) {
|
|
50333
|
+
groups.push({
|
|
50334
|
+
key: `other:${appId}`,
|
|
50335
|
+
label: `${this.tt('groupOther')}: ${this.appLabel(appId)}`,
|
|
50336
|
+
perms,
|
|
50337
|
+
});
|
|
50338
|
+
}
|
|
50339
|
+
return {
|
|
50340
|
+
id: role.id,
|
|
50341
|
+
name: role.name,
|
|
50342
|
+
description: role.description,
|
|
50343
|
+
isSystem: role.isSystem,
|
|
50344
|
+
groups,
|
|
50345
|
+
hasPerms: groups.length > 0,
|
|
50346
|
+
};
|
|
50347
|
+
}
|
|
50348
|
+
tt(key) {
|
|
50349
|
+
return this.i18n.t(key, this.ns);
|
|
50350
|
+
}
|
|
50351
|
+
appLabel(appId) {
|
|
50352
|
+
const key = `app_${appId}`;
|
|
50353
|
+
const label = this.tt(key);
|
|
50354
|
+
return label === key ? appId : label;
|
|
50355
|
+
}
|
|
50356
|
+
roleLabel(name) {
|
|
50357
|
+
const map = {
|
|
50358
|
+
viewer: 'roleViewer',
|
|
50359
|
+
editor: 'roleEditor',
|
|
50360
|
+
admin: 'roleAdmin',
|
|
50361
|
+
super_admin: 'roleSuperAdmin',
|
|
50362
|
+
bingo_admin: 'roleBingoAdmin',
|
|
50363
|
+
bingo_tesorero: 'roleBingoTesorero',
|
|
50364
|
+
bingo_taquilla: 'roleBingoTaquilla',
|
|
50365
|
+
bingo_operator: 'roleBingoOperator',
|
|
50366
|
+
};
|
|
50367
|
+
return this.tt(map[name] ?? name);
|
|
50368
|
+
}
|
|
50369
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
50370
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config", isModal: "isModal" }, ngImport: i0, template: `
|
|
50371
|
+
<div class="page">
|
|
50372
|
+
<header class="page-header">
|
|
50373
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
|
|
50374
|
+
<val-title
|
|
50128
50375
|
[props]="{
|
|
50129
|
-
|
|
50130
|
-
|
|
50131
|
-
color: 'medium',
|
|
50376
|
+
size: 'large',
|
|
50377
|
+
color: 'dark',
|
|
50132
50378
|
bold: false,
|
|
50379
|
+
content: tt('pageDescription'),
|
|
50133
50380
|
}"
|
|
50134
50381
|
/>
|
|
50382
|
+
</header>
|
|
50383
|
+
|
|
50384
|
+
@if (loading()) {
|
|
50385
|
+
<div class="spinner-row">
|
|
50386
|
+
<ion-spinner name="crescent" />
|
|
50387
|
+
</div>
|
|
50388
|
+
} @else if (loadError()) {
|
|
50389
|
+
<div class="error-row">
|
|
50390
|
+
<val-text
|
|
50391
|
+
[props]="{
|
|
50392
|
+
content: tt('errorTitle'),
|
|
50393
|
+
size: 'medium',
|
|
50394
|
+
color: 'medium',
|
|
50395
|
+
bold: false,
|
|
50396
|
+
}"
|
|
50397
|
+
/>
|
|
50398
|
+
<val-button [props]="retryButtonProps()" (click)="load()" />
|
|
50399
|
+
</div>
|
|
50135
50400
|
} @else {
|
|
50136
|
-
|
|
50137
|
-
|
|
50138
|
-
<
|
|
50139
|
-
|
|
50140
|
-
|
|
50141
|
-
|
|
50142
|
-
|
|
50143
|
-
|
|
50144
|
-
|
|
50145
|
-
|
|
50146
|
-
|
|
50401
|
+
@if (resolvedConfig().showSystemRoles) {
|
|
50402
|
+
<section class="perm-section">
|
|
50403
|
+
<div class="perm-section__header">
|
|
50404
|
+
<val-title
|
|
50405
|
+
[props]="{
|
|
50406
|
+
size: 'medium',
|
|
50407
|
+
color: 'dark',
|
|
50408
|
+
bold: true,
|
|
50409
|
+
content: tt('systemRolesTitle'),
|
|
50410
|
+
}"
|
|
50411
|
+
/>
|
|
50412
|
+
<val-text
|
|
50413
|
+
[props]="{
|
|
50414
|
+
content: tt('systemRolesHint'),
|
|
50415
|
+
size: 'medium',
|
|
50416
|
+
color: 'medium',
|
|
50417
|
+
bold: false,
|
|
50418
|
+
}"
|
|
50419
|
+
/>
|
|
50420
|
+
</div>
|
|
50421
|
+
<div class="role-list">
|
|
50422
|
+
@for (role of systemRoles(); track role.id) {
|
|
50423
|
+
<div class="role-card">
|
|
50424
|
+
<div class="role-card__header">
|
|
50425
|
+
<span class="role-card__name">{{ roleLabel(role.name) }}</span>
|
|
50426
|
+
<val-pill
|
|
50427
|
+
[props]="{
|
|
50428
|
+
label: tt('systemBadge'),
|
|
50429
|
+
color: 'primary',
|
|
50430
|
+
weight: 'bold',
|
|
50431
|
+
}"
|
|
50432
|
+
/>
|
|
50433
|
+
</div>
|
|
50434
|
+
@if (role.description) {
|
|
50435
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
50436
|
+
}
|
|
50437
|
+
@if (!role.hasPerms) {
|
|
50438
|
+
<div class="perm-chips">
|
|
50439
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
50440
|
+
</div>
|
|
50441
|
+
} @else {
|
|
50442
|
+
@for (g of role.groups; track g.key) {
|
|
50443
|
+
<div class="perm-group">
|
|
50444
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
50445
|
+
<div class="perm-chips">
|
|
50446
|
+
@for (p of g.perms; track p) {
|
|
50447
|
+
<span class="perm-chip">{{ p }}</span>
|
|
50448
|
+
}
|
|
50449
|
+
</div>
|
|
50450
|
+
</div>
|
|
50451
|
+
}
|
|
50452
|
+
}
|
|
50453
|
+
</div>
|
|
50147
50454
|
}
|
|
50148
|
-
</
|
|
50149
|
-
|
|
50150
|
-
|
|
50151
|
-
}
|
|
50152
|
-
|
|
50153
|
-
<div class="transfer-confirm-row">
|
|
50154
|
-
<ion-button expand="block" color="dark" shape="round" [disabled]="!selectedId()" (click)="confirm()">
|
|
50155
|
-
{{ t('confirmCta') }}
|
|
50156
|
-
</ion-button>
|
|
50157
|
-
</div>
|
|
50455
|
+
</div>
|
|
50456
|
+
</section>
|
|
50457
|
+
}
|
|
50158
50458
|
|
|
50159
|
-
|
|
50160
|
-
|
|
50161
|
-
|
|
50162
|
-
|
|
50163
|
-
|
|
50164
|
-
|
|
50165
|
-
|
|
50459
|
+
@if (resolvedConfig().showCustomRoles) {
|
|
50460
|
+
<section class="perm-section">
|
|
50461
|
+
<div class="perm-section__header">
|
|
50462
|
+
<val-title
|
|
50463
|
+
[props]="{
|
|
50464
|
+
size: 'medium',
|
|
50465
|
+
color: 'dark',
|
|
50466
|
+
bold: true,
|
|
50467
|
+
content: tt('customRolesTitle'),
|
|
50468
|
+
}"
|
|
50469
|
+
/>
|
|
50470
|
+
<val-text
|
|
50471
|
+
[props]="{
|
|
50472
|
+
content: tt('customRolesHint'),
|
|
50473
|
+
size: 'medium',
|
|
50474
|
+
color: 'medium',
|
|
50475
|
+
bold: false,
|
|
50476
|
+
}"
|
|
50477
|
+
/>
|
|
50478
|
+
</div>
|
|
50479
|
+
@if (customRoles().length === 0) {
|
|
50480
|
+
<val-text
|
|
50481
|
+
[props]="{
|
|
50482
|
+
content: tt('noCustomRoles'),
|
|
50483
|
+
size: 'medium',
|
|
50484
|
+
color: 'medium',
|
|
50485
|
+
bold: false,
|
|
50486
|
+
}"
|
|
50487
|
+
/>
|
|
50488
|
+
} @else {
|
|
50489
|
+
<div class="role-list">
|
|
50490
|
+
@for (role of customRoles(); track role.id) {
|
|
50491
|
+
<div class="role-card">
|
|
50492
|
+
<div class="role-card__header">
|
|
50493
|
+
<span class="role-card__name">{{ role.name }}</span>
|
|
50494
|
+
</div>
|
|
50495
|
+
@if (role.description) {
|
|
50496
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
50497
|
+
}
|
|
50498
|
+
@if (!role.hasPerms) {
|
|
50499
|
+
<div class="perm-chips">
|
|
50500
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
50501
|
+
</div>
|
|
50502
|
+
} @else {
|
|
50503
|
+
@for (g of role.groups; track g.key) {
|
|
50504
|
+
<div class="perm-group">
|
|
50505
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
50506
|
+
<div class="perm-chips">
|
|
50507
|
+
@for (p of g.perms; track p) {
|
|
50508
|
+
<span class="perm-chip">{{ p }}</span>
|
|
50509
|
+
}
|
|
50510
|
+
</div>
|
|
50511
|
+
</div>
|
|
50512
|
+
}
|
|
50513
|
+
}
|
|
50514
|
+
</div>
|
|
50515
|
+
}
|
|
50516
|
+
</div>
|
|
50517
|
+
}
|
|
50518
|
+
</section>
|
|
50519
|
+
}
|
|
50520
|
+
}
|
|
50521
|
+
</div>
|
|
50522
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:.9375rem;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:.8125rem;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:.75rem;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"], dependencies: [{ kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
|
|
50166
50523
|
}
|
|
50167
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
50524
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, decorators: [{
|
|
50168
50525
|
type: Component,
|
|
50169
|
-
args: [{ selector: 'val-
|
|
50170
|
-
|
|
50171
|
-
|
|
50172
|
-
|
|
50173
|
-
|
|
50174
|
-
IonContent,
|
|
50175
|
-
SearchbarComponent,
|
|
50176
|
-
DisplayComponent,
|
|
50177
|
-
TextComponent,
|
|
50178
|
-
TitleComponent,
|
|
50179
|
-
], template: `
|
|
50180
|
-
<ion-header>
|
|
50181
|
-
<ion-toolbar>
|
|
50182
|
-
<ion-buttons slot="end">
|
|
50183
|
-
<ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
|
|
50184
|
-
<strong>{{ t('close') }}</strong>
|
|
50185
|
-
</ion-button>
|
|
50186
|
-
</ion-buttons>
|
|
50187
|
-
</ion-toolbar>
|
|
50188
|
-
</ion-header>
|
|
50189
|
-
|
|
50190
|
-
<ion-content class="ion-padding">
|
|
50191
|
-
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
50192
|
-
<val-title
|
|
50193
|
-
[props]="{
|
|
50194
|
-
content: t('hint'),
|
|
50195
|
-
size: 'large',
|
|
50196
|
-
color: '',
|
|
50197
|
-
bold: false,
|
|
50198
|
-
}"
|
|
50199
|
-
/>
|
|
50200
|
-
|
|
50201
|
-
<val-searchbar
|
|
50202
|
-
[props]="{ placeholder: t('searchPlaceholder'), debounce: 150 }"
|
|
50203
|
-
(filterEvent)="query.set($event)"
|
|
50204
|
-
/>
|
|
50205
|
-
|
|
50206
|
-
@if (members.length === 0) {
|
|
50207
|
-
<val-text
|
|
50208
|
-
[props]="{
|
|
50209
|
-
content: t('empty'),
|
|
50210
|
-
size: 'medium',
|
|
50211
|
-
color: 'medium',
|
|
50212
|
-
bold: false,
|
|
50213
|
-
}"
|
|
50214
|
-
/>
|
|
50215
|
-
} @else if (filtered().length === 0) {
|
|
50216
|
-
<val-text
|
|
50526
|
+
args: [{ selector: 'val-permissions-view', standalone: true, imports: [IonSpinner, DisplayComponent, PillComponent, TitleComponent, TextComponent, ButtonComponent], template: `
|
|
50527
|
+
<div class="page">
|
|
50528
|
+
<header class="page-header">
|
|
50529
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
|
|
50530
|
+
<val-title
|
|
50217
50531
|
[props]="{
|
|
50218
|
-
|
|
50219
|
-
|
|
50220
|
-
color: 'medium',
|
|
50532
|
+
size: 'large',
|
|
50533
|
+
color: 'dark',
|
|
50221
50534
|
bold: false,
|
|
50535
|
+
content: tt('pageDescription'),
|
|
50222
50536
|
}"
|
|
50223
50537
|
/>
|
|
50538
|
+
</header>
|
|
50539
|
+
|
|
50540
|
+
@if (loading()) {
|
|
50541
|
+
<div class="spinner-row">
|
|
50542
|
+
<ion-spinner name="crescent" />
|
|
50543
|
+
</div>
|
|
50544
|
+
} @else if (loadError()) {
|
|
50545
|
+
<div class="error-row">
|
|
50546
|
+
<val-text
|
|
50547
|
+
[props]="{
|
|
50548
|
+
content: tt('errorTitle'),
|
|
50549
|
+
size: 'medium',
|
|
50550
|
+
color: 'medium',
|
|
50551
|
+
bold: false,
|
|
50552
|
+
}"
|
|
50553
|
+
/>
|
|
50554
|
+
<val-button [props]="retryButtonProps()" (click)="load()" />
|
|
50555
|
+
</div>
|
|
50224
50556
|
} @else {
|
|
50225
|
-
|
|
50226
|
-
|
|
50227
|
-
<
|
|
50228
|
-
|
|
50229
|
-
|
|
50230
|
-
|
|
50231
|
-
|
|
50232
|
-
|
|
50233
|
-
|
|
50234
|
-
|
|
50235
|
-
|
|
50557
|
+
@if (resolvedConfig().showSystemRoles) {
|
|
50558
|
+
<section class="perm-section">
|
|
50559
|
+
<div class="perm-section__header">
|
|
50560
|
+
<val-title
|
|
50561
|
+
[props]="{
|
|
50562
|
+
size: 'medium',
|
|
50563
|
+
color: 'dark',
|
|
50564
|
+
bold: true,
|
|
50565
|
+
content: tt('systemRolesTitle'),
|
|
50566
|
+
}"
|
|
50567
|
+
/>
|
|
50568
|
+
<val-text
|
|
50569
|
+
[props]="{
|
|
50570
|
+
content: tt('systemRolesHint'),
|
|
50571
|
+
size: 'medium',
|
|
50572
|
+
color: 'medium',
|
|
50573
|
+
bold: false,
|
|
50574
|
+
}"
|
|
50575
|
+
/>
|
|
50576
|
+
</div>
|
|
50577
|
+
<div class="role-list">
|
|
50578
|
+
@for (role of systemRoles(); track role.id) {
|
|
50579
|
+
<div class="role-card">
|
|
50580
|
+
<div class="role-card__header">
|
|
50581
|
+
<span class="role-card__name">{{ roleLabel(role.name) }}</span>
|
|
50582
|
+
<val-pill
|
|
50583
|
+
[props]="{
|
|
50584
|
+
label: tt('systemBadge'),
|
|
50585
|
+
color: 'primary',
|
|
50586
|
+
weight: 'bold',
|
|
50587
|
+
}"
|
|
50588
|
+
/>
|
|
50589
|
+
</div>
|
|
50590
|
+
@if (role.description) {
|
|
50591
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
50592
|
+
}
|
|
50593
|
+
@if (!role.hasPerms) {
|
|
50594
|
+
<div class="perm-chips">
|
|
50595
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
50596
|
+
</div>
|
|
50597
|
+
} @else {
|
|
50598
|
+
@for (g of role.groups; track g.key) {
|
|
50599
|
+
<div class="perm-group">
|
|
50600
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
50601
|
+
<div class="perm-chips">
|
|
50602
|
+
@for (p of g.perms; track p) {
|
|
50603
|
+
<span class="perm-chip">{{ p }}</span>
|
|
50604
|
+
}
|
|
50605
|
+
</div>
|
|
50606
|
+
</div>
|
|
50607
|
+
}
|
|
50608
|
+
}
|
|
50609
|
+
</div>
|
|
50236
50610
|
}
|
|
50237
|
-
</
|
|
50238
|
-
|
|
50239
|
-
|
|
50611
|
+
</div>
|
|
50612
|
+
</section>
|
|
50613
|
+
}
|
|
50614
|
+
|
|
50615
|
+
@if (resolvedConfig().showCustomRoles) {
|
|
50616
|
+
<section class="perm-section">
|
|
50617
|
+
<div class="perm-section__header">
|
|
50618
|
+
<val-title
|
|
50619
|
+
[props]="{
|
|
50620
|
+
size: 'medium',
|
|
50621
|
+
color: 'dark',
|
|
50622
|
+
bold: true,
|
|
50623
|
+
content: tt('customRolesTitle'),
|
|
50624
|
+
}"
|
|
50625
|
+
/>
|
|
50626
|
+
<val-text
|
|
50627
|
+
[props]="{
|
|
50628
|
+
content: tt('customRolesHint'),
|
|
50629
|
+
size: 'medium',
|
|
50630
|
+
color: 'medium',
|
|
50631
|
+
bold: false,
|
|
50632
|
+
}"
|
|
50633
|
+
/>
|
|
50634
|
+
</div>
|
|
50635
|
+
@if (customRoles().length === 0) {
|
|
50636
|
+
<val-text
|
|
50637
|
+
[props]="{
|
|
50638
|
+
content: tt('noCustomRoles'),
|
|
50639
|
+
size: 'medium',
|
|
50640
|
+
color: 'medium',
|
|
50641
|
+
bold: false,
|
|
50642
|
+
}"
|
|
50643
|
+
/>
|
|
50644
|
+
} @else {
|
|
50645
|
+
<div class="role-list">
|
|
50646
|
+
@for (role of customRoles(); track role.id) {
|
|
50647
|
+
<div class="role-card">
|
|
50648
|
+
<div class="role-card__header">
|
|
50649
|
+
<span class="role-card__name">{{ role.name }}</span>
|
|
50650
|
+
</div>
|
|
50651
|
+
@if (role.description) {
|
|
50652
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
50653
|
+
}
|
|
50654
|
+
@if (!role.hasPerms) {
|
|
50655
|
+
<div class="perm-chips">
|
|
50656
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
50657
|
+
</div>
|
|
50658
|
+
} @else {
|
|
50659
|
+
@for (g of role.groups; track g.key) {
|
|
50660
|
+
<div class="perm-group">
|
|
50661
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
50662
|
+
<div class="perm-chips">
|
|
50663
|
+
@for (p of g.perms; track p) {
|
|
50664
|
+
<span class="perm-chip">{{ p }}</span>
|
|
50665
|
+
}
|
|
50666
|
+
</div>
|
|
50667
|
+
</div>
|
|
50668
|
+
}
|
|
50669
|
+
}
|
|
50670
|
+
</div>
|
|
50671
|
+
}
|
|
50672
|
+
</div>
|
|
50673
|
+
}
|
|
50674
|
+
</section>
|
|
50675
|
+
}
|
|
50240
50676
|
}
|
|
50677
|
+
</div>
|
|
50678
|
+
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:.9375rem;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:.8125rem;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:.75rem;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"] }]
|
|
50679
|
+
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
50680
|
+
type: Input
|
|
50681
|
+
}], isModal: [{
|
|
50682
|
+
type: Input
|
|
50683
|
+
}] } });
|
|
50241
50684
|
|
|
50242
|
-
|
|
50243
|
-
|
|
50244
|
-
|
|
50245
|
-
|
|
50246
|
-
|
|
50685
|
+
class PermissionsModalComponent {
|
|
50686
|
+
constructor() {
|
|
50687
|
+
this.i18n = inject(I18nService);
|
|
50688
|
+
this.isOpen = false;
|
|
50689
|
+
this.dismissed = new EventEmitter();
|
|
50690
|
+
}
|
|
50691
|
+
close() {
|
|
50692
|
+
this.dismissed.emit();
|
|
50693
|
+
}
|
|
50694
|
+
t(key) {
|
|
50695
|
+
return this.i18n.t(key, '_global');
|
|
50696
|
+
}
|
|
50697
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
50698
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: PermissionsModalComponent, isStandalone: true, selector: "val-permissions-modal", inputs: { isOpen: "isOpen", config: "config" }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
|
|
50699
|
+
<ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
|
|
50700
|
+
<ng-template>
|
|
50701
|
+
<ion-header>
|
|
50702
|
+
<ion-toolbar>
|
|
50703
|
+
<ion-buttons slot="end">
|
|
50704
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="close()">
|
|
50705
|
+
<strong>{{ t('close') }}</strong>
|
|
50706
|
+
</ion-button>
|
|
50707
|
+
</ion-buttons>
|
|
50708
|
+
</ion-toolbar>
|
|
50709
|
+
</ion-header>
|
|
50710
|
+
<ion-content class="ion-padding">
|
|
50711
|
+
<val-permissions-view [config]="config" [isModal]="true" />
|
|
50712
|
+
</ion-content>
|
|
50713
|
+
</ng-template>
|
|
50714
|
+
</ion-modal>
|
|
50715
|
+
`, isInline: true, dependencies: [{ kind: "component", type: IonModal, selector: "ion-modal" }, { 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: PermissionsViewComponent, selector: "val-permissions-view", inputs: ["config", "isModal"] }] }); }
|
|
50716
|
+
}
|
|
50717
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsModalComponent, decorators: [{
|
|
50718
|
+
type: Component,
|
|
50719
|
+
args: [{
|
|
50720
|
+
selector: 'val-permissions-modal',
|
|
50721
|
+
standalone: true,
|
|
50722
|
+
imports: [IonModal, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, PermissionsViewComponent],
|
|
50723
|
+
template: `
|
|
50724
|
+
<ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
|
|
50725
|
+
<ng-template>
|
|
50726
|
+
<ion-header>
|
|
50727
|
+
<ion-toolbar>
|
|
50728
|
+
<ion-buttons slot="end">
|
|
50729
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="close()">
|
|
50730
|
+
<strong>{{ t('close') }}</strong>
|
|
50731
|
+
</ion-button>
|
|
50732
|
+
</ion-buttons>
|
|
50733
|
+
</ion-toolbar>
|
|
50734
|
+
</ion-header>
|
|
50735
|
+
<ion-content class="ion-padding">
|
|
50736
|
+
<val-permissions-view [config]="config" [isModal]="true" />
|
|
50737
|
+
</ion-content>
|
|
50738
|
+
</ng-template>
|
|
50739
|
+
</ion-modal>
|
|
50740
|
+
`,
|
|
50741
|
+
}]
|
|
50742
|
+
}], propDecorators: { isOpen: [{
|
|
50743
|
+
type: Input
|
|
50744
|
+
}], config: [{
|
|
50745
|
+
type: Input
|
|
50746
|
+
}], dismissed: [{
|
|
50747
|
+
type: Output
|
|
50748
|
+
}] } });
|
|
50749
|
+
|
|
50750
|
+
/**
|
|
50751
|
+
* Defaults i18n (es/en) embebidos en `val-transfer-ownership-modal`.
|
|
50752
|
+
* Auto-registrados en el constructor del componente si el consumer no proveyó el
|
|
50753
|
+
* namespace (`Settings.TransferModal` por default). SOLO incluye las keys que el
|
|
50754
|
+
* modal usa.
|
|
50755
|
+
*/
|
|
50756
|
+
const TRANSFER_OWNERSHIP_MODAL_I18N = {
|
|
50757
|
+
es: {
|
|
50758
|
+
title: 'Transferir propiedad',
|
|
50759
|
+
hint: 'Selecciona el nuevo propietario de la organización.',
|
|
50760
|
+
searchPlaceholder: 'Buscar miembro...',
|
|
50761
|
+
empty: 'No hay otros miembros disponibles.',
|
|
50762
|
+
noResults: 'Sin resultados para',
|
|
50763
|
+
confirmCta: 'Confirmar transferencia',
|
|
50764
|
+
close: 'Cancelar',
|
|
50765
|
+
},
|
|
50766
|
+
en: {
|
|
50767
|
+
title: 'Transfer ownership',
|
|
50768
|
+
hint: 'Select the new owner of the organization.',
|
|
50769
|
+
searchPlaceholder: 'Search member...',
|
|
50770
|
+
empty: 'No other members available.',
|
|
50771
|
+
noResults: 'No results for',
|
|
50772
|
+
confirmCta: 'Confirm transfer',
|
|
50773
|
+
close: 'Cancel',
|
|
50774
|
+
},
|
|
50775
|
+
};
|
|
50776
|
+
|
|
50777
|
+
const DEFAULT_NAMESPACE$8 = 'Settings.TransferModal';
|
|
50778
|
+
/**
|
|
50779
|
+
* `val-transfer-ownership-modal` — selector de miembro para transferir la
|
|
50780
|
+
* propiedad de la organización. Promovido desde `showcase` bajo el proceso de
|
|
50781
|
+
* ADR-021.
|
|
50782
|
+
*
|
|
50783
|
+
* Recibe la lista de candidatos por `members` (todos excepto el owner actual),
|
|
50784
|
+
* permite filtrarlos con `val-searchbar` y, al confirmar, ejecuta el callback
|
|
50785
|
+
* `onConfirm(newOwnerId)`. La ejecución real de la transferencia (vía
|
|
50786
|
+
* `OrgService.transferOwnership`) queda en el consumer.
|
|
50787
|
+
*
|
|
50788
|
+
* Abierto vía `ModalService` — recibe `_modalRef` por `componentProps`. Header
|
|
50789
|
+
* canónico (Regla #5): sin `ion-title`, botón Cerrar texto en `slot=end`,
|
|
50790
|
+
* título/subtítulo en el body con `val-display`/`val-title`.
|
|
50791
|
+
*
|
|
50792
|
+
* Auto-registra sus defaults i18n (es/en) si el consumer no proveyó el namespace
|
|
50793
|
+
* `Settings.TransferModal`.
|
|
50794
|
+
*/
|
|
50795
|
+
class TransferOwnershipModalComponent {
|
|
50796
|
+
constructor() {
|
|
50797
|
+
this.i18n = inject(I18nService);
|
|
50798
|
+
/** Lista de miembros transferibles (todos excepto el owner actual). */
|
|
50799
|
+
this.members = [];
|
|
50800
|
+
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
50801
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$8;
|
|
50802
|
+
this.query = signal('');
|
|
50803
|
+
this.selectedId = signal('');
|
|
50804
|
+
this.filtered = computed(() => {
|
|
50805
|
+
const q = this.query().trim().toLowerCase();
|
|
50806
|
+
if (!q)
|
|
50807
|
+
return this.members;
|
|
50808
|
+
return this.members.filter(m => m.name?.toLowerCase().includes(q) || m.email?.toLowerCase().includes(q) || m.userId.toLowerCase().includes(q));
|
|
50809
|
+
});
|
|
50810
|
+
this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
|
|
50811
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
|
|
50812
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$8, TRANSFER_OWNERSHIP_MODAL_I18N);
|
|
50813
|
+
}
|
|
50814
|
+
}
|
|
50815
|
+
ngOnInit() {
|
|
50816
|
+
if (!this.i18n.hasNamespace(this.i18nNamespace)) {
|
|
50817
|
+
this.i18n.registerContent(this.i18nNamespace, TRANSFER_OWNERSHIP_MODAL_I18N);
|
|
50818
|
+
}
|
|
50819
|
+
}
|
|
50820
|
+
confirm() {
|
|
50821
|
+
const id = this.selectedId();
|
|
50822
|
+
if (!id)
|
|
50823
|
+
return;
|
|
50824
|
+
this.onConfirm?.(id);
|
|
50825
|
+
this.dismiss();
|
|
50826
|
+
}
|
|
50827
|
+
dismiss() {
|
|
50828
|
+
this._modalRef?.dismiss(null, 'cancel');
|
|
50829
|
+
}
|
|
50830
|
+
t(key) {
|
|
50831
|
+
return this.i18n.t(key, this.i18nNamespace);
|
|
50832
|
+
}
|
|
50833
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TransferOwnershipModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
50834
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TransferOwnershipModalComponent, isStandalone: true, selector: "val-transfer-ownership-modal", inputs: { members: "members", onConfirm: "onConfirm", _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
|
|
50835
|
+
<ion-header>
|
|
50836
|
+
<ion-toolbar>
|
|
50837
|
+
<ion-buttons slot="end">
|
|
50838
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
|
|
50839
|
+
<strong>{{ t('close') }}</strong>
|
|
50840
|
+
</ion-button>
|
|
50841
|
+
</ion-buttons>
|
|
50842
|
+
</ion-toolbar>
|
|
50843
|
+
</ion-header>
|
|
50844
|
+
|
|
50845
|
+
<ion-content class="ion-padding">
|
|
50846
|
+
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
50847
|
+
<val-title
|
|
50848
|
+
[props]="{
|
|
50849
|
+
content: t('hint'),
|
|
50850
|
+
size: 'large',
|
|
50851
|
+
color: '',
|
|
50852
|
+
bold: false,
|
|
50853
|
+
}"
|
|
50854
|
+
/>
|
|
50855
|
+
|
|
50856
|
+
<val-searchbar
|
|
50857
|
+
[props]="{ placeholder: t('searchPlaceholder'), debounce: 150 }"
|
|
50858
|
+
(filterEvent)="query.set($event)"
|
|
50859
|
+
/>
|
|
50860
|
+
|
|
50861
|
+
@if (members.length === 0) {
|
|
50862
|
+
<val-text
|
|
50863
|
+
[props]="{
|
|
50864
|
+
content: t('empty'),
|
|
50865
|
+
size: 'medium',
|
|
50866
|
+
color: 'medium',
|
|
50867
|
+
bold: false,
|
|
50868
|
+
}"
|
|
50869
|
+
/>
|
|
50870
|
+
} @else if (filtered().length === 0) {
|
|
50871
|
+
<val-text
|
|
50872
|
+
[props]="{
|
|
50873
|
+
content: noResultsLabel(),
|
|
50874
|
+
size: 'medium',
|
|
50875
|
+
color: 'medium',
|
|
50876
|
+
bold: false,
|
|
50877
|
+
}"
|
|
50878
|
+
/>
|
|
50879
|
+
} @else {
|
|
50880
|
+
<div class="transfer-member-list">
|
|
50881
|
+
@for (m of filtered(); track m.userId) {
|
|
50882
|
+
<button
|
|
50883
|
+
class="transfer-member-row"
|
|
50884
|
+
[class.transfer-member-row--selected]="selectedId() === m.userId"
|
|
50885
|
+
(click)="selectedId.set(m.userId)"
|
|
50886
|
+
type="button"
|
|
50887
|
+
>
|
|
50888
|
+
<span class="transfer-member-name">{{ m.name || m.email || m.userId }}</span>
|
|
50889
|
+
@if (selectedId() === m.userId) {
|
|
50890
|
+
<span class="transfer-member-check">✓</span>
|
|
50891
|
+
}
|
|
50892
|
+
</button>
|
|
50893
|
+
}
|
|
50894
|
+
</div>
|
|
50895
|
+
}
|
|
50896
|
+
|
|
50897
|
+
<div class="transfer-confirm-row">
|
|
50898
|
+
<ion-button expand="block" color="dark" shape="round" [disabled]="!selectedId()" (click)="confirm()">
|
|
50899
|
+
{{ t('confirmCta') }}
|
|
50900
|
+
</ion-button>
|
|
50901
|
+
</div>
|
|
50902
|
+
|
|
50903
|
+
<div class="modal-close-bottom">
|
|
50904
|
+
<ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
|
|
50905
|
+
{{ t('close') }}
|
|
50906
|
+
</ion-button>
|
|
50907
|
+
</div>
|
|
50908
|
+
</ion-content>
|
|
50909
|
+
`, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:16px}val-searchbar{display:block;margin-bottom:12px}.transfer-member-list{display:flex;flex-direction:column;gap:4px}.transfer-member-row{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-radius:10px;border:1.5px solid var(--ion-color-light);background:transparent;cursor:pointer;width:100%;text-align:left;font-size:15px;color:var(--ion-color-dark);transition:border-color .15s}.transfer-member-row--selected{border-color:var(--ion-color-dark);background:var(--ion-color-light)}.transfer-member-check{font-size:17px;color:var(--ion-color-dark)}.transfer-confirm-row{margin-top:24px}.modal-close-bottom{margin-top:12px}\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: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
|
|
50910
|
+
}
|
|
50911
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TransferOwnershipModalComponent, decorators: [{
|
|
50912
|
+
type: Component,
|
|
50913
|
+
args: [{ selector: 'val-transfer-ownership-modal', standalone: true, imports: [
|
|
50914
|
+
IonHeader,
|
|
50915
|
+
IonToolbar,
|
|
50916
|
+
IonButtons,
|
|
50917
|
+
IonButton,
|
|
50918
|
+
IonContent,
|
|
50919
|
+
SearchbarComponent,
|
|
50920
|
+
DisplayComponent,
|
|
50921
|
+
TextComponent,
|
|
50922
|
+
TitleComponent,
|
|
50923
|
+
], template: `
|
|
50924
|
+
<ion-header>
|
|
50925
|
+
<ion-toolbar>
|
|
50926
|
+
<ion-buttons slot="end">
|
|
50927
|
+
<ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
|
|
50928
|
+
<strong>{{ t('close') }}</strong>
|
|
50929
|
+
</ion-button>
|
|
50930
|
+
</ion-buttons>
|
|
50931
|
+
</ion-toolbar>
|
|
50932
|
+
</ion-header>
|
|
50933
|
+
|
|
50934
|
+
<ion-content class="ion-padding">
|
|
50935
|
+
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
50936
|
+
<val-title
|
|
50937
|
+
[props]="{
|
|
50938
|
+
content: t('hint'),
|
|
50939
|
+
size: 'large',
|
|
50940
|
+
color: '',
|
|
50941
|
+
bold: false,
|
|
50942
|
+
}"
|
|
50943
|
+
/>
|
|
50944
|
+
|
|
50945
|
+
<val-searchbar
|
|
50946
|
+
[props]="{ placeholder: t('searchPlaceholder'), debounce: 150 }"
|
|
50947
|
+
(filterEvent)="query.set($event)"
|
|
50948
|
+
/>
|
|
50949
|
+
|
|
50950
|
+
@if (members.length === 0) {
|
|
50951
|
+
<val-text
|
|
50952
|
+
[props]="{
|
|
50953
|
+
content: t('empty'),
|
|
50954
|
+
size: 'medium',
|
|
50955
|
+
color: 'medium',
|
|
50956
|
+
bold: false,
|
|
50957
|
+
}"
|
|
50958
|
+
/>
|
|
50959
|
+
} @else if (filtered().length === 0) {
|
|
50960
|
+
<val-text
|
|
50961
|
+
[props]="{
|
|
50962
|
+
content: noResultsLabel(),
|
|
50963
|
+
size: 'medium',
|
|
50964
|
+
color: 'medium',
|
|
50965
|
+
bold: false,
|
|
50966
|
+
}"
|
|
50967
|
+
/>
|
|
50968
|
+
} @else {
|
|
50969
|
+
<div class="transfer-member-list">
|
|
50970
|
+
@for (m of filtered(); track m.userId) {
|
|
50971
|
+
<button
|
|
50972
|
+
class="transfer-member-row"
|
|
50973
|
+
[class.transfer-member-row--selected]="selectedId() === m.userId"
|
|
50974
|
+
(click)="selectedId.set(m.userId)"
|
|
50975
|
+
type="button"
|
|
50976
|
+
>
|
|
50977
|
+
<span class="transfer-member-name">{{ m.name || m.email || m.userId }}</span>
|
|
50978
|
+
@if (selectedId() === m.userId) {
|
|
50979
|
+
<span class="transfer-member-check">✓</span>
|
|
50980
|
+
}
|
|
50981
|
+
</button>
|
|
50982
|
+
}
|
|
50983
|
+
</div>
|
|
50984
|
+
}
|
|
50985
|
+
|
|
50986
|
+
<div class="transfer-confirm-row">
|
|
50987
|
+
<ion-button expand="block" color="dark" shape="round" [disabled]="!selectedId()" (click)="confirm()">
|
|
50988
|
+
{{ t('confirmCta') }}
|
|
50989
|
+
</ion-button>
|
|
50990
|
+
</div>
|
|
50247
50991
|
|
|
50248
50992
|
<div class="modal-close-bottom">
|
|
50249
50993
|
<ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
|
|
@@ -50341,7 +51085,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
|
|
|
50341
51085
|
},
|
|
50342
51086
|
};
|
|
50343
51087
|
|
|
50344
|
-
const DEFAULT_NAMESPACE$
|
|
51088
|
+
const DEFAULT_NAMESPACE$7 = 'Settings.ImportModal';
|
|
50345
51089
|
addIcons({ cloudUploadOutline, documentTextOutline });
|
|
50346
51090
|
/**
|
|
50347
51091
|
* `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
|
|
@@ -50361,7 +51105,7 @@ class MemberImportModalComponent {
|
|
|
50361
51105
|
/** Nombres de roles disponibles (para el hint). Opcional. */
|
|
50362
51106
|
this.availableRoles = [];
|
|
50363
51107
|
/** Namespace i18n. */
|
|
50364
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
51108
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$7;
|
|
50365
51109
|
this.csv = '';
|
|
50366
51110
|
this.importMode = signal('file');
|
|
50367
51111
|
this.onConflictControl = new FormControl('assignRole');
|
|
@@ -50403,8 +51147,8 @@ class MemberImportModalComponent {
|
|
|
50403
51147
|
return this.t('summary');
|
|
50404
51148
|
return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
|
|
50405
51149
|
});
|
|
50406
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
50407
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
51150
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
|
|
51151
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$7, MEMBER_IMPORT_MODAL_I18N);
|
|
50408
51152
|
}
|
|
50409
51153
|
}
|
|
50410
51154
|
ngOnInit() {
|
|
@@ -50996,7 +51740,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
50996
51740
|
},
|
|
50997
51741
|
};
|
|
50998
51742
|
|
|
50999
|
-
const DEFAULT_NAMESPACE$
|
|
51743
|
+
const DEFAULT_NAMESPACE$6 = 'Settings.Organization';
|
|
51000
51744
|
const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
|
|
51001
51745
|
const DEFAULT_API_KEYS_ROUTE = '/app/settings/api-keys';
|
|
51002
51746
|
const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
|
|
@@ -51054,7 +51798,7 @@ class OrganizationViewComponent {
|
|
|
51054
51798
|
showInvite: merged.showInvite ?? true,
|
|
51055
51799
|
showTransferOwnership: merged.showTransferOwnership ?? true,
|
|
51056
51800
|
showLeave: merged.showLeave ?? true,
|
|
51057
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$
|
|
51801
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$6,
|
|
51058
51802
|
viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
|
|
51059
51803
|
apiKeysRoute: merged.apiKeysRoute ?? DEFAULT_API_KEYS_ROUTE,
|
|
51060
51804
|
leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
|
|
@@ -51067,6 +51811,7 @@ class OrganizationViewComponent {
|
|
|
51067
51811
|
this.org = signal(null);
|
|
51068
51812
|
this.loading = signal(false);
|
|
51069
51813
|
this.leaving = signal(false);
|
|
51814
|
+
this.permissionsOpen = signal(false);
|
|
51070
51815
|
this.orgLoadError = signal(null);
|
|
51071
51816
|
this.orgErrorState = computed(() => {
|
|
51072
51817
|
this.i18n.lang();
|
|
@@ -51365,7 +52110,7 @@ class OrganizationViewComponent {
|
|
|
51365
52110
|
});
|
|
51366
52111
|
}
|
|
51367
52112
|
onViewPermissions() {
|
|
51368
|
-
this.
|
|
52113
|
+
this.permissionsOpen.set(true);
|
|
51369
52114
|
}
|
|
51370
52115
|
onOpenTransfer() {
|
|
51371
52116
|
void this.modalService.openAdaptive({
|
|
@@ -51838,8 +52583,13 @@ class OrganizationViewComponent {
|
|
|
51838
52583
|
</div>
|
|
51839
52584
|
</section>
|
|
51840
52585
|
}
|
|
52586
|
+
<val-permissions-modal
|
|
52587
|
+
[isOpen]="permissionsOpen()"
|
|
52588
|
+
[config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
|
|
52589
|
+
(dismissed)="permissionsOpen.set(false)"
|
|
52590
|
+
/>
|
|
51841
52591
|
</div>
|
|
51842
|
-
`, 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}: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)}\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: 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"] }] }); }
|
|
52592
|
+
`, 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}: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)}\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: 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"] }] }); }
|
|
51843
52593
|
}
|
|
51844
52594
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
|
|
51845
52595
|
type: Component,
|
|
@@ -51852,6 +52602,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
51852
52602
|
TextComponent,
|
|
51853
52603
|
ButtonComponent,
|
|
51854
52604
|
MemberCardComponent,
|
|
52605
|
+
PermissionsModalComponent,
|
|
51855
52606
|
TransferOwnershipModalComponent,
|
|
51856
52607
|
MemberImportModalComponent,
|
|
51857
52608
|
OrgInfoSheetComponent,
|
|
@@ -51928,1618 +52679,1007 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
51928
52679
|
<val-title
|
|
51929
52680
|
[props]="{
|
|
51930
52681
|
size: 'medium',
|
|
51931
|
-
color: 'dark',
|
|
51932
|
-
bold: true,
|
|
51933
|
-
content: tt('membersTitle'),
|
|
51934
|
-
}"
|
|
51935
|
-
/>
|
|
51936
|
-
<div class="section-body">
|
|
51937
|
-
@if (membersLoading()) {
|
|
51938
|
-
<val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
|
|
51939
|
-
} @else if (membersErrorState()) {
|
|
51940
|
-
<val-empty-state [props]="membersErrorState()!" />
|
|
51941
|
-
} @else if (members().length === 0) {
|
|
51942
|
-
<val-text
|
|
51943
|
-
[props]="{
|
|
51944
|
-
content: tt('membersEmpty'),
|
|
51945
|
-
size: 'medium',
|
|
51946
|
-
color: 'dark',
|
|
51947
|
-
bold: false,
|
|
51948
|
-
}"
|
|
51949
|
-
/>
|
|
51950
|
-
} @else {
|
|
51951
|
-
<div class="members-list">
|
|
51952
|
-
@for (m of visibleMembers(); track m.userId) {
|
|
51953
|
-
<val-member-card
|
|
51954
|
-
[attr.data-testid]="'org-member-' + m.userId"
|
|
51955
|
-
[props]="{
|
|
51956
|
-
id: m.userId,
|
|
51957
|
-
name: m.name,
|
|
51958
|
-
handle: m.handle,
|
|
51959
|
-
email: m.email,
|
|
51960
|
-
avatarUrl: m.avatar,
|
|
51961
|
-
roleLabel: m.roles[0] ? roleLabel(m.roles[0]) : '',
|
|
51962
|
-
actionAriaLabel: tt('viewMemberCta'),
|
|
51963
|
-
}"
|
|
51964
|
-
(onAction)="onViewMember(m)"
|
|
51965
|
-
/>
|
|
51966
|
-
}
|
|
51967
|
-
</div>
|
|
51968
|
-
@if (!showAllMembers() && members().length > 3) {
|
|
51969
|
-
<button class="members-show-more" (click)="showAllMembers.set(true)">
|
|
51970
|
-
{{ tt('membersShowMore') }} ({{ members().length - 3 }})
|
|
51971
|
-
</button>
|
|
51972
|
-
}
|
|
51973
|
-
@if (showAllMembers() && membersNextToken()) {
|
|
51974
|
-
<button class="members-show-more" [disabled]="loadingMoreMembers()" (click)="loadMoreMembers()">
|
|
51975
|
-
{{ loadingMoreMembers() ? '…' : tt('membersShowMore') }}
|
|
51976
|
-
</button>
|
|
51977
|
-
}
|
|
51978
|
-
}
|
|
51979
|
-
</div>
|
|
51980
|
-
</section>
|
|
51981
|
-
}
|
|
51982
|
-
|
|
51983
|
-
@if (resolvedConfig().showRoles) {
|
|
51984
|
-
<!-- Roles y permisos: seccion explicativa, detalle en la ruta de permisos -->
|
|
51985
|
-
<section class="settings-section">
|
|
51986
|
-
<val-title
|
|
51987
|
-
[props]="{
|
|
51988
|
-
size: 'medium',
|
|
51989
|
-
color: 'dark',
|
|
51990
|
-
bold: true,
|
|
51991
|
-
content: tt('rolesSectionTitle'),
|
|
51992
|
-
}"
|
|
51993
|
-
/>
|
|
51994
|
-
<div class="section-body">
|
|
51995
|
-
<val-text
|
|
51996
|
-
[props]="{
|
|
51997
|
-
content: tt('rolesHint'),
|
|
51998
|
-
size: 'medium',
|
|
51999
|
-
color: 'dark',
|
|
52000
|
-
bold: false,
|
|
52001
|
-
}"
|
|
52002
|
-
/>
|
|
52003
|
-
<div class="row-actions">
|
|
52004
|
-
<val-button [props]="viewPermissionsButtonProps()" (click)="onViewPermissions()" />
|
|
52005
|
-
</div>
|
|
52006
|
-
</div>
|
|
52007
|
-
</section>
|
|
52008
|
-
}
|
|
52009
|
-
|
|
52010
|
-
@if (resolvedConfig().showInvite) {
|
|
52011
|
-
<!-- Invite: solo admin+ (gating RBAC interno) -->
|
|
52012
|
-
<section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
|
|
52013
|
-
<div class="invite-cta">
|
|
52014
|
-
<div class="invite-cta__text">
|
|
52015
|
-
<val-title
|
|
52016
|
-
[props]="{
|
|
52017
|
-
size: 'medium',
|
|
52018
|
-
color: 'dark',
|
|
52019
|
-
bold: true,
|
|
52020
|
-
content: tt('inviteCtaTitle'),
|
|
52021
|
-
}"
|
|
52022
|
-
/>
|
|
52023
|
-
<val-text
|
|
52024
|
-
[props]="{
|
|
52025
|
-
size: 'medium',
|
|
52026
|
-
color: 'dark',
|
|
52027
|
-
bold: false,
|
|
52028
|
-
content: tt('inviteCtaHint'),
|
|
52029
|
-
}"
|
|
52030
|
-
/>
|
|
52031
|
-
</div>
|
|
52032
|
-
<div class="invite-cta__actions">
|
|
52033
|
-
<val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
|
|
52034
|
-
<val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
|
|
52035
|
-
</div>
|
|
52036
|
-
</div>
|
|
52037
|
-
</section>
|
|
52038
|
-
}
|
|
52039
|
-
|
|
52040
|
-
<!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
|
|
52041
|
-
<section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
|
|
52042
|
-
<div class="invite-cta">
|
|
52043
|
-
<div class="invite-cta__text">
|
|
52044
|
-
<val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
|
|
52045
|
-
<val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
|
|
52046
|
-
<button class="org-more-info-link" type="button" (click)="onApiKeysInfo()">
|
|
52047
|
-
{{ tt('apiKeysMoreInfo') }}
|
|
52048
|
-
</button>
|
|
52049
|
-
</div>
|
|
52050
|
-
<div class="invite-cta__actions">
|
|
52051
|
-
<val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
|
|
52052
|
-
</div>
|
|
52053
|
-
</div>
|
|
52054
|
-
</section>
|
|
52055
|
-
|
|
52056
|
-
@if (resolvedConfig().showTransferOwnership && isOwner()) {
|
|
52057
|
-
<!-- Transfer ownership: solo owner (gating RBAC interno) -->
|
|
52058
|
-
<section class="settings-section" data-testid="org-transfer-section">
|
|
52059
|
-
<val-title
|
|
52060
|
-
[props]="{
|
|
52061
|
-
size: 'medium',
|
|
52062
|
-
color: 'dark',
|
|
52063
|
-
bold: true,
|
|
52064
|
-
content: tt('transferTitle'),
|
|
52065
|
-
}"
|
|
52066
|
-
/>
|
|
52067
|
-
<div class="section-body">
|
|
52068
|
-
<val-text
|
|
52069
|
-
[props]="{
|
|
52070
|
-
content: tt('transferHint'),
|
|
52071
|
-
size: 'medium',
|
|
52072
|
-
color: 'dark',
|
|
52073
|
-
bold: false,
|
|
52074
|
-
}"
|
|
52075
|
-
/>
|
|
52076
|
-
@if (transferableMembers().length > 0) {
|
|
52077
|
-
<div class="row-actions">
|
|
52078
|
-
<val-button [props]="transferButtonProps()" (click)="onOpenTransfer()" />
|
|
52079
|
-
</div>
|
|
52080
|
-
} @else {
|
|
52081
|
-
<val-text
|
|
52082
|
-
[props]="{
|
|
52083
|
-
content: tt('membersEmpty'),
|
|
52084
|
-
size: 'medium',
|
|
52085
|
-
color: 'dark',
|
|
52086
|
-
bold: false,
|
|
52087
|
-
}"
|
|
52088
|
-
/>
|
|
52089
|
-
}
|
|
52090
|
-
</div>
|
|
52091
|
-
</section>
|
|
52092
|
-
}
|
|
52093
|
-
|
|
52094
|
-
@if (resolvedConfig().showLeave) {
|
|
52095
|
-
<!-- Leave org -->
|
|
52096
|
-
<section class="settings-section">
|
|
52097
|
-
<val-title
|
|
52098
|
-
[props]="{
|
|
52099
|
-
size: 'medium',
|
|
52100
|
-
color: 'dark',
|
|
52101
|
-
bold: true,
|
|
52102
|
-
content: tt('leaveTitle'),
|
|
52103
|
-
}"
|
|
52104
|
-
/>
|
|
52105
|
-
<div class="section-body">
|
|
52106
|
-
<val-text
|
|
52107
|
-
[props]="{
|
|
52108
|
-
content: isOwner() ? tt('leaveOwnerHint') : tt('leaveHint'),
|
|
52109
|
-
size: 'medium',
|
|
52110
|
-
color: 'dark',
|
|
52111
|
-
bold: false,
|
|
52112
|
-
}"
|
|
52113
|
-
/>
|
|
52114
|
-
<div class="row-actions">
|
|
52115
|
-
<val-button [props]="leaveButtonProps()" (click)="onLeave()" />
|
|
52116
|
-
</div>
|
|
52117
|
-
</div>
|
|
52118
|
-
</section>
|
|
52119
|
-
}
|
|
52120
|
-
|
|
52121
|
-
@if (isDebug) {
|
|
52122
|
-
<section class="settings-section rbac-debug">
|
|
52123
|
-
<div class="section-header-row">
|
|
52124
|
-
<val-title
|
|
52125
|
-
[props]="{
|
|
52126
|
-
content: 'RBAC (live)',
|
|
52127
|
-
size: 'small',
|
|
52128
|
-
color: 'medium',
|
|
52129
|
-
bold: false,
|
|
52130
|
-
}"
|
|
52131
|
-
/>
|
|
52132
|
-
</div>
|
|
52133
|
-
<div class="rbac-debug__body">
|
|
52134
|
-
<div class="rbac-debug__row">
|
|
52135
|
-
<span class="rbac-debug__label">Roles</span>
|
|
52136
|
-
<span class="rbac-debug__value">{{ auth.roles().join(', ') || '—' }}</span>
|
|
52137
|
-
</div>
|
|
52138
|
-
<div class="rbac-debug__row">
|
|
52139
|
-
<span class="rbac-debug__label">Permisos</span>
|
|
52140
|
-
<span class="rbac-debug__value rbac-debug__value--perms">{{ auth.permissions().join(', ') || '—' }}</span>
|
|
52141
|
-
</div>
|
|
52142
|
-
</div>
|
|
52143
|
-
</section>
|
|
52144
|
-
}
|
|
52145
|
-
</div>
|
|
52146
|
-
`, 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}: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)}\n"] }]
|
|
52147
|
-
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
52148
|
-
type: Input
|
|
52149
|
-
}] } });
|
|
52150
|
-
|
|
52151
|
-
/**
|
|
52152
|
-
* Helper para montar la vista Gestión de organización (`val-organization-view`)
|
|
52153
|
-
* como ruta en una app del factory. El `config` se pasa por route `data`
|
|
52154
|
-
* (`organizationConfig`) y el componente lo lee como fallback de su
|
|
52155
|
-
* `@Input() config`.
|
|
52156
|
-
*
|
|
52157
|
-
* @example
|
|
52158
|
-
* ```ts
|
|
52159
|
-
* // settings.routes.ts
|
|
52160
|
-
* export const settingsRoutes: Routes = [
|
|
52161
|
-
* ...provideValtechOrganizationRoutes(), // → /app/settings/organization
|
|
52162
|
-
* { path: 'preferences', loadComponent: () => ... },
|
|
52163
|
-
* ];
|
|
52164
|
-
*
|
|
52165
|
-
* // con config acotada:
|
|
52166
|
-
* ...provideValtechOrganizationRoutes({ config: { showLeave: false } }),
|
|
52167
|
-
* ...provideValtechOrganizationRoutes({ config: { viewPermissionsRoute: '/app/roles' } }),
|
|
52168
|
-
* ```
|
|
52169
|
-
*/
|
|
52170
|
-
function provideValtechOrganizationRoutes(opts) {
|
|
52171
|
-
return [
|
|
52172
|
-
{
|
|
52173
|
-
path: opts?.path ?? 'organization',
|
|
52174
|
-
component: OrganizationViewComponent,
|
|
52175
|
-
data: { organizationConfig: opts?.config },
|
|
52176
|
-
},
|
|
52177
|
-
];
|
|
52178
|
-
}
|
|
52179
|
-
|
|
52180
|
-
/**
|
|
52181
|
-
* Defaults i18n (es/en) embebidos en `val-notification-preferences-view`.
|
|
52182
|
-
* Auto-registrados en el constructor del componente si el consumer no proveyó
|
|
52183
|
-
* el namespace (`Settings.Notifications` por default). SOLO incluye las keys que
|
|
52184
|
-
* la vista usa.
|
|
52185
|
-
*
|
|
52186
|
-
* Portado del override `Settings.Notifications` (showcase) bajo el proceso de
|
|
52187
|
-
* ADR-021.
|
|
52188
|
-
*/
|
|
52189
|
-
const NOTIFICATION_PREFERENCES_VIEW_I18N = {
|
|
52190
|
-
es: {
|
|
52191
|
-
pageTitle: 'Notificaciones',
|
|
52192
|
-
pageDescription: 'Configura como quieres recibir notificaciones',
|
|
52193
|
-
// Control principal (UX simplificado)
|
|
52194
|
-
simpleTitle: 'Notificaciones push en este dispositivo',
|
|
52195
|
-
simpleDescription: 'Recibe alertas en tiempo real incluso con la app cerrada.',
|
|
52196
|
-
enabling: 'Activando…',
|
|
52197
|
-
disabling: 'Desactivando…',
|
|
52198
|
-
enabled: 'Activadas',
|
|
52199
|
-
errorGeneric: 'No pudimos completar la configuración. Verifica los permisos del navegador y reintenta.',
|
|
52200
|
-
retryCta: 'Reintentar',
|
|
52201
|
-
// Mensajes contextuales
|
|
52202
|
-
permissionUnsupported: 'Tu navegador no soporta notificaciones push.',
|
|
52203
|
-
permissionDeniedInfo: 'Tu navegador esta bloqueando las notificaciones. Para habilitarlas, ve a la configuracion del sitio y permitelas manualmente.',
|
|
52204
|
-
// iOS PWA install
|
|
52205
|
-
iosInstallTitle: 'En iOS, instala la app para recibir notificaciones',
|
|
52206
|
-
iosInstallHint: 'Las notificaciones push en iOS (iPhone/iPad) solo funcionan cuando la app está instalada en la pantalla de inicio. Edge, Chrome y Firefox en iOS comparten esta limitación.',
|
|
52207
|
-
iosInstallStep1: 'Abre este sitio en Safari (no en Chrome/Edge).',
|
|
52208
|
-
iosInstallStep2: 'Toca el botón Compartir (□ con flecha hacia arriba).',
|
|
52209
|
-
iosInstallStep3: "Elige 'Añadir a pantalla de inicio'.",
|
|
52210
|
-
iosInstallStep4: 'Abre la app desde el ícono recién creado y vuelve aquí.',
|
|
52211
|
-
// Detalles técnicos (expandible)
|
|
52212
|
-
detailsShow: 'Ver detalles técnicos',
|
|
52213
|
-
detailsHide: 'Ocultar detalles',
|
|
52214
|
-
detailsToken: 'Token',
|
|
52215
|
-
detailsNone: 'Sin registrar',
|
|
52216
|
-
diagnosticsPermission: 'Permiso del navegador',
|
|
52217
|
-
diagnosticsSupported: 'Soporte del navegador',
|
|
52218
|
-
// Toasts
|
|
52219
|
-
enableSuccess: 'Notificaciones habilitadas',
|
|
52220
|
-
disableSuccess: 'Notificaciones deshabilitadas',
|
|
52221
|
-
},
|
|
52222
|
-
en: {
|
|
52223
|
-
pageTitle: 'Notifications',
|
|
52224
|
-
pageDescription: 'Configure how you want to receive notifications',
|
|
52225
|
-
simpleTitle: 'Push notifications on this device',
|
|
52226
|
-
simpleDescription: 'Get real-time alerts even when the app is closed.',
|
|
52227
|
-
enabling: 'Enabling…',
|
|
52228
|
-
disabling: 'Disabling…',
|
|
52229
|
-
enabled: 'Enabled',
|
|
52230
|
-
errorGeneric: "We couldn't complete the setup. Check your browser permissions and retry.",
|
|
52231
|
-
retryCta: 'Retry',
|
|
52232
|
-
permissionUnsupported: 'Your browser does not support push notifications.',
|
|
52233
|
-
permissionDeniedInfo: 'Your browser is blocking notifications. To enable them, go to site settings and allow them manually.',
|
|
52234
|
-
iosInstallTitle: 'On iOS, install the app to receive notifications',
|
|
52235
|
-
iosInstallHint: 'Push notifications on iOS (iPhone/iPad) only work when the app is installed on the Home Screen. Edge, Chrome and Firefox on iOS share this limitation.',
|
|
52236
|
-
iosInstallStep1: 'Open this site in Safari (not Chrome/Edge).',
|
|
52237
|
-
iosInstallStep2: 'Tap the Share button (□ with an up arrow).',
|
|
52238
|
-
iosInstallStep3: "Choose 'Add to Home Screen'.",
|
|
52239
|
-
iosInstallStep4: 'Open the app from the new icon and come back here.',
|
|
52240
|
-
detailsShow: 'View technical details',
|
|
52241
|
-
detailsHide: 'Hide details',
|
|
52242
|
-
detailsToken: 'Token',
|
|
52243
|
-
detailsNone: 'Not registered',
|
|
52244
|
-
diagnosticsPermission: 'Browser permission',
|
|
52245
|
-
diagnosticsSupported: 'Browser support',
|
|
52246
|
-
enableSuccess: 'Notifications enabled',
|
|
52247
|
-
disableSuccess: 'Notifications disabled',
|
|
52248
|
-
},
|
|
52249
|
-
};
|
|
52250
|
-
|
|
52251
|
-
addIcons({
|
|
52252
|
-
alertCircleOutline,
|
|
52253
|
-
chevronDownOutline,
|
|
52254
|
-
chevronUpOutline,
|
|
52255
|
-
closeCircleOutline,
|
|
52256
|
-
notificationsOffOutline,
|
|
52257
|
-
notificationsOutline,
|
|
52258
|
-
refreshOutline,
|
|
52259
|
-
});
|
|
52260
|
-
const DEFAULT_NAMESPACE$6 = 'Settings.Notifications';
|
|
52261
|
-
/**
|
|
52262
|
-
* `val-notification-preferences-view` — **preferencias de notificaciones push**
|
|
52263
|
-
* (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
|
|
52264
|
-
* proceso de ADR-021.
|
|
52265
|
-
*
|
|
52266
|
-
* Separada de `val-notifications-view` (el inbox / feed). Aquí el user **activa o
|
|
52267
|
-
* desactiva** las notificaciones push en este dispositivo.
|
|
52268
|
-
*
|
|
52269
|
-
* UX simplificado: UN solo toggle que ejecuta toda la secuencia (pedir permiso →
|
|
52270
|
-
* SW ready → getToken → registrar en backend → set master pref) vía
|
|
52271
|
-
* `MessagingService.enable()`. El registro del device en el backend va como
|
|
52272
|
-
* callback (la lib no depende de `AuthService` para evitar ciclos). Mensajes
|
|
52273
|
-
* contextuales y botón "Reintentar" aparecen solo cuando aplican.
|
|
52274
|
-
*
|
|
52275
|
-
* NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
|
|
52276
|
-
* inyecta `{ optional: true }` → funciona sin router (embebida / Storybook).
|
|
52277
|
-
*/
|
|
52278
|
-
class NotificationPreferencesViewComponent {
|
|
52279
|
-
/** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
|
|
52280
|
-
get ns() {
|
|
52281
|
-
return this.resolvedConfig().i18nNamespace;
|
|
52282
|
-
}
|
|
52283
|
-
// ---------------------------------------------------------------------------
|
|
52284
|
-
// Lifecycle
|
|
52285
|
-
// ---------------------------------------------------------------------------
|
|
52286
|
-
constructor() {
|
|
52287
|
-
this.nav = inject(NavigationService);
|
|
52288
|
-
this.i18n = inject(I18nService);
|
|
52289
|
-
this.toast = inject(ToastService);
|
|
52290
|
-
this.auth = inject(AuthService);
|
|
52291
|
-
this.messaging = inject(MessagingService);
|
|
52292
|
-
this.pageRefresh = inject(PageRefreshService);
|
|
52293
|
-
this.prefs = inject(PreferencesService);
|
|
52294
|
-
// `ActivatedRoute` opcional → la vista funciona embebida / en Storybook sin
|
|
52295
|
-
// router (solo lo usa para leer el route data de config como fallback).
|
|
52296
|
-
this.route = inject(ActivatedRoute, { optional: true });
|
|
52297
|
-
this.resolvedConfig = computed(() => {
|
|
52298
|
-
const fromRoute = (this.route?.snapshot.data['notificationPreferencesConfig'] ??
|
|
52299
|
-
{});
|
|
52300
|
-
const merged = { ...fromRoute, ...(this.config ?? {}) };
|
|
52301
|
-
return {
|
|
52302
|
-
showDetails: merged.showDetails ?? true,
|
|
52303
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$6,
|
|
52304
|
-
onPushEnabled: merged.onPushEnabled,
|
|
52305
|
-
onPushDisabled: merged.onPushDisabled,
|
|
52306
|
-
};
|
|
52307
|
-
});
|
|
52308
|
-
/** Operación en curso (gobierna spinner del toggle + label "Activando…"). */
|
|
52309
|
-
this._busy = signal(null);
|
|
52310
|
-
/** True si el último intento falló — gobierna estado `error` + retry button. */
|
|
52311
|
-
this._lastFailed = signal(false);
|
|
52312
|
-
/** True mientras se re-obtiene el token FCM en background (no afecta la UI). */
|
|
52313
|
-
this._repairingToken = false;
|
|
52314
|
-
/** Toggle del expand de detalles técnicos. */
|
|
52315
|
-
this.showDetails = signal(false);
|
|
52316
|
-
this.permission = signal('default');
|
|
52317
|
-
this.isSupported = signal(false);
|
|
52318
|
-
this.currentToken = signal(null);
|
|
52319
|
-
this.needsIOSInstall = signal(this.detectNeedsIOSInstall());
|
|
52320
|
-
/** FormControl del toggle (sync-eado con `isEnabled` via effect). */
|
|
52321
|
-
this.toggleControl = new FormControl(false, {
|
|
52322
|
-
nonNullable: true,
|
|
52323
|
-
});
|
|
52324
|
-
/** Handler del listener `focus` — referencia guardada para poder removerlo. */
|
|
52325
|
-
this.onWindowFocus = () => this.scheduleBrowserRefresh();
|
|
52326
|
-
/** Timer del debounce del refresh disparado por `focus`. */
|
|
52327
|
-
this.focusDebounceTimer = null;
|
|
52328
|
-
/** Ventana de debounce (ms) para el refresh disparado por `focus`. */
|
|
52329
|
-
this.FOCUS_DEBOUNCE_MS = 500;
|
|
52330
|
-
// ---------------------------------------------------------------------------
|
|
52331
|
-
// Estado derivado
|
|
52332
|
-
// ---------------------------------------------------------------------------
|
|
52333
|
-
this.pageTitle = computed(() => this.tt('pageTitle'));
|
|
52334
|
-
this.pageDescription = computed(() => this.tt('pageDescription'));
|
|
52335
|
-
/**
|
|
52336
|
-
* Estado efectivo del toggle: "el usuario tiene push activo".
|
|
52337
|
-
*
|
|
52338
|
-
* Se deriva SOLO de la decisión del usuario (`master`) y del permiso del SO
|
|
52339
|
-
* (`permission`). El token FCM NO forma parte del estado de UI: vive en
|
|
52340
|
-
* memoria, se pierde en cada recarga/suspensión de la PWA y produciría flicker.
|
|
52341
|
-
* Si falta token con `master && granted`, es un detalle reparable en
|
|
52342
|
-
* background (ver `repairTokenIfNeeded`), no un cambio del toggle.
|
|
52343
|
-
*/
|
|
52344
|
-
this.isEnabled = computed(() => this.prefs.notificationsMaster() && this.permission() === 'granted');
|
|
52345
|
-
this.state = computed(() => {
|
|
52346
|
-
if (this.needsIOSInstall())
|
|
52347
|
-
return 'needs-install';
|
|
52348
|
-
if (this.permission() === 'unsupported' || !this.isSupported()) {
|
|
52349
|
-
// iOS standalone aún puede dar isSupported false temprano antes de init —
|
|
52350
|
-
// solo bloqueamos si el browser explícitamente no soporta APIs.
|
|
52351
|
-
if (this.permission() === 'unsupported')
|
|
52352
|
-
return 'unsupported';
|
|
52353
|
-
}
|
|
52354
|
-
const busy = this._busy();
|
|
52355
|
-
if (busy === 'enabling')
|
|
52356
|
-
return 'enabling';
|
|
52357
|
-
if (busy === 'disabling')
|
|
52358
|
-
return 'disabling';
|
|
52359
|
-
if (this.isEnabled())
|
|
52360
|
-
return 'enabled';
|
|
52361
|
-
if (this.permission() === 'denied')
|
|
52362
|
-
return 'blocked';
|
|
52363
|
-
if (this._lastFailed())
|
|
52364
|
-
return 'error';
|
|
52365
|
-
return 'idle';
|
|
52366
|
-
});
|
|
52367
|
-
this.busyLabel = computed(() => {
|
|
52368
|
-
const s = this.state();
|
|
52369
|
-
if (s === 'enabling')
|
|
52370
|
-
return this.tt('enabling');
|
|
52371
|
-
if (s === 'disabling')
|
|
52372
|
-
return this.tt('disabling');
|
|
52373
|
-
return null;
|
|
52374
|
-
});
|
|
52375
|
-
this.contextMessage = computed(() => {
|
|
52376
|
-
switch (this.state()) {
|
|
52377
|
-
case 'unsupported':
|
|
52378
|
-
return this.tt('permissionUnsupported');
|
|
52379
|
-
case 'blocked':
|
|
52380
|
-
return this.tt('permissionDeniedInfo');
|
|
52381
|
-
case 'error':
|
|
52382
|
-
return this.tt('errorGeneric');
|
|
52383
|
-
default:
|
|
52384
|
-
return null;
|
|
52385
|
-
}
|
|
52386
|
-
});
|
|
52387
|
-
this.alertVariant = computed(() => {
|
|
52388
|
-
const s = this.state();
|
|
52389
|
-
if (s === 'error' || s === 'blocked')
|
|
52390
|
-
return 'warning';
|
|
52391
|
-
if (s === 'unsupported')
|
|
52392
|
-
return 'danger';
|
|
52393
|
-
return 'info';
|
|
52394
|
-
});
|
|
52395
|
-
this.alertIcon = computed(() => {
|
|
52396
|
-
const s = this.state();
|
|
52397
|
-
if (s === 'unsupported')
|
|
52398
|
-
return 'close-circle-outline';
|
|
52399
|
-
return 'alert-circle-outline';
|
|
52400
|
-
});
|
|
52401
|
-
this.toggleProps = computed(() => ({
|
|
52402
|
-
control: this.toggleControl,
|
|
52403
|
-
color: 'primary',
|
|
52404
|
-
disabled: this._busy() !== null ||
|
|
52405
|
-
this.state() === 'unsupported' ||
|
|
52406
|
-
this.state() === 'needs-install' ||
|
|
52407
|
-
this.state() === 'blocked',
|
|
52408
|
-
}));
|
|
52409
|
-
this.retryButtonProps = computed(() => ({
|
|
52410
|
-
text: this.tt('retryCta'),
|
|
52411
|
-
color: 'dark',
|
|
52412
|
-
fill: 'outline',
|
|
52413
|
-
size: 'default',
|
|
52414
|
-
shape: 'round',
|
|
52415
|
-
type: 'button',
|
|
52416
|
-
icon: { name: 'refresh-outline', slot: 'start' },
|
|
52417
|
-
state: this._busy() !== null ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
52418
|
-
}));
|
|
52419
|
-
this.tokenPreview = computed(() => {
|
|
52420
|
-
const t = this.currentToken();
|
|
52421
|
-
if (!t)
|
|
52422
|
-
return null;
|
|
52423
|
-
return `${t.slice(0, 22)}…${t.slice(-6)}`;
|
|
52424
|
-
});
|
|
52425
|
-
const ns = this.ns;
|
|
52426
|
-
if (!this.i18n.hasNamespace(ns)) {
|
|
52427
|
-
this.i18n.registerContent(ns, NOTIFICATION_PREFERENCES_VIEW_I18N);
|
|
52428
|
-
}
|
|
52429
|
-
this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
|
|
52430
|
-
void this.refreshBrowserState();
|
|
52431
|
-
// Re-aplicar header en cambio de idioma.
|
|
52432
|
-
effect(() => {
|
|
52433
|
-
this.i18n.lang();
|
|
52434
|
-
this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
|
|
52435
|
-
}, { allowSignalWrites: true });
|
|
52436
|
-
// Sync FormControl ←→ estado efectivo. Cuando el estado cambia (cambio de
|
|
52437
|
-
// permission por el browser, o snapshot Firestore de `master`), el toggle se
|
|
52438
|
-
// re-alinea con la realidad.
|
|
52439
|
-
//
|
|
52440
|
-
// IMPORTANTE: se ignoran los cambios mientras hay una operación en curso
|
|
52441
|
-
// (`_busy() !== null`). Durante `onEnable`/`onDisable` las señales fuente se
|
|
52442
|
-
// actualizan en momentos distintos y producirían estados intermedios →
|
|
52443
|
-
// flicker visible. El toggle se re-alinea UNA sola vez en el `finally`.
|
|
52444
|
-
effect(() => {
|
|
52445
|
-
const eff = this.isEnabled();
|
|
52446
|
-
if (this._busy() !== null)
|
|
52447
|
-
return;
|
|
52448
|
-
if (this.toggleControl.value !== eff) {
|
|
52449
|
-
this.toggleControl.setValue(eff, { emitEvent: false });
|
|
52450
|
-
}
|
|
52451
|
-
}, { allowSignalWrites: true });
|
|
52452
|
-
// Interceptar click del user: el toggle ya cambió visualmente, ejecutar
|
|
52453
|
-
// la acción correspondiente.
|
|
52454
|
-
this.toggleControl.valueChanges.subscribe(v => {
|
|
52455
|
-
if (this._busy() !== null)
|
|
52456
|
-
return;
|
|
52457
|
-
if (v && !this.isEnabled()) {
|
|
52458
|
-
void this.onEnable();
|
|
52459
|
-
}
|
|
52460
|
-
else if (!v && this.isEnabled()) {
|
|
52461
|
-
void this.onDisable();
|
|
52462
|
-
}
|
|
52463
|
-
});
|
|
52464
|
-
// Detectar regreso a la pestaña — el user pudo haber cambiado permisos
|
|
52465
|
-
// desde la config del browser. El refresh va con debounce para no
|
|
52466
|
-
// dispararse en ráfagas (en PWA mobile `focus` salta con cada cambio de
|
|
52467
|
-
// app / aparición del teclado). El listener se remueve en ngOnDestroy.
|
|
52468
|
-
if (typeof window !== 'undefined') {
|
|
52469
|
-
window.addEventListener('focus', this.onWindowFocus);
|
|
52470
|
-
}
|
|
52471
|
-
}
|
|
52472
|
-
/**
|
|
52473
|
-
* Registra el pull-to-refresh estándar. Aunque esta vista no muestra una
|
|
52474
|
-
* lista remota, sí refleja estado mutable del browser (permiso, token,
|
|
52475
|
-
* soporte FCM) que puede cambiar fuera de la app — el refresh re-evalúa todo.
|
|
52476
|
-
*
|
|
52477
|
-
* Se usa `ngOnInit`/`ngOnDestroy` (NO `ionViewWillEnter`) porque
|
|
52478
|
-
* `val-page-wrapper` usa `<router-outlet>` plano — los hooks `ionView*` solo
|
|
52479
|
-
* disparan dentro de `<ion-router-outlet>`.
|
|
52480
|
-
*/
|
|
52481
|
-
ngOnInit() {
|
|
52482
|
-
this.pageRefresh.register(() => this.refreshBrowserState());
|
|
52483
|
-
}
|
|
52484
|
-
ngOnDestroy() {
|
|
52485
|
-
this.pageRefresh.unregister();
|
|
52486
|
-
if (typeof window !== 'undefined') {
|
|
52487
|
-
window.removeEventListener('focus', this.onWindowFocus);
|
|
52488
|
-
}
|
|
52489
|
-
if (this.focusDebounceTimer !== null) {
|
|
52490
|
-
clearTimeout(this.focusDebounceTimer);
|
|
52491
|
-
this.focusDebounceTimer = null;
|
|
52492
|
-
}
|
|
52493
|
-
}
|
|
52494
|
-
/** Refresca el estado del browser con debounce — evita ráfagas de `focus`. */
|
|
52495
|
-
scheduleBrowserRefresh() {
|
|
52496
|
-
if (this.focusDebounceTimer !== null) {
|
|
52497
|
-
clearTimeout(this.focusDebounceTimer);
|
|
52498
|
-
}
|
|
52499
|
-
this.focusDebounceTimer = setTimeout(() => {
|
|
52500
|
-
this.focusDebounceTimer = null;
|
|
52501
|
-
void this.refreshBrowserState();
|
|
52502
|
-
}, this.FOCUS_DEBOUNCE_MS);
|
|
52503
|
-
}
|
|
52504
|
-
// ---------------------------------------------------------------------------
|
|
52505
|
-
// Actions
|
|
52506
|
-
// ---------------------------------------------------------------------------
|
|
52507
|
-
/**
|
|
52508
|
-
* Secuencia completa de activación.
|
|
52509
|
-
*
|
|
52510
|
-
* Toda la mecánica (permission → SW ready → getToken con retry → watchdog de
|
|
52511
|
-
* auto-reload) vive en `MessagingService.enable()` de la lib. El paso de
|
|
52512
|
-
* registro del device en el backend se pasa como callback (`registerDevice`)
|
|
52513
|
-
* porque la lib no puede depender de `AuthService` sin crear un ciclo.
|
|
52514
|
-
*
|
|
52515
|
-
* `enable()` no hace throw: resuelve con un `EnablePushResult` descriptivo.
|
|
52516
|
-
*/
|
|
52517
|
-
async onEnable() {
|
|
52518
|
-
this._busy.set('enabling');
|
|
52519
|
-
this._lastFailed.set(false);
|
|
52520
|
-
try {
|
|
52521
|
-
const result = await this.messaging.enable({
|
|
52522
|
-
registerDevice: token => this.auth.registerDevice(token).then(r => r.registered),
|
|
52523
|
-
});
|
|
52524
|
-
if (result.status === 'enabled') {
|
|
52525
|
-
// Persistir flag master si aún no estaba activo.
|
|
52526
|
-
if (!this.prefs.notificationsMaster()) {
|
|
52527
|
-
await this.prefs.setNotificationsMaster(true);
|
|
52528
|
-
}
|
|
52529
|
-
this.toast.show({
|
|
52530
|
-
message: this.tt('enableSuccess'),
|
|
52531
|
-
duration: 2000,
|
|
52532
|
-
color: 'dark',
|
|
52533
|
-
position: 'top',
|
|
52534
|
-
});
|
|
52535
|
-
this.resolvedConfig().onPushEnabled?.();
|
|
52536
|
-
}
|
|
52537
|
-
else if (result.status === 'timeout' && result.reloaded) {
|
|
52538
|
-
// La lib disparó el auto-reload — la página está por recargarse.
|
|
52539
|
-
return;
|
|
52540
|
-
}
|
|
52541
|
-
else {
|
|
52542
|
-
// denied / unsupported / error / timeout sin reload → fallo recuperable.
|
|
52543
|
-
this._lastFailed.set(true);
|
|
52544
|
-
this.toast.show({
|
|
52545
|
-
message: this.tt('errorGeneric'),
|
|
52546
|
-
duration: 3000,
|
|
52547
|
-
color: 'dark',
|
|
52548
|
-
position: 'top',
|
|
52549
|
-
});
|
|
52550
|
-
}
|
|
52551
|
-
}
|
|
52552
|
-
catch {
|
|
52553
|
-
// `enable()` no hace throw, pero el callback de registro sí puede.
|
|
52554
|
-
this._lastFailed.set(true);
|
|
52555
|
-
this.toast.show({
|
|
52556
|
-
message: this.tt('errorGeneric'),
|
|
52557
|
-
duration: 3000,
|
|
52558
|
-
color: 'dark',
|
|
52559
|
-
position: 'top',
|
|
52560
|
-
});
|
|
52561
|
-
}
|
|
52562
|
-
finally {
|
|
52563
|
-
// Único refresh final: re-alinea el toggle UNA sola vez con el estado real.
|
|
52564
|
-
await this.refreshBrowserState();
|
|
52565
|
-
this._busy.set(null);
|
|
52566
|
-
}
|
|
52567
|
-
}
|
|
52568
|
-
async onDisable() {
|
|
52569
|
-
this._busy.set('disabling');
|
|
52570
|
-
this._lastFailed.set(false);
|
|
52571
|
-
try {
|
|
52572
|
-
await this.auth.disableNotifications();
|
|
52573
|
-
if (this.prefs.notificationsMaster()) {
|
|
52574
|
-
await this.prefs.setNotificationsMaster(false);
|
|
52575
|
-
}
|
|
52576
|
-
this.toast.show({
|
|
52577
|
-
message: this.tt('disableSuccess'),
|
|
52578
|
-
duration: 2000,
|
|
52579
|
-
color: 'dark',
|
|
52580
|
-
position: 'top',
|
|
52581
|
-
});
|
|
52582
|
-
this.resolvedConfig().onPushDisabled?.();
|
|
52583
|
-
}
|
|
52584
|
-
catch {
|
|
52585
|
-
this._lastFailed.set(true);
|
|
52586
|
-
}
|
|
52587
|
-
finally {
|
|
52588
|
-
await this.refreshBrowserState();
|
|
52589
|
-
this._busy.set(null);
|
|
52590
|
-
}
|
|
52591
|
-
}
|
|
52592
|
-
/** Reintentar la secuencia completa de activación, sin importar el estado actual. */
|
|
52593
|
-
async onRetry() {
|
|
52594
|
-
this._lastFailed.set(false);
|
|
52595
|
-
await this.onEnable();
|
|
52596
|
-
}
|
|
52597
|
-
toggleDetails() {
|
|
52598
|
-
this.showDetails.update(v => !v);
|
|
52599
|
-
}
|
|
52600
|
-
// ---------------------------------------------------------------------------
|
|
52601
|
-
// Privados
|
|
52602
|
-
// ---------------------------------------------------------------------------
|
|
52603
|
-
async refreshBrowserState() {
|
|
52604
|
-
this.isSupported.set(await this.messaging.isSupported());
|
|
52605
|
-
this.permission.set(this.auth.getNotificationPermissionState());
|
|
52606
|
-
this.currentToken.set(this.messaging.currentToken ?? null);
|
|
52607
|
-
void this.repairTokenIfNeeded();
|
|
52608
|
-
}
|
|
52609
|
-
/**
|
|
52610
|
-
* Repara el token FCM en background.
|
|
52611
|
-
*
|
|
52612
|
-
* Si el usuario tiene push activo (`master && granted`) pero no hay token en
|
|
52613
|
-
* memoria — caso típico tras un cold start de la PWA — re-obtenemos el token
|
|
52614
|
-
* y re-registramos el device sin tocar el toggle ni mostrar un error
|
|
52615
|
-
* alarmista. El token es un detalle de implementación reparable, no parte del
|
|
52616
|
-
* estado de UI (ver `isEnabled`).
|
|
52617
|
-
*/
|
|
52618
|
-
async repairTokenIfNeeded() {
|
|
52619
|
-
if (this._busy() !== null)
|
|
52620
|
-
return;
|
|
52621
|
-
if (this._repairingToken)
|
|
52622
|
-
return;
|
|
52623
|
-
if (!this.isEnabled())
|
|
52624
|
-
return;
|
|
52625
|
-
if (this.currentToken())
|
|
52626
|
-
return;
|
|
52627
|
-
this._repairingToken = true;
|
|
52628
|
-
try {
|
|
52629
|
-
const token = await this.messaging.getToken();
|
|
52630
|
-
if (token) {
|
|
52631
|
-
this.currentToken.set(token);
|
|
52632
|
-
await this.auth.registerDevice(token);
|
|
52682
|
+
color: 'dark',
|
|
52683
|
+
bold: true,
|
|
52684
|
+
content: tt('membersTitle'),
|
|
52685
|
+
}"
|
|
52686
|
+
/>
|
|
52687
|
+
<div class="section-body">
|
|
52688
|
+
@if (membersLoading()) {
|
|
52689
|
+
<val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
|
|
52690
|
+
} @else if (membersErrorState()) {
|
|
52691
|
+
<val-empty-state [props]="membersErrorState()!" />
|
|
52692
|
+
} @else if (members().length === 0) {
|
|
52693
|
+
<val-text
|
|
52694
|
+
[props]="{
|
|
52695
|
+
content: tt('membersEmpty'),
|
|
52696
|
+
size: 'medium',
|
|
52697
|
+
color: 'dark',
|
|
52698
|
+
bold: false,
|
|
52699
|
+
}"
|
|
52700
|
+
/>
|
|
52701
|
+
} @else {
|
|
52702
|
+
<div class="members-list">
|
|
52703
|
+
@for (m of visibleMembers(); track m.userId) {
|
|
52704
|
+
<val-member-card
|
|
52705
|
+
[attr.data-testid]="'org-member-' + m.userId"
|
|
52706
|
+
[props]="{
|
|
52707
|
+
id: m.userId,
|
|
52708
|
+
name: m.name,
|
|
52709
|
+
handle: m.handle,
|
|
52710
|
+
email: m.email,
|
|
52711
|
+
avatarUrl: m.avatar,
|
|
52712
|
+
roleLabel: m.roles[0] ? roleLabel(m.roles[0]) : '',
|
|
52713
|
+
actionAriaLabel: tt('viewMemberCta'),
|
|
52714
|
+
}"
|
|
52715
|
+
(onAction)="onViewMember(m)"
|
|
52716
|
+
/>
|
|
52717
|
+
}
|
|
52718
|
+
</div>
|
|
52719
|
+
@if (!showAllMembers() && members().length > 3) {
|
|
52720
|
+
<button class="members-show-more" (click)="showAllMembers.set(true)">
|
|
52721
|
+
{{ tt('membersShowMore') }} ({{ members().length - 3 }})
|
|
52722
|
+
</button>
|
|
52723
|
+
}
|
|
52724
|
+
@if (showAllMembers() && membersNextToken()) {
|
|
52725
|
+
<button class="members-show-more" [disabled]="loadingMoreMembers()" (click)="loadMoreMembers()">
|
|
52726
|
+
{{ loadingMoreMembers() ? '…' : tt('membersShowMore') }}
|
|
52727
|
+
</button>
|
|
52728
|
+
}
|
|
52633
52729
|
}
|
|
52634
|
-
|
|
52635
|
-
|
|
52636
|
-
|
|
52637
|
-
}
|
|
52638
|
-
finally {
|
|
52639
|
-
this._repairingToken = false;
|
|
52640
|
-
}
|
|
52641
|
-
}
|
|
52642
|
-
/**
|
|
52643
|
-
* iOS browser sin estar instalado como PWA — push web no disponible.
|
|
52644
|
-
* Aplica a Safari y a Edge/Chrome/Firefox en iOS (todos WebKit por mandato Apple).
|
|
52645
|
-
*/
|
|
52646
|
-
detectNeedsIOSInstall() {
|
|
52647
|
-
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
52648
|
-
return false;
|
|
52649
|
-
}
|
|
52650
|
-
const ua = navigator.userAgent || '';
|
|
52651
|
-
const isIOS = /iPad|iPhone|iPod/.test(ua);
|
|
52652
|
-
if (!isIOS)
|
|
52653
|
-
return false;
|
|
52654
|
-
const isStandalone = window.matchMedia?.('(display-mode: standalone)').matches ||
|
|
52655
|
-
navigator.standalone === true;
|
|
52656
|
-
return !isStandalone;
|
|
52657
|
-
}
|
|
52658
|
-
tt(key) {
|
|
52659
|
-
return this.i18n.t(key, this.ns);
|
|
52660
|
-
}
|
|
52661
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
52662
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: NotificationPreferencesViewComponent, isStandalone: true, selector: "val-notification-preferences-view", inputs: { config: "config" }, ngImport: i0, template: `
|
|
52663
|
-
<div class="page">
|
|
52664
|
-
<header class="page-header">
|
|
52665
|
-
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
52666
|
-
<val-title
|
|
52667
|
-
[props]="{
|
|
52668
|
-
size: 'large',
|
|
52669
|
-
color: 'dark',
|
|
52670
|
-
bold: false,
|
|
52671
|
-
content: pageDescription(),
|
|
52672
|
-
}"
|
|
52673
|
-
/>
|
|
52674
|
-
</header>
|
|
52730
|
+
</div>
|
|
52731
|
+
</section>
|
|
52732
|
+
}
|
|
52675
52733
|
|
|
52676
|
-
@if (
|
|
52734
|
+
@if (resolvedConfig().showRoles) {
|
|
52735
|
+
<!-- Roles y permisos: seccion explicativa, detalle en la ruta de permisos -->
|
|
52677
52736
|
<section class="settings-section">
|
|
52678
52737
|
<val-title
|
|
52679
52738
|
[props]="{
|
|
52680
52739
|
size: 'medium',
|
|
52681
52740
|
color: 'dark',
|
|
52682
52741
|
bold: true,
|
|
52683
|
-
content: tt('
|
|
52742
|
+
content: tt('rolesSectionTitle'),
|
|
52684
52743
|
}"
|
|
52685
52744
|
/>
|
|
52686
52745
|
<div class="section-body">
|
|
52687
52746
|
<val-text
|
|
52688
52747
|
[props]="{
|
|
52689
|
-
|
|
52690
|
-
|
|
52748
|
+
content: tt('rolesHint'),
|
|
52749
|
+
size: 'medium',
|
|
52750
|
+
color: 'dark',
|
|
52691
52751
|
bold: false,
|
|
52692
|
-
content: tt('iosInstallHint'),
|
|
52693
52752
|
}"
|
|
52694
52753
|
/>
|
|
52695
|
-
<
|
|
52696
|
-
<
|
|
52697
|
-
|
|
52698
|
-
<li>{{ tt('iosInstallStep3') }}</li>
|
|
52699
|
-
<li>{{ tt('iosInstallStep4') }}</li>
|
|
52700
|
-
</ol>
|
|
52754
|
+
<div class="row-actions">
|
|
52755
|
+
<val-button [props]="viewPermissionsButtonProps()" (click)="onViewPermissions()" />
|
|
52756
|
+
</div>
|
|
52701
52757
|
</div>
|
|
52702
52758
|
</section>
|
|
52703
52759
|
}
|
|
52704
52760
|
|
|
52705
|
-
|
|
52706
|
-
|
|
52707
|
-
|
|
52708
|
-
|
|
52761
|
+
@if (resolvedConfig().showInvite) {
|
|
52762
|
+
<!-- Invite: solo admin+ (gating RBAC interno) -->
|
|
52763
|
+
<section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
|
|
52764
|
+
<div class="invite-cta">
|
|
52765
|
+
<div class="invite-cta__text">
|
|
52766
|
+
<val-title
|
|
52767
|
+
[props]="{
|
|
52768
|
+
size: 'medium',
|
|
52769
|
+
color: 'dark',
|
|
52770
|
+
bold: true,
|
|
52771
|
+
content: tt('inviteCtaTitle'),
|
|
52772
|
+
}"
|
|
52773
|
+
/>
|
|
52774
|
+
<val-text
|
|
52775
|
+
[props]="{
|
|
52776
|
+
size: 'medium',
|
|
52777
|
+
color: 'dark',
|
|
52778
|
+
bold: false,
|
|
52779
|
+
content: tt('inviteCtaHint'),
|
|
52780
|
+
}"
|
|
52781
|
+
/>
|
|
52782
|
+
</div>
|
|
52783
|
+
<div class="invite-cta__actions">
|
|
52784
|
+
<val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
|
|
52785
|
+
<val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
|
|
52786
|
+
</div>
|
|
52709
52787
|
</div>
|
|
52710
|
-
|
|
52711
|
-
|
|
52788
|
+
</section>
|
|
52789
|
+
}
|
|
52790
|
+
|
|
52791
|
+
<!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
|
|
52792
|
+
<section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
|
|
52793
|
+
<div class="invite-cta">
|
|
52794
|
+
<div class="invite-cta__text">
|
|
52795
|
+
<val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
|
|
52796
|
+
<val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
|
|
52797
|
+
<button class="org-more-info-link" type="button" (click)="onApiKeysInfo()">
|
|
52798
|
+
{{ tt('apiKeysMoreInfo') }}
|
|
52799
|
+
</button>
|
|
52800
|
+
</div>
|
|
52801
|
+
<div class="invite-cta__actions">
|
|
52802
|
+
<val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
|
|
52803
|
+
</div>
|
|
52804
|
+
</div>
|
|
52805
|
+
</section>
|
|
52806
|
+
|
|
52807
|
+
@if (resolvedConfig().showTransferOwnership && isOwner()) {
|
|
52808
|
+
<!-- Transfer ownership: solo owner (gating RBAC interno) -->
|
|
52809
|
+
<section class="settings-section" data-testid="org-transfer-section">
|
|
52810
|
+
<val-title
|
|
52811
|
+
[props]="{
|
|
52812
|
+
size: 'medium',
|
|
52813
|
+
color: 'dark',
|
|
52814
|
+
bold: true,
|
|
52815
|
+
content: tt('transferTitle'),
|
|
52816
|
+
}"
|
|
52817
|
+
/>
|
|
52818
|
+
<div class="section-body">
|
|
52819
|
+
<val-text
|
|
52712
52820
|
[props]="{
|
|
52821
|
+
content: tt('transferHint'),
|
|
52713
52822
|
size: 'medium',
|
|
52714
52823
|
color: 'dark',
|
|
52715
|
-
bold: true,
|
|
52716
|
-
content: tt('simpleTitle'),
|
|
52717
|
-
}"
|
|
52718
|
-
/>
|
|
52719
|
-
<val-text
|
|
52720
|
-
[props]="{
|
|
52721
|
-
size: 'small',
|
|
52722
|
-
color: 'medium',
|
|
52723
52824
|
bold: false,
|
|
52724
|
-
content: tt('simpleDescription'),
|
|
52725
52825
|
}"
|
|
52726
52826
|
/>
|
|
52727
|
-
@if (
|
|
52728
|
-
<
|
|
52827
|
+
@if (transferableMembers().length > 0) {
|
|
52828
|
+
<div class="row-actions">
|
|
52829
|
+
<val-button [props]="transferButtonProps()" (click)="onOpenTransfer()" />
|
|
52830
|
+
</div>
|
|
52831
|
+
} @else {
|
|
52832
|
+
<val-text
|
|
52833
|
+
[props]="{
|
|
52834
|
+
content: tt('membersEmpty'),
|
|
52835
|
+
size: 'medium',
|
|
52836
|
+
color: 'dark',
|
|
52837
|
+
bold: false,
|
|
52838
|
+
}"
|
|
52839
|
+
/>
|
|
52729
52840
|
}
|
|
52730
52841
|
</div>
|
|
52731
|
-
<div class="notif-row__control">
|
|
52732
|
-
<val-toggle-input [props]="toggleProps()" />
|
|
52733
|
-
</div>
|
|
52734
|
-
</div>
|
|
52735
|
-
|
|
52736
|
-
@if (contextMessage(); as msg) {
|
|
52737
|
-
<div class="alert" [class]="'alert--' + alertVariant()">
|
|
52738
|
-
<ion-icon [name]="alertIcon()" aria-hidden="true" />
|
|
52739
|
-
<span>{{ msg }}</span>
|
|
52740
|
-
</div>
|
|
52741
|
-
}
|
|
52742
|
-
|
|
52743
|
-
@if (state() === 'error') {
|
|
52744
|
-
<div class="row-actions">
|
|
52745
|
-
<val-button [props]="retryButtonProps()" (click)="onRetry()" />
|
|
52746
|
-
</div>
|
|
52747
|
-
}
|
|
52748
|
-
</section>
|
|
52749
|
-
|
|
52750
|
-
@if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
|
|
52751
|
-
<section class="settings-section">
|
|
52752
|
-
<button class="details-toggle" type="button" (click)="toggleDetails()">
|
|
52753
|
-
<span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
|
|
52754
|
-
<ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
|
|
52755
|
-
</button>
|
|
52756
|
-
@if (showDetails()) {
|
|
52757
|
-
<dl class="details">
|
|
52758
|
-
<div class="details__row">
|
|
52759
|
-
<dt>{{ tt('detailsToken') }}</dt>
|
|
52760
|
-
<dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
|
|
52761
|
-
</div>
|
|
52762
|
-
<div class="details__row">
|
|
52763
|
-
<dt>{{ tt('diagnosticsPermission') }}</dt>
|
|
52764
|
-
<dd>{{ permission() }}</dd>
|
|
52765
|
-
</div>
|
|
52766
|
-
<div class="details__row">
|
|
52767
|
-
<dt>{{ tt('diagnosticsSupported') }}</dt>
|
|
52768
|
-
<dd>{{ isSupported() ? '✓' : '✗' }}</dd>
|
|
52769
|
-
</div>
|
|
52770
|
-
</dl>
|
|
52771
|
-
}
|
|
52772
52842
|
</section>
|
|
52773
52843
|
}
|
|
52774
|
-
</div>
|
|
52775
|
-
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: ToggleInputComponent, selector: "val-toggle-input", inputs: ["preset", "props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
|
|
52776
|
-
}
|
|
52777
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, decorators: [{
|
|
52778
|
-
type: Component,
|
|
52779
|
-
args: [{ selector: 'val-notification-preferences-view', standalone: true, imports: [IonIcon, ToggleInputComponent, ButtonComponent, DisplayComponent, TitleComponent, TextComponent], template: `
|
|
52780
|
-
<div class="page">
|
|
52781
|
-
<header class="page-header">
|
|
52782
|
-
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
52783
|
-
<val-title
|
|
52784
|
-
[props]="{
|
|
52785
|
-
size: 'large',
|
|
52786
|
-
color: 'dark',
|
|
52787
|
-
bold: false,
|
|
52788
|
-
content: pageDescription(),
|
|
52789
|
-
}"
|
|
52790
|
-
/>
|
|
52791
|
-
</header>
|
|
52792
52844
|
|
|
52793
|
-
@if (
|
|
52845
|
+
@if (resolvedConfig().showLeave) {
|
|
52846
|
+
<!-- Leave org -->
|
|
52794
52847
|
<section class="settings-section">
|
|
52795
52848
|
<val-title
|
|
52796
52849
|
[props]="{
|
|
52797
52850
|
size: 'medium',
|
|
52798
52851
|
color: 'dark',
|
|
52799
52852
|
bold: true,
|
|
52800
|
-
content: tt('
|
|
52853
|
+
content: tt('leaveTitle'),
|
|
52801
52854
|
}"
|
|
52802
52855
|
/>
|
|
52803
52856
|
<div class="section-body">
|
|
52804
52857
|
<val-text
|
|
52805
52858
|
[props]="{
|
|
52806
|
-
|
|
52807
|
-
|
|
52859
|
+
content: isOwner() ? tt('leaveOwnerHint') : tt('leaveHint'),
|
|
52860
|
+
size: 'medium',
|
|
52861
|
+
color: 'dark',
|
|
52808
52862
|
bold: false,
|
|
52809
|
-
content: tt('iosInstallHint'),
|
|
52810
52863
|
}"
|
|
52811
52864
|
/>
|
|
52812
|
-
<
|
|
52813
|
-
<
|
|
52814
|
-
|
|
52815
|
-
<li>{{ tt('iosInstallStep3') }}</li>
|
|
52816
|
-
<li>{{ tt('iosInstallStep4') }}</li>
|
|
52817
|
-
</ol>
|
|
52865
|
+
<div class="row-actions">
|
|
52866
|
+
<val-button [props]="leaveButtonProps()" (click)="onLeave()" />
|
|
52867
|
+
</div>
|
|
52818
52868
|
</div>
|
|
52819
52869
|
</section>
|
|
52820
52870
|
}
|
|
52821
52871
|
|
|
52822
|
-
|
|
52823
|
-
<
|
|
52824
|
-
<div class="
|
|
52825
|
-
<ion-icon name="notifications-outline" aria-hidden="true" />
|
|
52826
|
-
</div>
|
|
52827
|
-
<div class="notif-row__body">
|
|
52872
|
+
@if (isDebug) {
|
|
52873
|
+
<section class="settings-section rbac-debug">
|
|
52874
|
+
<div class="section-header-row">
|
|
52828
52875
|
<val-title
|
|
52829
52876
|
[props]="{
|
|
52830
|
-
|
|
52831
|
-
color: 'dark',
|
|
52832
|
-
bold: true,
|
|
52833
|
-
content: tt('simpleTitle'),
|
|
52834
|
-
}"
|
|
52835
|
-
/>
|
|
52836
|
-
<val-text
|
|
52837
|
-
[props]="{
|
|
52877
|
+
content: 'RBAC (live)',
|
|
52838
52878
|
size: 'small',
|
|
52839
52879
|
color: 'medium',
|
|
52840
52880
|
bold: false,
|
|
52841
|
-
content: tt('simpleDescription'),
|
|
52842
52881
|
}"
|
|
52843
52882
|
/>
|
|
52844
|
-
@if (busyLabel(); as label) {
|
|
52845
|
-
<span class="notif-row__busy">{{ label }}</span>
|
|
52846
|
-
}
|
|
52847
|
-
</div>
|
|
52848
|
-
<div class="notif-row__control">
|
|
52849
|
-
<val-toggle-input [props]="toggleProps()" />
|
|
52850
52883
|
</div>
|
|
52851
|
-
|
|
52852
|
-
|
|
52853
|
-
|
|
52854
|
-
|
|
52855
|
-
|
|
52856
|
-
<
|
|
52857
|
-
|
|
52858
|
-
|
|
52859
|
-
|
|
52860
|
-
@if (state() === 'error') {
|
|
52861
|
-
<div class="row-actions">
|
|
52862
|
-
<val-button [props]="retryButtonProps()" (click)="onRetry()" />
|
|
52884
|
+
<div class="rbac-debug__body">
|
|
52885
|
+
<div class="rbac-debug__row">
|
|
52886
|
+
<span class="rbac-debug__label">Roles</span>
|
|
52887
|
+
<span class="rbac-debug__value">{{ auth.roles().join(', ') || '—' }}</span>
|
|
52888
|
+
</div>
|
|
52889
|
+
<div class="rbac-debug__row">
|
|
52890
|
+
<span class="rbac-debug__label">Permisos</span>
|
|
52891
|
+
<span class="rbac-debug__value rbac-debug__value--perms">{{ auth.permissions().join(', ') || '—' }}</span>
|
|
52892
|
+
</div>
|
|
52863
52893
|
</div>
|
|
52864
|
-
}
|
|
52865
|
-
</section>
|
|
52866
|
-
|
|
52867
|
-
@if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
|
|
52868
|
-
<section class="settings-section">
|
|
52869
|
-
<button class="details-toggle" type="button" (click)="toggleDetails()">
|
|
52870
|
-
<span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
|
|
52871
|
-
<ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
|
|
52872
|
-
</button>
|
|
52873
|
-
@if (showDetails()) {
|
|
52874
|
-
<dl class="details">
|
|
52875
|
-
<div class="details__row">
|
|
52876
|
-
<dt>{{ tt('detailsToken') }}</dt>
|
|
52877
|
-
<dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
|
|
52878
|
-
</div>
|
|
52879
|
-
<div class="details__row">
|
|
52880
|
-
<dt>{{ tt('diagnosticsPermission') }}</dt>
|
|
52881
|
-
<dd>{{ permission() }}</dd>
|
|
52882
|
-
</div>
|
|
52883
|
-
<div class="details__row">
|
|
52884
|
-
<dt>{{ tt('diagnosticsSupported') }}</dt>
|
|
52885
|
-
<dd>{{ isSupported() ? '✓' : '✗' }}</dd>
|
|
52886
|
-
</div>
|
|
52887
|
-
</dl>
|
|
52888
|
-
}
|
|
52889
52894
|
</section>
|
|
52890
52895
|
}
|
|
52896
|
+
<val-permissions-modal
|
|
52897
|
+
[isOpen]="permissionsOpen()"
|
|
52898
|
+
[config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
|
|
52899
|
+
(dismissed)="permissionsOpen.set(false)"
|
|
52900
|
+
/>
|
|
52891
52901
|
</div>
|
|
52892
|
-
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:
|
|
52902
|
+
`, 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}: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)}\n"] }]
|
|
52893
52903
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
52894
52904
|
type: Input
|
|
52895
52905
|
}] } });
|
|
52896
52906
|
|
|
52897
52907
|
/**
|
|
52898
|
-
* Helper para montar
|
|
52899
|
-
*
|
|
52900
|
-
* `
|
|
52901
|
-
*
|
|
52902
|
-
*
|
|
52903
|
-
* No confundir con `provideValtechNotificationsRoutes` (el inbox / feed).
|
|
52908
|
+
* Helper para montar la vista Gestión de organización (`val-organization-view`)
|
|
52909
|
+
* como ruta en una app del factory. El `config` se pasa por route `data`
|
|
52910
|
+
* (`organizationConfig`) y el componente lo lee como fallback de su
|
|
52911
|
+
* `@Input() config`.
|
|
52904
52912
|
*
|
|
52905
52913
|
* @example
|
|
52906
52914
|
* ```ts
|
|
52907
|
-
* //
|
|
52915
|
+
* // settings.routes.ts
|
|
52908
52916
|
* export const settingsRoutes: Routes = [
|
|
52909
|
-
* ...
|
|
52917
|
+
* ...provideValtechOrganizationRoutes(), // → /app/settings/organization
|
|
52918
|
+
* { path: 'preferences', loadComponent: () => ... },
|
|
52910
52919
|
* ];
|
|
52911
52920
|
*
|
|
52912
52921
|
* // con config acotada:
|
|
52913
|
-
* ...
|
|
52914
|
-
* ...
|
|
52922
|
+
* ...provideValtechOrganizationRoutes({ config: { showLeave: false } }),
|
|
52923
|
+
* ...provideValtechOrganizationRoutes({ config: { viewPermissionsRoute: '/app/roles' } }),
|
|
52915
52924
|
* ```
|
|
52916
52925
|
*/
|
|
52917
|
-
function
|
|
52926
|
+
function provideValtechOrganizationRoutes(opts) {
|
|
52918
52927
|
return [
|
|
52919
52928
|
{
|
|
52920
|
-
path: opts?.path ?? '
|
|
52921
|
-
component:
|
|
52922
|
-
data: {
|
|
52929
|
+
path: opts?.path ?? 'organization',
|
|
52930
|
+
component: OrganizationViewComponent,
|
|
52931
|
+
data: { organizationConfig: opts?.config },
|
|
52923
52932
|
},
|
|
52924
52933
|
];
|
|
52925
52934
|
}
|
|
52926
52935
|
|
|
52927
52936
|
/**
|
|
52928
|
-
* Defaults i18n (es/en) embebidos en `val-
|
|
52929
|
-
* el constructor del componente si el consumer no proveyó
|
|
52930
|
-
* (`Settings.
|
|
52937
|
+
* Defaults i18n (es/en) embebidos en `val-notification-preferences-view`.
|
|
52938
|
+
* Auto-registrados en el constructor del componente si el consumer no proveyó
|
|
52939
|
+
* el namespace (`Settings.Notifications` por default). SOLO incluye las keys que
|
|
52940
|
+
* la vista usa.
|
|
52931
52941
|
*
|
|
52932
|
-
* Portado
|
|
52942
|
+
* Portado del override `Settings.Notifications` (showcase) bajo el proceso de
|
|
52943
|
+
* ADR-021.
|
|
52933
52944
|
*/
|
|
52934
|
-
const
|
|
52945
|
+
const NOTIFICATION_PREFERENCES_VIEW_I18N = {
|
|
52935
52946
|
es: {
|
|
52936
|
-
pageTitle: '
|
|
52937
|
-
pageDescription: '
|
|
52938
|
-
|
|
52939
|
-
|
|
52940
|
-
|
|
52941
|
-
|
|
52942
|
-
|
|
52943
|
-
|
|
52944
|
-
|
|
52945
|
-
|
|
52946
|
-
|
|
52947
|
-
|
|
52948
|
-
|
|
52949
|
-
|
|
52950
|
-
|
|
52951
|
-
|
|
52952
|
-
|
|
52953
|
-
|
|
52954
|
-
|
|
52955
|
-
|
|
52956
|
-
|
|
52957
|
-
|
|
52958
|
-
|
|
52959
|
-
|
|
52960
|
-
|
|
52961
|
-
|
|
52962
|
-
|
|
52963
|
-
|
|
52964
|
-
|
|
52965
|
-
|
|
52966
|
-
|
|
52967
|
-
|
|
52968
|
-
|
|
52969
|
-
|
|
52970
|
-
|
|
52971
|
-
|
|
52972
|
-
|
|
52973
|
-
|
|
52974
|
-
|
|
52975
|
-
|
|
52976
|
-
|
|
52977
|
-
|
|
52978
|
-
|
|
52979
|
-
|
|
52980
|
-
|
|
52981
|
-
|
|
52982
|
-
|
|
52983
|
-
|
|
52984
|
-
|
|
52985
|
-
|
|
52986
|
-
|
|
52987
|
-
|
|
52988
|
-
|
|
52989
|
-
|
|
52990
|
-
|
|
52991
|
-
|
|
52992
|
-
|
|
52993
|
-
pageTitle: 'Permissions',
|
|
52994
|
-
pageDescription: 'Roles and permissions of this app in the active organization',
|
|
52995
|
-
systemRolesTitle: 'System roles',
|
|
52996
|
-
systemRolesHint: 'Defined by the platform. Not editable.',
|
|
52997
|
-
customRolesTitle: 'Custom roles',
|
|
52998
|
-
customRolesHint: 'Defined by your organization.',
|
|
52999
|
-
systemBadge: 'System',
|
|
53000
|
-
noCustomRoles: 'This organization has no custom roles.',
|
|
53001
|
-
noPermissions: 'No permissions assigned.',
|
|
53002
|
-
groupApp: 'App permissions',
|
|
53003
|
-
groupOrg: 'Organization permissions',
|
|
53004
|
-
groupPlatform: 'Platform permissions',
|
|
53005
|
-
groupOther: 'Other apps',
|
|
53006
|
-
roleViewer: 'Viewer',
|
|
53007
|
-
roleEditor: 'Editor',
|
|
53008
|
-
roleAdmin: 'Administrator',
|
|
53009
|
-
roleSuperAdmin: 'Super Administrator',
|
|
53010
|
-
roleBingoAdmin: 'Bingo · Administrator',
|
|
53011
|
-
roleBingoTesorero: 'Bingo · Treasurer',
|
|
53012
|
-
roleBingoTaquilla: 'Bingo · Box office',
|
|
53013
|
-
roleBingoOperator: 'Bingo · Operator',
|
|
53014
|
-
app_showcase: 'Showcase',
|
|
53015
|
-
app_bingo: 'Bingo',
|
|
53016
|
-
permUsers: 'Users',
|
|
53017
|
-
permRoles: 'Roles',
|
|
53018
|
-
permRbac: 'Access control',
|
|
53019
|
-
permApps: 'Apps',
|
|
53020
|
-
'permApi-keys': 'API keys',
|
|
53021
|
-
'permEmail-logs': 'Email logs',
|
|
53022
|
-
'permSms-logs': 'SMS logs',
|
|
53023
|
-
permDocuments: 'Documents',
|
|
53024
|
-
permMedia: 'Media',
|
|
53025
|
-
permTemplates: 'Templates',
|
|
53026
|
-
permRequests: 'Requests',
|
|
53027
|
-
permBingo: 'Bingo',
|
|
53028
|
-
permRead: 'Read',
|
|
53029
|
-
permWrite: 'Write',
|
|
53030
|
-
permCreate: 'Create',
|
|
53031
|
-
permDelete: 'Delete',
|
|
53032
|
-
permManage: 'Manage',
|
|
53033
|
-
permEdit: 'Edit',
|
|
53034
|
-
permSell: 'Sell',
|
|
53035
|
-
permCheckin: 'Check-in',
|
|
53036
|
-
permSettle: 'Settle',
|
|
53037
|
-
permExport: 'Export',
|
|
53038
|
-
permOperate: 'Operate',
|
|
53039
|
-
permAll: 'All',
|
|
53040
|
-
errorTitle: 'Error loading permissions',
|
|
53041
|
-
retry: 'Retry',
|
|
52947
|
+
pageTitle: 'Notificaciones',
|
|
52948
|
+
pageDescription: 'Configura como quieres recibir notificaciones',
|
|
52949
|
+
// Control principal (UX simplificado)
|
|
52950
|
+
simpleTitle: 'Notificaciones push en este dispositivo',
|
|
52951
|
+
simpleDescription: 'Recibe alertas en tiempo real incluso con la app cerrada.',
|
|
52952
|
+
enabling: 'Activando…',
|
|
52953
|
+
disabling: 'Desactivando…',
|
|
52954
|
+
enabled: 'Activadas',
|
|
52955
|
+
errorGeneric: 'No pudimos completar la configuración. Verifica los permisos del navegador y reintenta.',
|
|
52956
|
+
retryCta: 'Reintentar',
|
|
52957
|
+
// Mensajes contextuales
|
|
52958
|
+
permissionUnsupported: 'Tu navegador no soporta notificaciones push.',
|
|
52959
|
+
permissionDeniedInfo: 'Tu navegador esta bloqueando las notificaciones. Para habilitarlas, ve a la configuracion del sitio y permitelas manualmente.',
|
|
52960
|
+
// iOS PWA install
|
|
52961
|
+
iosInstallTitle: 'En iOS, instala la app para recibir notificaciones',
|
|
52962
|
+
iosInstallHint: 'Las notificaciones push en iOS (iPhone/iPad) solo funcionan cuando la app está instalada en la pantalla de inicio. Edge, Chrome y Firefox en iOS comparten esta limitación.',
|
|
52963
|
+
iosInstallStep1: 'Abre este sitio en Safari (no en Chrome/Edge).',
|
|
52964
|
+
iosInstallStep2: 'Toca el botón Compartir (□ con flecha hacia arriba).',
|
|
52965
|
+
iosInstallStep3: "Elige 'Añadir a pantalla de inicio'.",
|
|
52966
|
+
iosInstallStep4: 'Abre la app desde el ícono recién creado y vuelve aquí.',
|
|
52967
|
+
// Detalles técnicos (expandible)
|
|
52968
|
+
detailsShow: 'Ver detalles técnicos',
|
|
52969
|
+
detailsHide: 'Ocultar detalles',
|
|
52970
|
+
detailsToken: 'Token',
|
|
52971
|
+
detailsNone: 'Sin registrar',
|
|
52972
|
+
diagnosticsPermission: 'Permiso del navegador',
|
|
52973
|
+
diagnosticsSupported: 'Soporte del navegador',
|
|
52974
|
+
// Toasts
|
|
52975
|
+
enableSuccess: 'Notificaciones habilitadas',
|
|
52976
|
+
disableSuccess: 'Notificaciones deshabilitadas',
|
|
52977
|
+
},
|
|
52978
|
+
en: {
|
|
52979
|
+
pageTitle: 'Notifications',
|
|
52980
|
+
pageDescription: 'Configure how you want to receive notifications',
|
|
52981
|
+
simpleTitle: 'Push notifications on this device',
|
|
52982
|
+
simpleDescription: 'Get real-time alerts even when the app is closed.',
|
|
52983
|
+
enabling: 'Enabling…',
|
|
52984
|
+
disabling: 'Disabling…',
|
|
52985
|
+
enabled: 'Enabled',
|
|
52986
|
+
errorGeneric: "We couldn't complete the setup. Check your browser permissions and retry.",
|
|
52987
|
+
retryCta: 'Retry',
|
|
52988
|
+
permissionUnsupported: 'Your browser does not support push notifications.',
|
|
52989
|
+
permissionDeniedInfo: 'Your browser is blocking notifications. To enable them, go to site settings and allow them manually.',
|
|
52990
|
+
iosInstallTitle: 'On iOS, install the app to receive notifications',
|
|
52991
|
+
iosInstallHint: 'Push notifications on iOS (iPhone/iPad) only work when the app is installed on the Home Screen. Edge, Chrome and Firefox on iOS share this limitation.',
|
|
52992
|
+
iosInstallStep1: 'Open this site in Safari (not Chrome/Edge).',
|
|
52993
|
+
iosInstallStep2: 'Tap the Share button (□ with an up arrow).',
|
|
52994
|
+
iosInstallStep3: "Choose 'Add to Home Screen'.",
|
|
52995
|
+
iosInstallStep4: 'Open the app from the new icon and come back here.',
|
|
52996
|
+
detailsShow: 'View technical details',
|
|
52997
|
+
detailsHide: 'Hide details',
|
|
52998
|
+
detailsToken: 'Token',
|
|
52999
|
+
detailsNone: 'Not registered',
|
|
53000
|
+
diagnosticsPermission: 'Browser permission',
|
|
53001
|
+
diagnosticsSupported: 'Browser support',
|
|
53002
|
+
enableSuccess: 'Notifications enabled',
|
|
53003
|
+
disableSuccess: 'Notifications disabled',
|
|
53042
53004
|
},
|
|
53043
53005
|
};
|
|
53044
53006
|
|
|
53045
|
-
|
|
53007
|
+
addIcons({
|
|
53008
|
+
alertCircleOutline,
|
|
53009
|
+
chevronDownOutline,
|
|
53010
|
+
chevronUpOutline,
|
|
53011
|
+
closeCircleOutline,
|
|
53012
|
+
notificationsOffOutline,
|
|
53013
|
+
notificationsOutline,
|
|
53014
|
+
refreshOutline,
|
|
53015
|
+
});
|
|
53016
|
+
const DEFAULT_NAMESPACE$5 = 'Settings.Notifications';
|
|
53046
53017
|
/**
|
|
53047
|
-
* `val-
|
|
53048
|
-
*
|
|
53049
|
-
*
|
|
53050
|
-
* catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
|
|
53051
|
-
* Promovida desde `showcase` bajo el proceso de ADR-021.
|
|
53018
|
+
* `val-notification-preferences-view` — **preferencias de notificaciones push**
|
|
53019
|
+
* (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
|
|
53020
|
+
* proceso de ADR-021.
|
|
53052
53021
|
*
|
|
53053
|
-
*
|
|
53054
|
-
*
|
|
53055
|
-
* puede verla es responsabilidad del guard de ruta del consumer; no hay gating
|
|
53056
|
-
* fino interno ni se expone como config.
|
|
53022
|
+
* Separada de `val-notifications-view` (el inbox / feed). Aquí el user **activa o
|
|
53023
|
+
* desactiva** las notificaciones push en este dispositivo.
|
|
53057
53024
|
*
|
|
53058
|
-
*
|
|
53059
|
-
*
|
|
53060
|
-
*
|
|
53025
|
+
* UX simplificado: UN solo toggle que ejecuta toda la secuencia (pedir permiso →
|
|
53026
|
+
* SW ready → getToken → registrar en backend → set master pref) vía
|
|
53027
|
+
* `MessagingService.enable()`. El registro del device en el backend va como
|
|
53028
|
+
* callback (la lib no depende de `AuthService` para evitar ciclos). Mensajes
|
|
53029
|
+
* contextuales y botón "Reintentar" aparecen solo cuando aplican.
|
|
53061
53030
|
*
|
|
53062
53031
|
* NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
|
|
53063
|
-
* inyecta `{ optional: true }`
|
|
53064
|
-
* sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
|
|
53065
|
-
* `Settings.Permissions`).
|
|
53032
|
+
* inyecta `{ optional: true }` → funciona sin router (embebida / Storybook).
|
|
53066
53033
|
*/
|
|
53067
|
-
class
|
|
53034
|
+
class NotificationPreferencesViewComponent {
|
|
53068
53035
|
/** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
|
|
53069
53036
|
get ns() {
|
|
53070
53037
|
return this.resolvedConfig().i18nNamespace;
|
|
53071
53038
|
}
|
|
53039
|
+
// ---------------------------------------------------------------------------
|
|
53040
|
+
// Lifecycle
|
|
53041
|
+
// ---------------------------------------------------------------------------
|
|
53072
53042
|
constructor() {
|
|
53043
|
+
this.nav = inject(NavigationService);
|
|
53073
53044
|
this.i18n = inject(I18nService);
|
|
53074
|
-
this.
|
|
53075
|
-
this.catalog = inject(PermissionCatalogService);
|
|
53045
|
+
this.toast = inject(ToastService);
|
|
53076
53046
|
this.auth = inject(AuthService);
|
|
53077
|
-
this.
|
|
53047
|
+
this.messaging = inject(MessagingService);
|
|
53048
|
+
this.pageRefresh = inject(PageRefreshService);
|
|
53049
|
+
this.prefs = inject(PreferencesService);
|
|
53050
|
+
// `ActivatedRoute` opcional → la vista funciona embebida / en Storybook sin
|
|
53051
|
+
// router (solo lo usa para leer el route data de config como fallback).
|
|
53078
53052
|
this.route = inject(ActivatedRoute, { optional: true });
|
|
53079
53053
|
this.resolvedConfig = computed(() => {
|
|
53080
|
-
const fromRoute = (this.route?.snapshot.data['
|
|
53054
|
+
const fromRoute = (this.route?.snapshot.data['notificationPreferencesConfig'] ??
|
|
53055
|
+
{});
|
|
53081
53056
|
const merged = { ...fromRoute, ...(this.config ?? {}) };
|
|
53082
53057
|
return {
|
|
53083
|
-
|
|
53084
|
-
showCustomRoles: merged.showCustomRoles ?? true,
|
|
53058
|
+
showDetails: merged.showDetails ?? true,
|
|
53085
53059
|
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$5,
|
|
53060
|
+
onPushEnabled: merged.onPushEnabled,
|
|
53061
|
+
onPushDisabled: merged.onPushDisabled,
|
|
53086
53062
|
};
|
|
53087
53063
|
});
|
|
53088
|
-
|
|
53089
|
-
this.
|
|
53090
|
-
|
|
53091
|
-
this.
|
|
53092
|
-
|
|
53093
|
-
this.
|
|
53094
|
-
|
|
53095
|
-
|
|
53064
|
+
/** Operación en curso (gobierna spinner del toggle + label "Activando…"). */
|
|
53065
|
+
this._busy = signal(null);
|
|
53066
|
+
/** True si el último intento falló — gobierna estado `error` + retry button. */
|
|
53067
|
+
this._lastFailed = signal(false);
|
|
53068
|
+
/** True mientras se re-obtiene el token FCM en background (no afecta la UI). */
|
|
53069
|
+
this._repairingToken = false;
|
|
53070
|
+
/** Toggle del expand de detalles técnicos. */
|
|
53071
|
+
this.showDetails = signal(false);
|
|
53072
|
+
this.permission = signal('default');
|
|
53073
|
+
this.isSupported = signal(false);
|
|
53074
|
+
this.currentToken = signal(null);
|
|
53075
|
+
this.needsIOSInstall = signal(this.detectNeedsIOSInstall());
|
|
53076
|
+
/** FormControl del toggle (sync-eado con `isEnabled` via effect). */
|
|
53077
|
+
this.toggleControl = new FormControl(false, {
|
|
53078
|
+
nonNullable: true,
|
|
53079
|
+
});
|
|
53080
|
+
/** Handler del listener `focus` — referencia guardada para poder removerlo. */
|
|
53081
|
+
this.onWindowFocus = () => this.scheduleBrowserRefresh();
|
|
53082
|
+
/** Timer del debounce del refresh disparado por `focus`. */
|
|
53083
|
+
this.focusDebounceTimer = null;
|
|
53084
|
+
/** Ventana de debounce (ms) para el refresh disparado por `focus`. */
|
|
53085
|
+
this.FOCUS_DEBOUNCE_MS = 500;
|
|
53086
|
+
// ---------------------------------------------------------------------------
|
|
53087
|
+
// Estado derivado
|
|
53088
|
+
// ---------------------------------------------------------------------------
|
|
53089
|
+
this.pageTitle = computed(() => this.tt('pageTitle'));
|
|
53090
|
+
this.pageDescription = computed(() => this.tt('pageDescription'));
|
|
53091
|
+
/**
|
|
53092
|
+
* Estado efectivo del toggle: "el usuario tiene push activo".
|
|
53093
|
+
*
|
|
53094
|
+
* Se deriva SOLO de la decisión del usuario (`master`) y del permiso del SO
|
|
53095
|
+
* (`permission`). El token FCM NO forma parte del estado de UI: vive en
|
|
53096
|
+
* memoria, se pierde en cada recarga/suspensión de la PWA y produciría flicker.
|
|
53097
|
+
* Si falta token con `master && granted`, es un detalle reparable en
|
|
53098
|
+
* background (ver `repairTokenIfNeeded`), no un cambio del toggle.
|
|
53099
|
+
*/
|
|
53100
|
+
this.isEnabled = computed(() => this.prefs.notificationsMaster() && this.permission() === 'granted');
|
|
53101
|
+
this.state = computed(() => {
|
|
53102
|
+
if (this.needsIOSInstall())
|
|
53103
|
+
return 'needs-install';
|
|
53104
|
+
if (this.permission() === 'unsupported' || !this.isSupported()) {
|
|
53105
|
+
// iOS standalone aún puede dar isSupported false temprano antes de init —
|
|
53106
|
+
// solo bloqueamos si el browser explícitamente no soporta APIs.
|
|
53107
|
+
if (this.permission() === 'unsupported')
|
|
53108
|
+
return 'unsupported';
|
|
53109
|
+
}
|
|
53110
|
+
const busy = this._busy();
|
|
53111
|
+
if (busy === 'enabling')
|
|
53112
|
+
return 'enabling';
|
|
53113
|
+
if (busy === 'disabling')
|
|
53114
|
+
return 'disabling';
|
|
53115
|
+
if (this.isEnabled())
|
|
53116
|
+
return 'enabled';
|
|
53117
|
+
if (this.permission() === 'denied')
|
|
53118
|
+
return 'blocked';
|
|
53119
|
+
if (this._lastFailed())
|
|
53120
|
+
return 'error';
|
|
53121
|
+
return 'idle';
|
|
53122
|
+
});
|
|
53123
|
+
this.busyLabel = computed(() => {
|
|
53124
|
+
const s = this.state();
|
|
53125
|
+
if (s === 'enabling')
|
|
53126
|
+
return this.tt('enabling');
|
|
53127
|
+
if (s === 'disabling')
|
|
53128
|
+
return this.tt('disabling');
|
|
53129
|
+
return null;
|
|
53130
|
+
});
|
|
53131
|
+
this.contextMessage = computed(() => {
|
|
53132
|
+
switch (this.state()) {
|
|
53133
|
+
case 'unsupported':
|
|
53134
|
+
return this.tt('permissionUnsupported');
|
|
53135
|
+
case 'blocked':
|
|
53136
|
+
return this.tt('permissionDeniedInfo');
|
|
53137
|
+
case 'error':
|
|
53138
|
+
return this.tt('errorGeneric');
|
|
53139
|
+
default:
|
|
53140
|
+
return null;
|
|
53141
|
+
}
|
|
53142
|
+
});
|
|
53143
|
+
this.alertVariant = computed(() => {
|
|
53144
|
+
const s = this.state();
|
|
53145
|
+
if (s === 'error' || s === 'blocked')
|
|
53146
|
+
return 'warning';
|
|
53147
|
+
if (s === 'unsupported')
|
|
53148
|
+
return 'danger';
|
|
53149
|
+
return 'info';
|
|
53150
|
+
});
|
|
53151
|
+
this.alertIcon = computed(() => {
|
|
53152
|
+
const s = this.state();
|
|
53153
|
+
if (s === 'unsupported')
|
|
53154
|
+
return 'close-circle-outline';
|
|
53155
|
+
return 'alert-circle-outline';
|
|
53096
53156
|
});
|
|
53157
|
+
this.toggleProps = computed(() => ({
|
|
53158
|
+
control: this.toggleControl,
|
|
53159
|
+
color: 'primary',
|
|
53160
|
+
disabled: this._busy() !== null ||
|
|
53161
|
+
this.state() === 'unsupported' ||
|
|
53162
|
+
this.state() === 'needs-install' ||
|
|
53163
|
+
this.state() === 'blocked',
|
|
53164
|
+
}));
|
|
53097
53165
|
this.retryButtonProps = computed(() => ({
|
|
53098
|
-
text: this.tt('
|
|
53166
|
+
text: this.tt('retryCta'),
|
|
53099
53167
|
color: 'dark',
|
|
53100
53168
|
fill: 'outline',
|
|
53101
53169
|
size: 'default',
|
|
53170
|
+
shape: 'round',
|
|
53102
53171
|
type: 'button',
|
|
53103
|
-
|
|
53172
|
+
icon: { name: 'refresh-outline', slot: 'start' },
|
|
53173
|
+
state: this._busy() !== null ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
53104
53174
|
}));
|
|
53175
|
+
this.tokenPreview = computed(() => {
|
|
53176
|
+
const t = this.currentToken();
|
|
53177
|
+
if (!t)
|
|
53178
|
+
return null;
|
|
53179
|
+
return `${t.slice(0, 22)}…${t.slice(-6)}`;
|
|
53180
|
+
});
|
|
53105
53181
|
const ns = this.ns;
|
|
53106
53182
|
if (!this.i18n.hasNamespace(ns)) {
|
|
53107
|
-
this.i18n.registerContent(ns,
|
|
53183
|
+
this.i18n.registerContent(ns, NOTIFICATION_PREFERENCES_VIEW_I18N);
|
|
53108
53184
|
}
|
|
53109
53185
|
this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
|
|
53110
|
-
this.
|
|
53111
|
-
|
|
53112
|
-
|
|
53113
|
-
|
|
53114
|
-
|
|
53115
|
-
|
|
53116
|
-
|
|
53117
|
-
|
|
53118
|
-
|
|
53119
|
-
|
|
53120
|
-
|
|
53121
|
-
|
|
53122
|
-
|
|
53123
|
-
|
|
53124
|
-
|
|
53125
|
-
|
|
53126
|
-
|
|
53127
|
-
|
|
53128
|
-
|
|
53129
|
-
|
|
53130
|
-
|
|
53131
|
-
|
|
53132
|
-
|
|
53133
|
-
|
|
53134
|
-
|
|
53135
|
-
|
|
53136
|
-
|
|
53137
|
-
|
|
53138
|
-
|
|
53139
|
-
|
|
53140
|
-
|
|
53141
|
-
this.
|
|
53142
|
-
}
|
|
53143
|
-
error: err => {
|
|
53144
|
-
this.loadError.set(err);
|
|
53145
|
-
this.loading.set(false);
|
|
53146
|
-
},
|
|
53186
|
+
void this.refreshBrowserState();
|
|
53187
|
+
// Re-aplicar header en cambio de idioma.
|
|
53188
|
+
effect(() => {
|
|
53189
|
+
this.i18n.lang();
|
|
53190
|
+
this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
|
|
53191
|
+
}, { allowSignalWrites: true });
|
|
53192
|
+
// Sync FormControl ←→ estado efectivo. Cuando el estado cambia (cambio de
|
|
53193
|
+
// permission por el browser, o snapshot Firestore de `master`), el toggle se
|
|
53194
|
+
// re-alinea con la realidad.
|
|
53195
|
+
//
|
|
53196
|
+
// IMPORTANTE: se ignoran los cambios mientras hay una operación en curso
|
|
53197
|
+
// (`_busy() !== null`). Durante `onEnable`/`onDisable` las señales fuente se
|
|
53198
|
+
// actualizan en momentos distintos y producirían estados intermedios →
|
|
53199
|
+
// flicker visible. El toggle se re-alinea UNA sola vez en el `finally`.
|
|
53200
|
+
effect(() => {
|
|
53201
|
+
const eff = this.isEnabled();
|
|
53202
|
+
if (this._busy() !== null)
|
|
53203
|
+
return;
|
|
53204
|
+
if (this.toggleControl.value !== eff) {
|
|
53205
|
+
this.toggleControl.setValue(eff, { emitEvent: false });
|
|
53206
|
+
}
|
|
53207
|
+
}, { allowSignalWrites: true });
|
|
53208
|
+
// Interceptar click del user: el toggle ya cambió visualmente, ejecutar
|
|
53209
|
+
// la acción correspondiente.
|
|
53210
|
+
this.toggleControl.valueChanges.subscribe(v => {
|
|
53211
|
+
if (this._busy() !== null)
|
|
53212
|
+
return;
|
|
53213
|
+
if (v && !this.isEnabled()) {
|
|
53214
|
+
void this.onEnable();
|
|
53215
|
+
}
|
|
53216
|
+
else if (!v && this.isEnabled()) {
|
|
53217
|
+
void this.onDisable();
|
|
53218
|
+
}
|
|
53147
53219
|
});
|
|
53220
|
+
// Detectar regreso a la pestaña — el user pudo haber cambiado permisos
|
|
53221
|
+
// desde la config del browser. El refresh va con debounce para no
|
|
53222
|
+
// dispararse en ráfagas (en PWA mobile `focus` salta con cada cambio de
|
|
53223
|
+
// app / aparición del teclado). El listener se remueve en ngOnDestroy.
|
|
53224
|
+
if (typeof window !== 'undefined') {
|
|
53225
|
+
window.addEventListener('focus', this.onWindowFocus);
|
|
53226
|
+
}
|
|
53148
53227
|
}
|
|
53149
|
-
|
|
53150
|
-
|
|
53151
|
-
|
|
53152
|
-
|
|
53153
|
-
|
|
53154
|
-
|
|
53155
|
-
|
|
53156
|
-
|
|
53157
|
-
|
|
53158
|
-
|
|
53159
|
-
|
|
53160
|
-
|
|
53161
|
-
|
|
53162
|
-
|
|
53163
|
-
|
|
53164
|
-
|
|
53165
|
-
|
|
53166
|
-
|
|
53167
|
-
|
|
53168
|
-
|
|
53169
|
-
|
|
53170
|
-
|
|
53171
|
-
|
|
53172
|
-
|
|
53173
|
-
|
|
53228
|
+
/**
|
|
53229
|
+
* Registra el pull-to-refresh estándar. Aunque esta vista no muestra una
|
|
53230
|
+
* lista remota, sí refleja estado mutable del browser (permiso, token,
|
|
53231
|
+
* soporte FCM) que puede cambiar fuera de la app — el refresh re-evalúa todo.
|
|
53232
|
+
*
|
|
53233
|
+
* Se usa `ngOnInit`/`ngOnDestroy` (NO `ionViewWillEnter`) porque
|
|
53234
|
+
* `val-page-wrapper` usa `<router-outlet>` plano — los hooks `ionView*` solo
|
|
53235
|
+
* disparan dentro de `<ion-router-outlet>`.
|
|
53236
|
+
*/
|
|
53237
|
+
ngOnInit() {
|
|
53238
|
+
this.pageRefresh.register(() => this.refreshBrowserState());
|
|
53239
|
+
}
|
|
53240
|
+
ngOnDestroy() {
|
|
53241
|
+
this.pageRefresh.unregister();
|
|
53242
|
+
if (typeof window !== 'undefined') {
|
|
53243
|
+
window.removeEventListener('focus', this.onWindowFocus);
|
|
53244
|
+
}
|
|
53245
|
+
if (this.focusDebounceTimer !== null) {
|
|
53246
|
+
clearTimeout(this.focusDebounceTimer);
|
|
53247
|
+
this.focusDebounceTimer = null;
|
|
53248
|
+
}
|
|
53249
|
+
}
|
|
53250
|
+
/** Refresca el estado del browser con debounce — evita ráfagas de `focus`. */
|
|
53251
|
+
scheduleBrowserRefresh() {
|
|
53252
|
+
if (this.focusDebounceTimer !== null) {
|
|
53253
|
+
clearTimeout(this.focusDebounceTimer);
|
|
53254
|
+
}
|
|
53255
|
+
this.focusDebounceTimer = setTimeout(() => {
|
|
53256
|
+
this.focusDebounceTimer = null;
|
|
53257
|
+
void this.refreshBrowserState();
|
|
53258
|
+
}, this.FOCUS_DEBOUNCE_MS);
|
|
53259
|
+
}
|
|
53260
|
+
// ---------------------------------------------------------------------------
|
|
53261
|
+
// Actions
|
|
53262
|
+
// ---------------------------------------------------------------------------
|
|
53263
|
+
/**
|
|
53264
|
+
* Secuencia completa de activación.
|
|
53265
|
+
*
|
|
53266
|
+
* Toda la mecánica (permission → SW ready → getToken con retry → watchdog de
|
|
53267
|
+
* auto-reload) vive en `MessagingService.enable()` de la lib. El paso de
|
|
53268
|
+
* registro del device en el backend se pasa como callback (`registerDevice`)
|
|
53269
|
+
* porque la lib no puede depender de `AuthService` sin crear un ciclo.
|
|
53270
|
+
*
|
|
53271
|
+
* `enable()` no hace throw: resuelve con un `EnablePushResult` descriptivo.
|
|
53272
|
+
*/
|
|
53273
|
+
async onEnable() {
|
|
53274
|
+
this._busy.set('enabling');
|
|
53275
|
+
this._lastFailed.set(false);
|
|
53276
|
+
try {
|
|
53277
|
+
const result = await this.messaging.enable({
|
|
53278
|
+
registerDevice: token => this.auth.registerDevice(token).then(r => r.registered),
|
|
53279
|
+
});
|
|
53280
|
+
if (result.status === 'enabled') {
|
|
53281
|
+
// Persistir flag master si aún no estaba activo.
|
|
53282
|
+
if (!this.prefs.notificationsMaster()) {
|
|
53283
|
+
await this.prefs.setNotificationsMaster(true);
|
|
53174
53284
|
}
|
|
53285
|
+
this.toast.show({
|
|
53286
|
+
message: this.tt('enableSuccess'),
|
|
53287
|
+
duration: 2000,
|
|
53288
|
+
color: 'dark',
|
|
53289
|
+
position: 'top',
|
|
53290
|
+
});
|
|
53291
|
+
this.resolvedConfig().onPushEnabled?.();
|
|
53292
|
+
}
|
|
53293
|
+
else if (result.status === 'timeout' && result.reloaded) {
|
|
53294
|
+
// La lib disparó el auto-reload — la página está por recargarse.
|
|
53295
|
+
return;
|
|
53175
53296
|
}
|
|
53176
53297
|
else {
|
|
53177
|
-
//
|
|
53178
|
-
|
|
53298
|
+
// denied / unsupported / error / timeout sin reload → fallo recuperable.
|
|
53299
|
+
this._lastFailed.set(true);
|
|
53300
|
+
this.toast.show({
|
|
53301
|
+
message: this.tt('errorGeneric'),
|
|
53302
|
+
duration: 3000,
|
|
53303
|
+
color: 'dark',
|
|
53304
|
+
position: 'top',
|
|
53305
|
+
});
|
|
53179
53306
|
}
|
|
53180
53307
|
}
|
|
53181
|
-
|
|
53182
|
-
|
|
53183
|
-
|
|
53184
|
-
|
|
53185
|
-
|
|
53186
|
-
|
|
53187
|
-
|
|
53188
|
-
|
|
53189
|
-
perms: orgPerms,
|
|
53308
|
+
catch {
|
|
53309
|
+
// `enable()` no hace throw, pero el callback de registro sí puede.
|
|
53310
|
+
this._lastFailed.set(true);
|
|
53311
|
+
this.toast.show({
|
|
53312
|
+
message: this.tt('errorGeneric'),
|
|
53313
|
+
duration: 3000,
|
|
53314
|
+
color: 'dark',
|
|
53315
|
+
position: 'top',
|
|
53190
53316
|
});
|
|
53191
53317
|
}
|
|
53192
|
-
|
|
53193
|
-
|
|
53194
|
-
|
|
53195
|
-
|
|
53196
|
-
|
|
53318
|
+
finally {
|
|
53319
|
+
// Único refresh final: re-alinea el toggle UNA sola vez con el estado real.
|
|
53320
|
+
await this.refreshBrowserState();
|
|
53321
|
+
this._busy.set(null);
|
|
53322
|
+
}
|
|
53323
|
+
}
|
|
53324
|
+
async onDisable() {
|
|
53325
|
+
this._busy.set('disabling');
|
|
53326
|
+
this._lastFailed.set(false);
|
|
53327
|
+
try {
|
|
53328
|
+
await this.auth.disableNotifications();
|
|
53329
|
+
if (this.prefs.notificationsMaster()) {
|
|
53330
|
+
await this.prefs.setNotificationsMaster(false);
|
|
53331
|
+
}
|
|
53332
|
+
this.toast.show({
|
|
53333
|
+
message: this.tt('disableSuccess'),
|
|
53334
|
+
duration: 2000,
|
|
53335
|
+
color: 'dark',
|
|
53336
|
+
position: 'top',
|
|
53197
53337
|
});
|
|
53338
|
+
this.resolvedConfig().onPushDisabled?.();
|
|
53198
53339
|
}
|
|
53199
|
-
|
|
53200
|
-
|
|
53201
|
-
|
|
53202
|
-
|
|
53203
|
-
|
|
53204
|
-
|
|
53205
|
-
|
|
53206
|
-
|
|
53340
|
+
catch {
|
|
53341
|
+
this._lastFailed.set(true);
|
|
53342
|
+
}
|
|
53343
|
+
finally {
|
|
53344
|
+
await this.refreshBrowserState();
|
|
53345
|
+
this._busy.set(null);
|
|
53346
|
+
}
|
|
53347
|
+
}
|
|
53348
|
+
/** Reintentar la secuencia completa de activación, sin importar el estado actual. */
|
|
53349
|
+
async onRetry() {
|
|
53350
|
+
this._lastFailed.set(false);
|
|
53351
|
+
await this.onEnable();
|
|
53352
|
+
}
|
|
53353
|
+
toggleDetails() {
|
|
53354
|
+
this.showDetails.update(v => !v);
|
|
53355
|
+
}
|
|
53356
|
+
// ---------------------------------------------------------------------------
|
|
53357
|
+
// Privados
|
|
53358
|
+
// ---------------------------------------------------------------------------
|
|
53359
|
+
async refreshBrowserState() {
|
|
53360
|
+
this.isSupported.set(await this.messaging.isSupported());
|
|
53361
|
+
this.permission.set(this.auth.getNotificationPermissionState());
|
|
53362
|
+
this.currentToken.set(this.messaging.currentToken ?? null);
|
|
53363
|
+
void this.repairTokenIfNeeded();
|
|
53364
|
+
}
|
|
53365
|
+
/**
|
|
53366
|
+
* Repara el token FCM en background.
|
|
53367
|
+
*
|
|
53368
|
+
* Si el usuario tiene push activo (`master && granted`) pero no hay token en
|
|
53369
|
+
* memoria — caso típico tras un cold start de la PWA — re-obtenemos el token
|
|
53370
|
+
* y re-registramos el device sin tocar el toggle ni mostrar un error
|
|
53371
|
+
* alarmista. El token es un detalle de implementación reparable, no parte del
|
|
53372
|
+
* estado de UI (ver `isEnabled`).
|
|
53373
|
+
*/
|
|
53374
|
+
async repairTokenIfNeeded() {
|
|
53375
|
+
if (this._busy() !== null)
|
|
53376
|
+
return;
|
|
53377
|
+
if (this._repairingToken)
|
|
53378
|
+
return;
|
|
53379
|
+
if (!this.isEnabled())
|
|
53380
|
+
return;
|
|
53381
|
+
if (this.currentToken())
|
|
53382
|
+
return;
|
|
53383
|
+
this._repairingToken = true;
|
|
53384
|
+
try {
|
|
53385
|
+
const token = await this.messaging.getToken();
|
|
53386
|
+
if (token) {
|
|
53387
|
+
this.currentToken.set(token);
|
|
53388
|
+
await this.auth.registerDevice(token);
|
|
53389
|
+
}
|
|
53390
|
+
}
|
|
53391
|
+
catch {
|
|
53392
|
+
// Reparación best-effort: no degradamos el toggle ni mostramos error.
|
|
53393
|
+
}
|
|
53394
|
+
finally {
|
|
53395
|
+
this._repairingToken = false;
|
|
53396
|
+
}
|
|
53397
|
+
}
|
|
53398
|
+
/**
|
|
53399
|
+
* iOS browser sin estar instalado como PWA — push web no disponible.
|
|
53400
|
+
* Aplica a Safari y a Edge/Chrome/Firefox en iOS (todos WebKit por mandato Apple).
|
|
53401
|
+
*/
|
|
53402
|
+
detectNeedsIOSInstall() {
|
|
53403
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
53404
|
+
return false;
|
|
53405
|
+
}
|
|
53406
|
+
const ua = navigator.userAgent || '';
|
|
53407
|
+
const isIOS = /iPad|iPhone|iPod/.test(ua);
|
|
53408
|
+
if (!isIOS)
|
|
53409
|
+
return false;
|
|
53410
|
+
const isStandalone = window.matchMedia?.('(display-mode: standalone)').matches ||
|
|
53411
|
+
navigator.standalone === true;
|
|
53412
|
+
return !isStandalone;
|
|
53207
53413
|
}
|
|
53208
53414
|
tt(key) {
|
|
53209
53415
|
return this.i18n.t(key, this.ns);
|
|
53210
53416
|
}
|
|
53211
|
-
|
|
53212
|
-
|
|
53213
|
-
const label = this.tt(key);
|
|
53214
|
-
return label === key ? appId : label;
|
|
53215
|
-
}
|
|
53216
|
-
roleLabel(name) {
|
|
53217
|
-
const map = {
|
|
53218
|
-
viewer: 'roleViewer',
|
|
53219
|
-
editor: 'roleEditor',
|
|
53220
|
-
admin: 'roleAdmin',
|
|
53221
|
-
super_admin: 'roleSuperAdmin',
|
|
53222
|
-
bingo_admin: 'roleBingoAdmin',
|
|
53223
|
-
bingo_tesorero: 'roleBingoTesorero',
|
|
53224
|
-
bingo_taquilla: 'roleBingoTaquilla',
|
|
53225
|
-
bingo_operator: 'roleBingoOperator',
|
|
53226
|
-
};
|
|
53227
|
-
return this.tt(map[name] ?? name);
|
|
53228
|
-
}
|
|
53229
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
53230
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config" }, ngImport: i0, template: `
|
|
53417
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
53418
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: NotificationPreferencesViewComponent, isStandalone: true, selector: "val-notification-preferences-view", inputs: { config: "config" }, ngImport: i0, template: `
|
|
53231
53419
|
<div class="page">
|
|
53232
53420
|
<header class="page-header">
|
|
53233
|
-
<val-display [props]="{ size: 'small', color: 'dark', content:
|
|
53421
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
53234
53422
|
<val-title
|
|
53235
53423
|
[props]="{
|
|
53236
53424
|
size: 'large',
|
|
53237
53425
|
color: 'dark',
|
|
53238
53426
|
bold: false,
|
|
53239
|
-
content:
|
|
53427
|
+
content: pageDescription(),
|
|
53240
53428
|
}"
|
|
53241
53429
|
/>
|
|
53242
53430
|
</header>
|
|
53243
53431
|
|
|
53244
|
-
@if (
|
|
53245
|
-
<
|
|
53246
|
-
<
|
|
53247
|
-
</div>
|
|
53248
|
-
} @else if (loadError()) {
|
|
53249
|
-
<div class="error-row">
|
|
53250
|
-
<val-text
|
|
53432
|
+
@if (state() === 'needs-install') {
|
|
53433
|
+
<section class="settings-section">
|
|
53434
|
+
<val-title
|
|
53251
53435
|
[props]="{
|
|
53252
|
-
content: tt('errorTitle'),
|
|
53253
53436
|
size: 'medium',
|
|
53254
|
-
color: '
|
|
53255
|
-
bold:
|
|
53437
|
+
color: 'dark',
|
|
53438
|
+
bold: true,
|
|
53439
|
+
content: tt('iosInstallTitle'),
|
|
53256
53440
|
}"
|
|
53257
53441
|
/>
|
|
53258
|
-
<
|
|
53442
|
+
<div class="section-body">
|
|
53443
|
+
<val-text
|
|
53444
|
+
[props]="{
|
|
53445
|
+
size: 'small',
|
|
53446
|
+
color: 'medium',
|
|
53447
|
+
bold: false,
|
|
53448
|
+
content: tt('iosInstallHint'),
|
|
53449
|
+
}"
|
|
53450
|
+
/>
|
|
53451
|
+
<ol class="install-steps">
|
|
53452
|
+
<li>{{ tt('iosInstallStep1') }}</li>
|
|
53453
|
+
<li>{{ tt('iosInstallStep2') }}</li>
|
|
53454
|
+
<li>{{ tt('iosInstallStep3') }}</li>
|
|
53455
|
+
<li>{{ tt('iosInstallStep4') }}</li>
|
|
53456
|
+
</ol>
|
|
53457
|
+
</div>
|
|
53458
|
+
</section>
|
|
53459
|
+
}
|
|
53460
|
+
|
|
53461
|
+
<section class="settings-section">
|
|
53462
|
+
<div class="notif-row">
|
|
53463
|
+
<div class="notif-row__icon">
|
|
53464
|
+
<ion-icon name="notifications-outline" aria-hidden="true" />
|
|
53465
|
+
</div>
|
|
53466
|
+
<div class="notif-row__body">
|
|
53467
|
+
<val-title
|
|
53468
|
+
[props]="{
|
|
53469
|
+
size: 'medium',
|
|
53470
|
+
color: 'dark',
|
|
53471
|
+
bold: true,
|
|
53472
|
+
content: tt('simpleTitle'),
|
|
53473
|
+
}"
|
|
53474
|
+
/>
|
|
53475
|
+
<val-text
|
|
53476
|
+
[props]="{
|
|
53477
|
+
size: 'small',
|
|
53478
|
+
color: 'medium',
|
|
53479
|
+
bold: false,
|
|
53480
|
+
content: tt('simpleDescription'),
|
|
53481
|
+
}"
|
|
53482
|
+
/>
|
|
53483
|
+
@if (busyLabel(); as label) {
|
|
53484
|
+
<span class="notif-row__busy">{{ label }}</span>
|
|
53485
|
+
}
|
|
53486
|
+
</div>
|
|
53487
|
+
<div class="notif-row__control">
|
|
53488
|
+
<val-toggle-input [props]="toggleProps()" />
|
|
53489
|
+
</div>
|
|
53259
53490
|
</div>
|
|
53260
|
-
|
|
53261
|
-
@if (
|
|
53262
|
-
<
|
|
53263
|
-
<
|
|
53264
|
-
|
|
53265
|
-
|
|
53266
|
-
size: 'medium',
|
|
53267
|
-
color: 'dark',
|
|
53268
|
-
bold: true,
|
|
53269
|
-
content: tt('systemRolesTitle'),
|
|
53270
|
-
}"
|
|
53271
|
-
/>
|
|
53272
|
-
<val-text
|
|
53273
|
-
[props]="{
|
|
53274
|
-
content: tt('systemRolesHint'),
|
|
53275
|
-
size: 'medium',
|
|
53276
|
-
color: 'medium',
|
|
53277
|
-
bold: false,
|
|
53278
|
-
}"
|
|
53279
|
-
/>
|
|
53280
|
-
</div>
|
|
53281
|
-
<div class="role-list">
|
|
53282
|
-
@for (role of systemRoles(); track role.id) {
|
|
53283
|
-
<div class="role-card">
|
|
53284
|
-
<div class="role-card__header">
|
|
53285
|
-
<span class="role-card__name">{{ roleLabel(role.name) }}</span>
|
|
53286
|
-
<val-pill
|
|
53287
|
-
[props]="{
|
|
53288
|
-
label: tt('systemBadge'),
|
|
53289
|
-
color: 'primary',
|
|
53290
|
-
weight: 'bold',
|
|
53291
|
-
}"
|
|
53292
|
-
/>
|
|
53293
|
-
</div>
|
|
53294
|
-
@if (role.description) {
|
|
53295
|
-
<span class="role-card__desc">{{ role.description }}</span>
|
|
53296
|
-
}
|
|
53297
|
-
@if (!role.hasPerms) {
|
|
53298
|
-
<div class="perm-chips">
|
|
53299
|
-
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
53300
|
-
</div>
|
|
53301
|
-
} @else {
|
|
53302
|
-
@for (g of role.groups; track g.key) {
|
|
53303
|
-
<div class="perm-group">
|
|
53304
|
-
<span class="perm-group__label">{{ g.label }}</span>
|
|
53305
|
-
<div class="perm-chips">
|
|
53306
|
-
@for (p of g.perms; track p) {
|
|
53307
|
-
<span class="perm-chip">{{ p }}</span>
|
|
53308
|
-
}
|
|
53309
|
-
</div>
|
|
53310
|
-
</div>
|
|
53311
|
-
}
|
|
53312
|
-
}
|
|
53313
|
-
</div>
|
|
53314
|
-
}
|
|
53315
|
-
</div>
|
|
53316
|
-
</section>
|
|
53491
|
+
|
|
53492
|
+
@if (contextMessage(); as msg) {
|
|
53493
|
+
<div class="alert" [class]="'alert--' + alertVariant()">
|
|
53494
|
+
<ion-icon [name]="alertIcon()" aria-hidden="true" />
|
|
53495
|
+
<span>{{ msg }}</span>
|
|
53496
|
+
</div>
|
|
53317
53497
|
}
|
|
53318
53498
|
|
|
53319
|
-
@if (
|
|
53320
|
-
<
|
|
53321
|
-
<
|
|
53322
|
-
|
|
53323
|
-
[props]="{
|
|
53324
|
-
size: 'medium',
|
|
53325
|
-
color: 'dark',
|
|
53326
|
-
bold: true,
|
|
53327
|
-
content: tt('customRolesTitle'),
|
|
53328
|
-
}"
|
|
53329
|
-
/>
|
|
53330
|
-
<val-text
|
|
53331
|
-
[props]="{
|
|
53332
|
-
content: tt('customRolesHint'),
|
|
53333
|
-
size: 'medium',
|
|
53334
|
-
color: 'medium',
|
|
53335
|
-
bold: false,
|
|
53336
|
-
}"
|
|
53337
|
-
/>
|
|
53338
|
-
</div>
|
|
53339
|
-
@if (customRoles().length === 0) {
|
|
53340
|
-
<val-text
|
|
53341
|
-
[props]="{
|
|
53342
|
-
content: tt('noCustomRoles'),
|
|
53343
|
-
size: 'medium',
|
|
53344
|
-
color: 'medium',
|
|
53345
|
-
bold: false,
|
|
53346
|
-
}"
|
|
53347
|
-
/>
|
|
53348
|
-
} @else {
|
|
53349
|
-
<div class="role-list">
|
|
53350
|
-
@for (role of customRoles(); track role.id) {
|
|
53351
|
-
<div class="role-card">
|
|
53352
|
-
<div class="role-card__header">
|
|
53353
|
-
<span class="role-card__name">{{ role.name }}</span>
|
|
53354
|
-
</div>
|
|
53355
|
-
@if (role.description) {
|
|
53356
|
-
<span class="role-card__desc">{{ role.description }}</span>
|
|
53357
|
-
}
|
|
53358
|
-
@if (!role.hasPerms) {
|
|
53359
|
-
<div class="perm-chips">
|
|
53360
|
-
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
53361
|
-
</div>
|
|
53362
|
-
} @else {
|
|
53363
|
-
@for (g of role.groups; track g.key) {
|
|
53364
|
-
<div class="perm-group">
|
|
53365
|
-
<span class="perm-group__label">{{ g.label }}</span>
|
|
53366
|
-
<div class="perm-chips">
|
|
53367
|
-
@for (p of g.perms; track p) {
|
|
53368
|
-
<span class="perm-chip">{{ p }}</span>
|
|
53369
|
-
}
|
|
53370
|
-
</div>
|
|
53371
|
-
</div>
|
|
53372
|
-
}
|
|
53373
|
-
}
|
|
53374
|
-
</div>
|
|
53375
|
-
}
|
|
53376
|
-
</div>
|
|
53377
|
-
}
|
|
53378
|
-
</section>
|
|
53499
|
+
@if (state() === 'error') {
|
|
53500
|
+
<div class="row-actions">
|
|
53501
|
+
<val-button [props]="retryButtonProps()" (click)="onRetry()" />
|
|
53502
|
+
</div>
|
|
53379
53503
|
}
|
|
53504
|
+
</section>
|
|
53505
|
+
|
|
53506
|
+
@if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
|
|
53507
|
+
<section class="settings-section">
|
|
53508
|
+
<button class="details-toggle" type="button" (click)="toggleDetails()">
|
|
53509
|
+
<span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
|
|
53510
|
+
<ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
|
|
53511
|
+
</button>
|
|
53512
|
+
@if (showDetails()) {
|
|
53513
|
+
<dl class="details">
|
|
53514
|
+
<div class="details__row">
|
|
53515
|
+
<dt>{{ tt('detailsToken') }}</dt>
|
|
53516
|
+
<dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
|
|
53517
|
+
</div>
|
|
53518
|
+
<div class="details__row">
|
|
53519
|
+
<dt>{{ tt('diagnosticsPermission') }}</dt>
|
|
53520
|
+
<dd>{{ permission() }}</dd>
|
|
53521
|
+
</div>
|
|
53522
|
+
<div class="details__row">
|
|
53523
|
+
<dt>{{ tt('diagnosticsSupported') }}</dt>
|
|
53524
|
+
<dd>{{ isSupported() ? '✓' : '✗' }}</dd>
|
|
53525
|
+
</div>
|
|
53526
|
+
</dl>
|
|
53527
|
+
}
|
|
53528
|
+
</section>
|
|
53380
53529
|
}
|
|
53381
53530
|
</div>
|
|
53382
|
-
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:
|
|
53531
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: ToggleInputComponent, selector: "val-toggle-input", inputs: ["preset", "props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
|
|
53383
53532
|
}
|
|
53384
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
53533
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, decorators: [{
|
|
53385
53534
|
type: Component,
|
|
53386
|
-
args: [{ selector: 'val-
|
|
53535
|
+
args: [{ selector: 'val-notification-preferences-view', standalone: true, imports: [IonIcon, ToggleInputComponent, ButtonComponent, DisplayComponent, TitleComponent, TextComponent], template: `
|
|
53387
53536
|
<div class="page">
|
|
53388
53537
|
<header class="page-header">
|
|
53389
|
-
<val-display [props]="{ size: 'small', color: 'dark', content:
|
|
53538
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
53390
53539
|
<val-title
|
|
53391
53540
|
[props]="{
|
|
53392
53541
|
size: 'large',
|
|
53393
53542
|
color: 'dark',
|
|
53394
53543
|
bold: false,
|
|
53395
|
-
content:
|
|
53544
|
+
content: pageDescription(),
|
|
53396
53545
|
}"
|
|
53397
53546
|
/>
|
|
53398
53547
|
</header>
|
|
53399
53548
|
|
|
53400
|
-
@if (
|
|
53401
|
-
<
|
|
53402
|
-
<
|
|
53403
|
-
</div>
|
|
53404
|
-
} @else if (loadError()) {
|
|
53405
|
-
<div class="error-row">
|
|
53406
|
-
<val-text
|
|
53549
|
+
@if (state() === 'needs-install') {
|
|
53550
|
+
<section class="settings-section">
|
|
53551
|
+
<val-title
|
|
53407
53552
|
[props]="{
|
|
53408
|
-
content: tt('errorTitle'),
|
|
53409
53553
|
size: 'medium',
|
|
53410
|
-
color: '
|
|
53411
|
-
bold:
|
|
53554
|
+
color: 'dark',
|
|
53555
|
+
bold: true,
|
|
53556
|
+
content: tt('iosInstallTitle'),
|
|
53412
53557
|
}"
|
|
53413
53558
|
/>
|
|
53414
|
-
<
|
|
53559
|
+
<div class="section-body">
|
|
53560
|
+
<val-text
|
|
53561
|
+
[props]="{
|
|
53562
|
+
size: 'small',
|
|
53563
|
+
color: 'medium',
|
|
53564
|
+
bold: false,
|
|
53565
|
+
content: tt('iosInstallHint'),
|
|
53566
|
+
}"
|
|
53567
|
+
/>
|
|
53568
|
+
<ol class="install-steps">
|
|
53569
|
+
<li>{{ tt('iosInstallStep1') }}</li>
|
|
53570
|
+
<li>{{ tt('iosInstallStep2') }}</li>
|
|
53571
|
+
<li>{{ tt('iosInstallStep3') }}</li>
|
|
53572
|
+
<li>{{ tt('iosInstallStep4') }}</li>
|
|
53573
|
+
</ol>
|
|
53574
|
+
</div>
|
|
53575
|
+
</section>
|
|
53576
|
+
}
|
|
53577
|
+
|
|
53578
|
+
<section class="settings-section">
|
|
53579
|
+
<div class="notif-row">
|
|
53580
|
+
<div class="notif-row__icon">
|
|
53581
|
+
<ion-icon name="notifications-outline" aria-hidden="true" />
|
|
53582
|
+
</div>
|
|
53583
|
+
<div class="notif-row__body">
|
|
53584
|
+
<val-title
|
|
53585
|
+
[props]="{
|
|
53586
|
+
size: 'medium',
|
|
53587
|
+
color: 'dark',
|
|
53588
|
+
bold: true,
|
|
53589
|
+
content: tt('simpleTitle'),
|
|
53590
|
+
}"
|
|
53591
|
+
/>
|
|
53592
|
+
<val-text
|
|
53593
|
+
[props]="{
|
|
53594
|
+
size: 'small',
|
|
53595
|
+
color: 'medium',
|
|
53596
|
+
bold: false,
|
|
53597
|
+
content: tt('simpleDescription'),
|
|
53598
|
+
}"
|
|
53599
|
+
/>
|
|
53600
|
+
@if (busyLabel(); as label) {
|
|
53601
|
+
<span class="notif-row__busy">{{ label }}</span>
|
|
53602
|
+
}
|
|
53603
|
+
</div>
|
|
53604
|
+
<div class="notif-row__control">
|
|
53605
|
+
<val-toggle-input [props]="toggleProps()" />
|
|
53606
|
+
</div>
|
|
53415
53607
|
</div>
|
|
53416
|
-
|
|
53417
|
-
@if (
|
|
53418
|
-
<
|
|
53419
|
-
<
|
|
53420
|
-
|
|
53421
|
-
|
|
53422
|
-
size: 'medium',
|
|
53423
|
-
color: 'dark',
|
|
53424
|
-
bold: true,
|
|
53425
|
-
content: tt('systemRolesTitle'),
|
|
53426
|
-
}"
|
|
53427
|
-
/>
|
|
53428
|
-
<val-text
|
|
53429
|
-
[props]="{
|
|
53430
|
-
content: tt('systemRolesHint'),
|
|
53431
|
-
size: 'medium',
|
|
53432
|
-
color: 'medium',
|
|
53433
|
-
bold: false,
|
|
53434
|
-
}"
|
|
53435
|
-
/>
|
|
53436
|
-
</div>
|
|
53437
|
-
<div class="role-list">
|
|
53438
|
-
@for (role of systemRoles(); track role.id) {
|
|
53439
|
-
<div class="role-card">
|
|
53440
|
-
<div class="role-card__header">
|
|
53441
|
-
<span class="role-card__name">{{ roleLabel(role.name) }}</span>
|
|
53442
|
-
<val-pill
|
|
53443
|
-
[props]="{
|
|
53444
|
-
label: tt('systemBadge'),
|
|
53445
|
-
color: 'primary',
|
|
53446
|
-
weight: 'bold',
|
|
53447
|
-
}"
|
|
53448
|
-
/>
|
|
53449
|
-
</div>
|
|
53450
|
-
@if (role.description) {
|
|
53451
|
-
<span class="role-card__desc">{{ role.description }}</span>
|
|
53452
|
-
}
|
|
53453
|
-
@if (!role.hasPerms) {
|
|
53454
|
-
<div class="perm-chips">
|
|
53455
|
-
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
53456
|
-
</div>
|
|
53457
|
-
} @else {
|
|
53458
|
-
@for (g of role.groups; track g.key) {
|
|
53459
|
-
<div class="perm-group">
|
|
53460
|
-
<span class="perm-group__label">{{ g.label }}</span>
|
|
53461
|
-
<div class="perm-chips">
|
|
53462
|
-
@for (p of g.perms; track p) {
|
|
53463
|
-
<span class="perm-chip">{{ p }}</span>
|
|
53464
|
-
}
|
|
53465
|
-
</div>
|
|
53466
|
-
</div>
|
|
53467
|
-
}
|
|
53468
|
-
}
|
|
53469
|
-
</div>
|
|
53470
|
-
}
|
|
53471
|
-
</div>
|
|
53472
|
-
</section>
|
|
53608
|
+
|
|
53609
|
+
@if (contextMessage(); as msg) {
|
|
53610
|
+
<div class="alert" [class]="'alert--' + alertVariant()">
|
|
53611
|
+
<ion-icon [name]="alertIcon()" aria-hidden="true" />
|
|
53612
|
+
<span>{{ msg }}</span>
|
|
53613
|
+
</div>
|
|
53473
53614
|
}
|
|
53474
53615
|
|
|
53475
|
-
@if (
|
|
53476
|
-
<
|
|
53477
|
-
<
|
|
53478
|
-
|
|
53479
|
-
[props]="{
|
|
53480
|
-
size: 'medium',
|
|
53481
|
-
color: 'dark',
|
|
53482
|
-
bold: true,
|
|
53483
|
-
content: tt('customRolesTitle'),
|
|
53484
|
-
}"
|
|
53485
|
-
/>
|
|
53486
|
-
<val-text
|
|
53487
|
-
[props]="{
|
|
53488
|
-
content: tt('customRolesHint'),
|
|
53489
|
-
size: 'medium',
|
|
53490
|
-
color: 'medium',
|
|
53491
|
-
bold: false,
|
|
53492
|
-
}"
|
|
53493
|
-
/>
|
|
53494
|
-
</div>
|
|
53495
|
-
@if (customRoles().length === 0) {
|
|
53496
|
-
<val-text
|
|
53497
|
-
[props]="{
|
|
53498
|
-
content: tt('noCustomRoles'),
|
|
53499
|
-
size: 'medium',
|
|
53500
|
-
color: 'medium',
|
|
53501
|
-
bold: false,
|
|
53502
|
-
}"
|
|
53503
|
-
/>
|
|
53504
|
-
} @else {
|
|
53505
|
-
<div class="role-list">
|
|
53506
|
-
@for (role of customRoles(); track role.id) {
|
|
53507
|
-
<div class="role-card">
|
|
53508
|
-
<div class="role-card__header">
|
|
53509
|
-
<span class="role-card__name">{{ role.name }}</span>
|
|
53510
|
-
</div>
|
|
53511
|
-
@if (role.description) {
|
|
53512
|
-
<span class="role-card__desc">{{ role.description }}</span>
|
|
53513
|
-
}
|
|
53514
|
-
@if (!role.hasPerms) {
|
|
53515
|
-
<div class="perm-chips">
|
|
53516
|
-
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
53517
|
-
</div>
|
|
53518
|
-
} @else {
|
|
53519
|
-
@for (g of role.groups; track g.key) {
|
|
53520
|
-
<div class="perm-group">
|
|
53521
|
-
<span class="perm-group__label">{{ g.label }}</span>
|
|
53522
|
-
<div class="perm-chips">
|
|
53523
|
-
@for (p of g.perms; track p) {
|
|
53524
|
-
<span class="perm-chip">{{ p }}</span>
|
|
53525
|
-
}
|
|
53526
|
-
</div>
|
|
53527
|
-
</div>
|
|
53528
|
-
}
|
|
53529
|
-
}
|
|
53530
|
-
</div>
|
|
53531
|
-
}
|
|
53532
|
-
</div>
|
|
53533
|
-
}
|
|
53534
|
-
</section>
|
|
53616
|
+
@if (state() === 'error') {
|
|
53617
|
+
<div class="row-actions">
|
|
53618
|
+
<val-button [props]="retryButtonProps()" (click)="onRetry()" />
|
|
53619
|
+
</div>
|
|
53535
53620
|
}
|
|
53621
|
+
</section>
|
|
53622
|
+
|
|
53623
|
+
@if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
|
|
53624
|
+
<section class="settings-section">
|
|
53625
|
+
<button class="details-toggle" type="button" (click)="toggleDetails()">
|
|
53626
|
+
<span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
|
|
53627
|
+
<ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
|
|
53628
|
+
</button>
|
|
53629
|
+
@if (showDetails()) {
|
|
53630
|
+
<dl class="details">
|
|
53631
|
+
<div class="details__row">
|
|
53632
|
+
<dt>{{ tt('detailsToken') }}</dt>
|
|
53633
|
+
<dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
|
|
53634
|
+
</div>
|
|
53635
|
+
<div class="details__row">
|
|
53636
|
+
<dt>{{ tt('diagnosticsPermission') }}</dt>
|
|
53637
|
+
<dd>{{ permission() }}</dd>
|
|
53638
|
+
</div>
|
|
53639
|
+
<div class="details__row">
|
|
53640
|
+
<dt>{{ tt('diagnosticsSupported') }}</dt>
|
|
53641
|
+
<dd>{{ isSupported() ? '✓' : '✗' }}</dd>
|
|
53642
|
+
</div>
|
|
53643
|
+
</dl>
|
|
53644
|
+
}
|
|
53645
|
+
</section>
|
|
53536
53646
|
}
|
|
53537
53647
|
</div>
|
|
53538
|
-
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:
|
|
53648
|
+
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"] }]
|
|
53539
53649
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
53540
53650
|
type: Input
|
|
53541
53651
|
}] } });
|
|
53542
53652
|
|
|
53653
|
+
/**
|
|
53654
|
+
* Helper para montar las **preferencias de notificaciones push**
|
|
53655
|
+
* (`val-notification-preferences-view`) como ruta en una app del factory. El
|
|
53656
|
+
* `config` se pasa por route `data` (`notificationPreferencesConfig`) y el
|
|
53657
|
+
* componente lo lee como fallback de su `@Input() config`.
|
|
53658
|
+
*
|
|
53659
|
+
* No confundir con `provideValtechNotificationsRoutes` (el inbox / feed).
|
|
53660
|
+
*
|
|
53661
|
+
* @example
|
|
53662
|
+
* ```ts
|
|
53663
|
+
* // app.routes.ts (típicamente bajo settings)
|
|
53664
|
+
* export const settingsRoutes: Routes = [
|
|
53665
|
+
* ...provideValtechNotificationPreferencesRoutes(), // → notifications
|
|
53666
|
+
* ];
|
|
53667
|
+
*
|
|
53668
|
+
* // con config acotada:
|
|
53669
|
+
* ...provideValtechNotificationPreferencesRoutes({ config: { showDetails: false } }),
|
|
53670
|
+
* ...provideValtechNotificationPreferencesRoutes({ path: 'push' }),
|
|
53671
|
+
* ```
|
|
53672
|
+
*/
|
|
53673
|
+
function provideValtechNotificationPreferencesRoutes(opts) {
|
|
53674
|
+
return [
|
|
53675
|
+
{
|
|
53676
|
+
path: opts?.path ?? 'notifications',
|
|
53677
|
+
component: NotificationPreferencesViewComponent,
|
|
53678
|
+
data: { notificationPreferencesConfig: opts?.config },
|
|
53679
|
+
},
|
|
53680
|
+
];
|
|
53681
|
+
}
|
|
53682
|
+
|
|
53543
53683
|
/**
|
|
53544
53684
|
* Helper para montar la vista Permisos (`val-permissions-view`) como ruta en una
|
|
53545
53685
|
* app del factory. El `config` se pasa por route `data` (`permissionsConfig`) y el
|
|
@@ -67204,5 +67344,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
67204
67344
|
* Generated bundle index. Do not edit.
|
|
67205
67345
|
*/
|
|
67206
67346
|
|
|
67207
|
-
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PermissionCatalogService, PermissionSelectorComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketCardComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
67347
|
+
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketCardComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
67208
67348
|
//# sourceMappingURL=valtech-components.mjs.map
|