valtech-components 2.0.510 → 2.0.511
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/molecules/refresher/refresher.component.mjs +254 -0
- package/esm2022/lib/components/molecules/refresher/types.mjs +15 -0
- package/esm2022/lib/components/organisms/infinite-list/infinite-list.component.mjs +618 -0
- package/esm2022/lib/components/organisms/infinite-list/types.mjs +15 -0
- package/esm2022/lib/components/templates/page-template/page-template.component.mjs +3 -3
- package/esm2022/lib/services/pagination/index.mjs +5 -0
- package/esm2022/lib/services/pagination/pagination.service.mjs +218 -0
- package/esm2022/lib/services/pagination/types.mjs +14 -0
- package/esm2022/lib/services/skeleton/config.mjs +79 -0
- package/esm2022/lib/services/skeleton/directives/loading.directive.mjs +215 -0
- package/esm2022/lib/services/skeleton/index.mjs +16 -0
- package/esm2022/lib/services/skeleton/skeleton.service.mjs +198 -0
- package/esm2022/lib/services/skeleton/templates/detail-skeleton.component.mjs +223 -0
- package/esm2022/lib/services/skeleton/templates/form-skeleton.component.mjs +127 -0
- package/esm2022/lib/services/skeleton/templates/grid-skeleton.component.mjs +154 -0
- package/esm2022/lib/services/skeleton/templates/list-skeleton.component.mjs +110 -0
- package/esm2022/lib/services/skeleton/templates/profile-skeleton.component.mjs +207 -0
- package/esm2022/lib/services/skeleton/templates/table-skeleton.component.mjs +116 -0
- package/esm2022/lib/services/skeleton/types.mjs +11 -0
- package/esm2022/public-api.mjs +12 -1
- package/fesm2022/valtech-components.mjs +3467 -950
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/molecules/refresher/refresher.component.d.ts +79 -0
- package/lib/components/molecules/refresher/types.d.ts +86 -0
- package/lib/components/organisms/infinite-list/infinite-list.component.d.ts +111 -0
- package/lib/components/organisms/infinite-list/types.d.ts +197 -0
- package/lib/services/pagination/index.d.ts +2 -0
- package/lib/services/pagination/pagination.service.d.ts +43 -0
- package/lib/services/pagination/types.d.ts +113 -0
- package/lib/services/skeleton/config.d.ts +30 -0
- package/lib/services/skeleton/directives/loading.directive.d.ts +71 -0
- package/lib/services/skeleton/index.d.ts +10 -0
- package/lib/services/skeleton/skeleton.service.d.ts +127 -0
- package/lib/services/skeleton/templates/detail-skeleton.component.d.ts +18 -0
- package/lib/services/skeleton/templates/form-skeleton.component.d.ts +22 -0
- package/lib/services/skeleton/templates/grid-skeleton.component.d.ts +18 -0
- package/lib/services/skeleton/templates/list-skeleton.component.d.ts +17 -0
- package/lib/services/skeleton/templates/profile-skeleton.component.d.ts +20 -0
- package/lib/services/skeleton/templates/table-skeleton.component.d.ts +17 -0
- package/lib/services/skeleton/types.d.ts +111 -0
- package/package.json +1 -1
- package/public-api.d.ts +6 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { EventEmitter, Component, Input, Output, Injectable, signal, makeEnvironmentProviders, APP_INITIALIZER, inject, HostListener, Pipe, ChangeDetectionStrategy, computed, ViewChild, ChangeDetectorRef, ElementRef, PLATFORM_ID, Inject, ErrorHandler, DestroyRef, InjectionToken, Optional, runInInjectionContext, effect } from '@angular/core';
|
|
2
|
+
import { EventEmitter, Component, Input, Output, Injectable, signal, makeEnvironmentProviders, APP_INITIALIZER, inject, HostListener, Pipe, ChangeDetectionStrategy, computed, ViewChild, ChangeDetectorRef, ElementRef, ContentChild, PLATFORM_ID, Inject, ErrorHandler, DestroyRef, InjectionToken, Optional, runInInjectionContext, effect, TemplateRef, ViewContainerRef, isSignal, Directive } from '@angular/core';
|
|
3
3
|
import * as i2$1 from '@ionic/angular/standalone';
|
|
4
|
-
import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonTextarea, IonDatetime, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, AlertController } from '@ionic/angular/standalone';
|
|
4
|
+
import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonTextarea, IonDatetime, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, AlertController } from '@ionic/angular/standalone';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgStyle, NgFor, NgClass, isPlatformBrowser } from '@angular/common';
|
|
7
7
|
import { addIcons } from 'ionicons';
|
|
@@ -13,7 +13,7 @@ import * as i1$2 from '@angular/platform-browser';
|
|
|
13
13
|
import QRCodeStyling from 'qr-code-styling';
|
|
14
14
|
import * as i1$3 from '@angular/forms';
|
|
15
15
|
import { ReactiveFormsModule, FormsModule, FormControl, Validators } from '@angular/forms';
|
|
16
|
-
import { BehaviorSubject, filter, map, distinctUntilChanged, Subject, throwError, Observable,
|
|
16
|
+
import { BehaviorSubject, isObservable, firstValueFrom, filter, map, distinctUntilChanged, Subject, throwError, Observable, of, from } from 'rxjs';
|
|
17
17
|
import * as i1$4 from 'ng-otp-input';
|
|
18
18
|
import { NgOtpInputComponent, NgOtpInputModule } from 'ng-otp-input';
|
|
19
19
|
import * as i2 from '@ionic/angular';
|
|
@@ -36,7 +36,7 @@ import { provideFirestore, getFirestore, connectFirestoreEmulator, enableIndexed
|
|
|
36
36
|
import { provideMessaging, getMessaging, Messaging, getToken, deleteToken, onMessage } from '@angular/fire/messaging';
|
|
37
37
|
import * as i1$7 from '@angular/fire/storage';
|
|
38
38
|
import { provideStorage, getStorage, connectStorageEmulator, ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject, listAll } from '@angular/fire/storage';
|
|
39
|
-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
39
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
40
40
|
import { filter as filter$1, catchError, switchMap, finalize, take, tap, map as map$1 } from 'rxjs/operators';
|
|
41
41
|
import * as i1$8 from '@angular/common/http';
|
|
42
42
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
@@ -16074,6 +16074,269 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
16074
16074
|
args: ['accordionGroup']
|
|
16075
16075
|
}] } });
|
|
16076
16076
|
|
|
16077
|
+
/**
|
|
16078
|
+
* Valores por defecto para el refresher.
|
|
16079
|
+
*/
|
|
16080
|
+
const DEFAULT_REFRESHER_METADATA = {
|
|
16081
|
+
disabled: false,
|
|
16082
|
+
pullThreshold: 70,
|
|
16083
|
+
pullMax: 200,
|
|
16084
|
+
pullFactor: 0.5,
|
|
16085
|
+
snapbackDuration: 280,
|
|
16086
|
+
closeDuration: 280,
|
|
16087
|
+
spinnerType: 'crescent',
|
|
16088
|
+
hapticFeedback: true,
|
|
16089
|
+
webMode: false,
|
|
16090
|
+
};
|
|
16091
|
+
|
|
16092
|
+
/**
|
|
16093
|
+
* Componente de pull-to-refresh para movil y web.
|
|
16094
|
+
*
|
|
16095
|
+
* @example
|
|
16096
|
+
* <!-- Uso basico -->
|
|
16097
|
+
* <val-refresher
|
|
16098
|
+
* [props]="{
|
|
16099
|
+
* color: 'primary',
|
|
16100
|
+
* pullingText: 'Arrastra para actualizar',
|
|
16101
|
+
* refreshingText: 'Cargando...'
|
|
16102
|
+
* }"
|
|
16103
|
+
* (refresh)="onRefresh($event)"
|
|
16104
|
+
* >
|
|
16105
|
+
* <ion-content>
|
|
16106
|
+
* <!-- Contenido scrolleable -->
|
|
16107
|
+
* </ion-content>
|
|
16108
|
+
* </val-refresher>
|
|
16109
|
+
*
|
|
16110
|
+
* @example
|
|
16111
|
+
* <!-- Con indicador personalizado -->
|
|
16112
|
+
* <val-refresher [props]="{ color: 'primary' }" (refresh)="onRefresh($event)">
|
|
16113
|
+
* <ng-template #pullingIndicator let-progress="progress">
|
|
16114
|
+
* <div class="custom-indicator">
|
|
16115
|
+
* <ion-icon name="arrow-down" [style.transform]="'rotate(' + progress * 180 + 'deg)'"></ion-icon>
|
|
16116
|
+
* </div>
|
|
16117
|
+
* </ng-template>
|
|
16118
|
+
* <ion-content>...</ion-content>
|
|
16119
|
+
* </val-refresher>
|
|
16120
|
+
*/
|
|
16121
|
+
class RefresherComponent {
|
|
16122
|
+
constructor() {
|
|
16123
|
+
/** Configuracion del refresher */
|
|
16124
|
+
this.props = {};
|
|
16125
|
+
/** Evento emitido cuando se activa el refresh */
|
|
16126
|
+
this.refresh = new EventEmitter();
|
|
16127
|
+
/** Evento de progreso durante el pull */
|
|
16128
|
+
this.pullProgressChange = new EventEmitter();
|
|
16129
|
+
/** Evento de cambio de estado */
|
|
16130
|
+
this.stateChange = new EventEmitter();
|
|
16131
|
+
/** Estado actual del refresher */
|
|
16132
|
+
this.state = signal('idle');
|
|
16133
|
+
/** Progreso actual del pull (0-1) */
|
|
16134
|
+
this.pullProgress = signal(0);
|
|
16135
|
+
}
|
|
16136
|
+
/** Props combinados con defaults */
|
|
16137
|
+
get mergedProps() {
|
|
16138
|
+
return { ...DEFAULT_REFRESHER_METADATA, ...this.props };
|
|
16139
|
+
}
|
|
16140
|
+
/** Si hay indicadores personalizados */
|
|
16141
|
+
get hasCustomIndicator() {
|
|
16142
|
+
return !!(this.pullingIndicator ||
|
|
16143
|
+
this.refreshingIndicator ||
|
|
16144
|
+
this.completingIndicator ||
|
|
16145
|
+
this.props.indicatorConfig);
|
|
16146
|
+
}
|
|
16147
|
+
/**
|
|
16148
|
+
* Activa programaticamente el refresh.
|
|
16149
|
+
*/
|
|
16150
|
+
triggerRefresh() {
|
|
16151
|
+
this.state.set('refreshing');
|
|
16152
|
+
this.emitRefreshEvent();
|
|
16153
|
+
}
|
|
16154
|
+
/**
|
|
16155
|
+
* Completa la operacion de refresh actual.
|
|
16156
|
+
*/
|
|
16157
|
+
complete() {
|
|
16158
|
+
this.state.set('completing');
|
|
16159
|
+
this.stateChange.emit('completing');
|
|
16160
|
+
this.ionRefresher?.complete();
|
|
16161
|
+
// Resetear a idle despues de la animacion
|
|
16162
|
+
setTimeout(() => {
|
|
16163
|
+
this.state.set('idle');
|
|
16164
|
+
this.stateChange.emit('idle');
|
|
16165
|
+
}, this.mergedProps.closeDuration ?? 280);
|
|
16166
|
+
}
|
|
16167
|
+
/**
|
|
16168
|
+
* Cancela la operacion de refresh actual.
|
|
16169
|
+
*/
|
|
16170
|
+
cancel() {
|
|
16171
|
+
this.ionRefresher?.cancel();
|
|
16172
|
+
this.state.set('idle');
|
|
16173
|
+
this.stateChange.emit('idle');
|
|
16174
|
+
}
|
|
16175
|
+
/** Handler para evento ionRefresh */
|
|
16176
|
+
onIonRefresh(event) {
|
|
16177
|
+
this.state.set('refreshing');
|
|
16178
|
+
this.stateChange.emit('refreshing');
|
|
16179
|
+
this.emitRefreshEvent();
|
|
16180
|
+
}
|
|
16181
|
+
/** Handler para evento ionPull */
|
|
16182
|
+
onIonPull(event) {
|
|
16183
|
+
const detail = event.detail;
|
|
16184
|
+
const progress = Math.min(detail.progress, 1);
|
|
16185
|
+
this.pullProgress.set(progress);
|
|
16186
|
+
this.pullProgressChange.emit({
|
|
16187
|
+
progress,
|
|
16188
|
+
thresholdReached: progress >= 1,
|
|
16189
|
+
});
|
|
16190
|
+
}
|
|
16191
|
+
/** Handler para evento ionStart */
|
|
16192
|
+
onIonStart() {
|
|
16193
|
+
this.state.set('pulling');
|
|
16194
|
+
this.stateChange.emit('pulling');
|
|
16195
|
+
}
|
|
16196
|
+
emitRefreshEvent() {
|
|
16197
|
+
const event = {
|
|
16198
|
+
complete: () => this.complete(),
|
|
16199
|
+
cancel: () => this.cancel(),
|
|
16200
|
+
timestamp: new Date(),
|
|
16201
|
+
};
|
|
16202
|
+
this.refresh.emit(event);
|
|
16203
|
+
}
|
|
16204
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RefresherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16205
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RefresherComponent, isStandalone: true, selector: "val-refresher", inputs: { props: "props" }, outputs: { refresh: "refresh", pullProgressChange: "pullProgressChange", stateChange: "stateChange" }, queries: [{ propertyName: "pullingIndicator", first: true, predicate: ["pullingIndicator"], descendants: true }, { propertyName: "refreshingIndicator", first: true, predicate: ["refreshingIndicator"], descendants: true }, { propertyName: "completingIndicator", first: true, predicate: ["completingIndicator"], descendants: true }], viewQueries: [{ propertyName: "ionRefresher", first: true, predicate: ["refresher"], descendants: true }], ngImport: i0, template: `
|
|
16206
|
+
<ion-refresher
|
|
16207
|
+
#refresher
|
|
16208
|
+
slot="fixed"
|
|
16209
|
+
[disabled]="mergedProps.disabled"
|
|
16210
|
+
[pullMin]="mergedProps.pullThreshold"
|
|
16211
|
+
[pullMax]="mergedProps.pullMax"
|
|
16212
|
+
[pullFactor]="mergedProps.pullFactor"
|
|
16213
|
+
[snapbackDuration]="mergedProps.snapbackDuration"
|
|
16214
|
+
[closeDuration]="mergedProps.closeDuration"
|
|
16215
|
+
(ionRefresh)="onIonRefresh($event)"
|
|
16216
|
+
(ionPull)="onIonPull($event)"
|
|
16217
|
+
(ionStart)="onIonStart()"
|
|
16218
|
+
>
|
|
16219
|
+
@if (hasCustomIndicator) {
|
|
16220
|
+
<!-- Custom indicator templates -->
|
|
16221
|
+
<div class="refresher-custom-content">
|
|
16222
|
+
@switch (state()) {
|
|
16223
|
+
@case ('pulling') {
|
|
16224
|
+
@if (pullingIndicator) {
|
|
16225
|
+
<ng-container
|
|
16226
|
+
*ngTemplateOutlet="pullingIndicator; context: { progress: pullProgress() }"
|
|
16227
|
+
></ng-container>
|
|
16228
|
+
}
|
|
16229
|
+
}
|
|
16230
|
+
@case ('refreshing') {
|
|
16231
|
+
@if (refreshingIndicator) {
|
|
16232
|
+
<ng-container *ngTemplateOutlet="refreshingIndicator"></ng-container>
|
|
16233
|
+
} @else {
|
|
16234
|
+
<ion-spinner [name]="mergedProps.spinnerType" [color]="mergedProps.color"></ion-spinner>
|
|
16235
|
+
@if (mergedProps.refreshingText) {
|
|
16236
|
+
<ion-text [color]="mergedProps.color">{{ mergedProps.refreshingText }}</ion-text>
|
|
16237
|
+
}
|
|
16238
|
+
}
|
|
16239
|
+
}
|
|
16240
|
+
@case ('completing') {
|
|
16241
|
+
@if (completingIndicator) {
|
|
16242
|
+
<ng-container *ngTemplateOutlet="completingIndicator"></ng-container>
|
|
16243
|
+
}
|
|
16244
|
+
}
|
|
16245
|
+
}
|
|
16246
|
+
</div>
|
|
16247
|
+
} @else {
|
|
16248
|
+
<!-- Default Ionic refresher content -->
|
|
16249
|
+
<ion-refresher-content
|
|
16250
|
+
[pullingIcon]="mergedProps.spinnerType === 'crescent' ? 'chevron-down-circle-outline' : undefined"
|
|
16251
|
+
[pullingText]="mergedProps.pullingText"
|
|
16252
|
+
[refreshingSpinner]="mergedProps.spinnerType"
|
|
16253
|
+
[refreshingText]="mergedProps.refreshingText"
|
|
16254
|
+
></ion-refresher-content>
|
|
16255
|
+
}
|
|
16256
|
+
</ion-refresher>
|
|
16257
|
+
|
|
16258
|
+
<ng-content></ng-content>
|
|
16259
|
+
`, isInline: true, styles: [":host{display:block;position:relative}.refresher-custom-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IonRefresher, selector: "ion-refresher", inputs: ["closeDuration", "disabled", "mode", "pullFactor", "pullMax", "pullMin", "snapbackDuration"] }, { kind: "component", type: IonRefresherContent, selector: "ion-refresher-content", inputs: ["pullingIcon", "pullingText", "refreshingSpinner", "refreshingText"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }] }); }
|
|
16260
|
+
}
|
|
16261
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RefresherComponent, decorators: [{
|
|
16262
|
+
type: Component,
|
|
16263
|
+
args: [{ selector: 'val-refresher', standalone: true, imports: [CommonModule, IonRefresher, IonRefresherContent, IonSpinner, IonText], template: `
|
|
16264
|
+
<ion-refresher
|
|
16265
|
+
#refresher
|
|
16266
|
+
slot="fixed"
|
|
16267
|
+
[disabled]="mergedProps.disabled"
|
|
16268
|
+
[pullMin]="mergedProps.pullThreshold"
|
|
16269
|
+
[pullMax]="mergedProps.pullMax"
|
|
16270
|
+
[pullFactor]="mergedProps.pullFactor"
|
|
16271
|
+
[snapbackDuration]="mergedProps.snapbackDuration"
|
|
16272
|
+
[closeDuration]="mergedProps.closeDuration"
|
|
16273
|
+
(ionRefresh)="onIonRefresh($event)"
|
|
16274
|
+
(ionPull)="onIonPull($event)"
|
|
16275
|
+
(ionStart)="onIonStart()"
|
|
16276
|
+
>
|
|
16277
|
+
@if (hasCustomIndicator) {
|
|
16278
|
+
<!-- Custom indicator templates -->
|
|
16279
|
+
<div class="refresher-custom-content">
|
|
16280
|
+
@switch (state()) {
|
|
16281
|
+
@case ('pulling') {
|
|
16282
|
+
@if (pullingIndicator) {
|
|
16283
|
+
<ng-container
|
|
16284
|
+
*ngTemplateOutlet="pullingIndicator; context: { progress: pullProgress() }"
|
|
16285
|
+
></ng-container>
|
|
16286
|
+
}
|
|
16287
|
+
}
|
|
16288
|
+
@case ('refreshing') {
|
|
16289
|
+
@if (refreshingIndicator) {
|
|
16290
|
+
<ng-container *ngTemplateOutlet="refreshingIndicator"></ng-container>
|
|
16291
|
+
} @else {
|
|
16292
|
+
<ion-spinner [name]="mergedProps.spinnerType" [color]="mergedProps.color"></ion-spinner>
|
|
16293
|
+
@if (mergedProps.refreshingText) {
|
|
16294
|
+
<ion-text [color]="mergedProps.color">{{ mergedProps.refreshingText }}</ion-text>
|
|
16295
|
+
}
|
|
16296
|
+
}
|
|
16297
|
+
}
|
|
16298
|
+
@case ('completing') {
|
|
16299
|
+
@if (completingIndicator) {
|
|
16300
|
+
<ng-container *ngTemplateOutlet="completingIndicator"></ng-container>
|
|
16301
|
+
}
|
|
16302
|
+
}
|
|
16303
|
+
}
|
|
16304
|
+
</div>
|
|
16305
|
+
} @else {
|
|
16306
|
+
<!-- Default Ionic refresher content -->
|
|
16307
|
+
<ion-refresher-content
|
|
16308
|
+
[pullingIcon]="mergedProps.spinnerType === 'crescent' ? 'chevron-down-circle-outline' : undefined"
|
|
16309
|
+
[pullingText]="mergedProps.pullingText"
|
|
16310
|
+
[refreshingSpinner]="mergedProps.spinnerType"
|
|
16311
|
+
[refreshingText]="mergedProps.refreshingText"
|
|
16312
|
+
></ion-refresher-content>
|
|
16313
|
+
}
|
|
16314
|
+
</ion-refresher>
|
|
16315
|
+
|
|
16316
|
+
<ng-content></ng-content>
|
|
16317
|
+
`, styles: [":host{display:block;position:relative}.refresher-custom-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px}\n"] }]
|
|
16318
|
+
}], propDecorators: { ionRefresher: [{
|
|
16319
|
+
type: ViewChild,
|
|
16320
|
+
args: ['refresher']
|
|
16321
|
+
}], pullingIndicator: [{
|
|
16322
|
+
type: ContentChild,
|
|
16323
|
+
args: ['pullingIndicator']
|
|
16324
|
+
}], refreshingIndicator: [{
|
|
16325
|
+
type: ContentChild,
|
|
16326
|
+
args: ['refreshingIndicator']
|
|
16327
|
+
}], completingIndicator: [{
|
|
16328
|
+
type: ContentChild,
|
|
16329
|
+
args: ['completingIndicator']
|
|
16330
|
+
}], props: [{
|
|
16331
|
+
type: Input
|
|
16332
|
+
}], refresh: [{
|
|
16333
|
+
type: Output
|
|
16334
|
+
}], pullProgressChange: [{
|
|
16335
|
+
type: Output
|
|
16336
|
+
}], stateChange: [{
|
|
16337
|
+
type: Output
|
|
16338
|
+
}] } });
|
|
16339
|
+
|
|
16077
16340
|
/**
|
|
16078
16341
|
* Configuración de espaciado predefinida
|
|
16079
16342
|
*/
|
|
@@ -20854,203 +21117,1033 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
20854
21117
|
type: Output
|
|
20855
21118
|
}] } });
|
|
20856
21119
|
|
|
20857
|
-
|
|
20858
|
-
|
|
20859
|
-
|
|
20860
|
-
|
|
20861
|
-
|
|
20862
|
-
|
|
20863
|
-
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
`, styles: [":root{--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #9e9e9e;--ion-color-medium-rgb: 158, 158, 158;--ion-color-medium-contrast: #000000;--ion-color-medium-contrast-rgb: 0, 0, 0;--ion-color-medium-shade: #8b8b8b;--ion-color-medium-tint: #a8a8a8;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-medium)}@media (prefers-color-scheme: dark){:root{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}.layout-container{margin:0 auto;padding:0;width:100%;box-sizing:border-box;margin-bottom:1rem;padding-top:.5rem}@media (max-width: 768px){.layout-container{max-width:100%}}@media (min-width: 768px){.layout-container{margin:0 auto;max-width:33.75rem;margin-bottom:1.5rem}}@media (min-width: 1200px){.layout-container{margin:0 auto;max-width:45rem}}\n"] }]
|
|
20872
|
-
}] });
|
|
20873
|
-
|
|
20874
|
-
class SimpleComponent {
|
|
20875
|
-
constructor() {
|
|
20876
|
-
this.onClick = new EventEmitter();
|
|
20877
|
-
this.onScrollEvent = new EventEmitter();
|
|
20878
|
-
this.theme = inject(ThemeService);
|
|
20879
|
-
}
|
|
20880
|
-
onClickHandler(token) {
|
|
20881
|
-
this.onClick.emit(token);
|
|
20882
|
-
}
|
|
20883
|
-
onScrollHandler($event) {
|
|
20884
|
-
this.onScrollEvent.emit(true);
|
|
20885
|
-
}
|
|
20886
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
20887
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SimpleComponent, isStandalone: true, selector: "val-simple", inputs: { props: "props" }, outputs: { onClick: "onClick", onScrollEvent: "onScrollEvent" }, ngImport: i0, template: `
|
|
20888
|
-
<val-header [props]="props.header" />
|
|
20889
|
-
|
|
20890
|
-
<ion-content
|
|
20891
|
-
[fullscreen]="true"
|
|
20892
|
-
[ngStyle]="{
|
|
20893
|
-
'--background': theme.IsDark ? 'var(--ion-background-color)' : props.background,
|
|
20894
|
-
}"
|
|
20895
|
-
[scrollEvents]="true"
|
|
20896
|
-
(ionScroll)="onScrollHandler($event)"
|
|
20897
|
-
>
|
|
20898
|
-
<ion-header collapse="condense">
|
|
20899
|
-
<ion-toolbar style="--background: transparent;">
|
|
20900
|
-
<ion-title style="padding: 0;" size="large">{{ props.pageTitle }}</ion-title>
|
|
20901
|
-
</ion-toolbar>
|
|
20902
|
-
</ion-header>
|
|
20903
|
-
@if (props.pageDescription) {
|
|
20904
|
-
<div class="description-container">
|
|
20905
|
-
<val-expandable-text
|
|
20906
|
-
[props]="{
|
|
20907
|
-
limit: 180,
|
|
20908
|
-
content: props.pageDescription,
|
|
20909
|
-
color: 'primary',
|
|
20910
|
-
expandText: 'más',
|
|
20911
|
-
}"
|
|
20912
|
-
/>
|
|
20913
|
-
</div>
|
|
20914
|
-
}
|
|
20915
|
-
@if (props.link) {
|
|
20916
|
-
<val-link [props]="props.link" (onClick)="onClickHandler($event)"></val-link>
|
|
20917
|
-
}
|
|
20918
|
-
@if (props.withDivider) {
|
|
20919
|
-
<val-divider [props]="{ fill: 'solid', size: 'medium', color: 'dark' }" />
|
|
20920
|
-
}
|
|
20921
|
-
<val-layout>
|
|
20922
|
-
<ng-content select="[inner-container]"></ng-content>
|
|
20923
|
-
</val-layout>
|
|
20924
|
-
</ion-content>
|
|
20925
|
-
<ng-content select="[outter-container]"></ng-content>
|
|
20926
|
-
`, isInline: true, styles: [".description-container{padding:0 16px}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: HeaderComponent, selector: "val-header", inputs: ["props"], outputs: ["onClick"] }, { kind: "component", type: LayoutComponent, selector: "val-layout" }, { kind: "component", type: DividerComponent, selector: "val-divider", inputs: ["props"] }, { kind: "component", type: LinkComponent, selector: "val-link", inputs: ["props"], outputs: ["onClick"] }, { kind: "component", type: ExpandableTextComponent, selector: "val-expandable-text", inputs: ["props"] }] }); }
|
|
20927
|
-
}
|
|
20928
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleComponent, decorators: [{
|
|
20929
|
-
type: Component,
|
|
20930
|
-
args: [{ selector: 'val-simple', standalone: true, imports: [
|
|
20931
|
-
NgStyle,
|
|
20932
|
-
IonHeader,
|
|
20933
|
-
IonToolbar,
|
|
20934
|
-
IonTitle,
|
|
20935
|
-
IonContent,
|
|
20936
|
-
HeaderComponent,
|
|
20937
|
-
LayoutComponent,
|
|
20938
|
-
DividerComponent,
|
|
20939
|
-
LinkComponent,
|
|
20940
|
-
ExpandableTextComponent,
|
|
20941
|
-
], template: `
|
|
20942
|
-
<val-header [props]="props.header" />
|
|
21120
|
+
/**
|
|
21121
|
+
* Valores por defecto para infinite list.
|
|
21122
|
+
*/
|
|
21123
|
+
const DEFAULT_INFINITE_LIST_METADATA = {
|
|
21124
|
+
direction: 'bottom',
|
|
21125
|
+
pageSize: 20,
|
|
21126
|
+
threshold: '100px',
|
|
21127
|
+
debounceTime: 300,
|
|
21128
|
+
autoLoad: true,
|
|
21129
|
+
spinnerType: 'crescent',
|
|
21130
|
+
useLoadMoreButton: false,
|
|
21131
|
+
showDividers: false,
|
|
21132
|
+
enableRefresh: false,
|
|
21133
|
+
};
|
|
20943
21134
|
|
|
20944
|
-
|
|
20945
|
-
|
|
20946
|
-
|
|
20947
|
-
|
|
20948
|
-
|
|
20949
|
-
|
|
20950
|
-
|
|
20951
|
-
|
|
20952
|
-
|
|
20953
|
-
|
|
20954
|
-
<ion-title style="padding: 0;" size="large">{{ props.pageTitle }}</ion-title>
|
|
20955
|
-
</ion-toolbar>
|
|
20956
|
-
</ion-header>
|
|
20957
|
-
@if (props.pageDescription) {
|
|
20958
|
-
<div class="description-container">
|
|
20959
|
-
<val-expandable-text
|
|
20960
|
-
[props]="{
|
|
20961
|
-
limit: 180,
|
|
20962
|
-
content: props.pageDescription,
|
|
20963
|
-
color: 'primary',
|
|
20964
|
-
expandText: 'más',
|
|
20965
|
-
}"
|
|
20966
|
-
/>
|
|
20967
|
-
</div>
|
|
20968
|
-
}
|
|
20969
|
-
@if (props.link) {
|
|
20970
|
-
<val-link [props]="props.link" (onClick)="onClickHandler($event)"></val-link>
|
|
20971
|
-
}
|
|
20972
|
-
@if (props.withDivider) {
|
|
20973
|
-
<val-divider [props]="{ fill: 'solid', size: 'medium', color: 'dark' }" />
|
|
20974
|
-
}
|
|
20975
|
-
<val-layout>
|
|
20976
|
-
<ng-content select="[inner-container]"></ng-content>
|
|
20977
|
-
</val-layout>
|
|
20978
|
-
</ion-content>
|
|
20979
|
-
<ng-content select="[outter-container]"></ng-content>
|
|
20980
|
-
`, styles: [".description-container{padding:0 16px}\n"] }]
|
|
20981
|
-
}], propDecorators: { props: [{
|
|
20982
|
-
type: Input
|
|
20983
|
-
}], onClick: [{
|
|
20984
|
-
type: Output
|
|
20985
|
-
}], onScrollEvent: [{
|
|
20986
|
-
type: Output
|
|
20987
|
-
}] } });
|
|
21135
|
+
/**
|
|
21136
|
+
* Configuracion por defecto para el sistema de skeletons.
|
|
21137
|
+
*/
|
|
21138
|
+
const DEFAULT_SKELETON_CONFIG = {
|
|
21139
|
+
animated: true,
|
|
21140
|
+
defaultDelay: 0,
|
|
21141
|
+
defaultMinTime: 300,
|
|
21142
|
+
defaultListTemplate: 'list',
|
|
21143
|
+
defaultGridTemplate: 'grid-cards',
|
|
21144
|
+
};
|
|
20988
21145
|
|
|
20989
21146
|
/**
|
|
20990
|
-
*
|
|
20991
|
-
*
|
|
20992
|
-
* A page template component with title, expandable description,
|
|
20993
|
-
* content projection, and optional back navigation button.
|
|
21147
|
+
* Servicio para gestionar templates de skeleton y estados de carga globales.
|
|
20994
21148
|
*
|
|
20995
21149
|
* @example
|
|
20996
|
-
*
|
|
20997
|
-
*
|
|
20998
|
-
* pageTitle: 'Getting Started',
|
|
20999
|
-
* pageDescription: 'Learn how to use our components...',
|
|
21000
|
-
* showBackButton: true
|
|
21001
|
-
* }"
|
|
21002
|
-
* >
|
|
21003
|
-
* <div extra-description>
|
|
21004
|
-
* <p>Additional info here</p>
|
|
21005
|
-
* </div>
|
|
21150
|
+
* // En un componente
|
|
21151
|
+
* skeleton = inject(SkeletonService);
|
|
21006
21152
|
*
|
|
21007
|
-
*
|
|
21008
|
-
*
|
|
21153
|
+
* // Registrar template personalizado
|
|
21154
|
+
* skeleton.registerTemplate('my-custom', MyCustomSkeletonComponent);
|
|
21009
21155
|
*
|
|
21010
|
-
*
|
|
21011
|
-
*
|
|
21012
|
-
* </div>
|
|
21013
|
-
* </val-page-template>
|
|
21156
|
+
* // Obtener componente de template
|
|
21157
|
+
* const component = skeleton.getTemplate('list');
|
|
21014
21158
|
*
|
|
21015
|
-
*
|
|
21016
|
-
*
|
|
21159
|
+
* // Gestionar estados de carga globales
|
|
21160
|
+
* skeleton.setLoadingState('dashboard', true);
|
|
21161
|
+
* const isDashboardLoading = skeleton.loadingState('dashboard');
|
|
21017
21162
|
*/
|
|
21018
|
-
class
|
|
21163
|
+
class SkeletonService {
|
|
21019
21164
|
constructor() {
|
|
21020
|
-
this.
|
|
21021
|
-
|
|
21022
|
-
|
|
21023
|
-
|
|
21024
|
-
|
|
21025
|
-
|
|
21026
|
-
|
|
21027
|
-
|
|
21028
|
-
|
|
21165
|
+
this._config = signal(DEFAULT_SKELETON_CONFIG);
|
|
21166
|
+
this._templates = signal(new Map());
|
|
21167
|
+
this._loadingStates = signal(new Map());
|
|
21168
|
+
this._initialized = false;
|
|
21169
|
+
/** Configuracion actual (solo lectura) */
|
|
21170
|
+
this.config = this._config.asReadonly();
|
|
21171
|
+
/** Templates registrados (solo lectura) */
|
|
21172
|
+
this.templates = this._templates.asReadonly();
|
|
21173
|
+
/** Estado de carga global (cualquier estado registrado esta cargando) */
|
|
21174
|
+
this.isAnyLoading = computed(() => {
|
|
21175
|
+
const states = this._loadingStates();
|
|
21176
|
+
return Array.from(states.values()).some((v) => v);
|
|
21177
|
+
});
|
|
21178
|
+
/** Cantidad de templates registrados */
|
|
21179
|
+
this.templateCount = computed(() => this._templates().size);
|
|
21029
21180
|
}
|
|
21030
21181
|
/**
|
|
21031
|
-
*
|
|
21182
|
+
* Configura el servicio de skeleton.
|
|
21183
|
+
* Llamado por provideValtechSkeleton().
|
|
21032
21184
|
*/
|
|
21033
|
-
|
|
21034
|
-
this.
|
|
21035
|
-
|
|
21185
|
+
configure(config) {
|
|
21186
|
+
if (this._initialized) {
|
|
21187
|
+
console.warn('[SkeletonService] Service already configured. Ignoring reconfiguration.');
|
|
21188
|
+
return;
|
|
21189
|
+
}
|
|
21190
|
+
this._config.set({ ...DEFAULT_SKELETON_CONFIG, ...config });
|
|
21191
|
+
// Registrar templates personalizados de la configuracion
|
|
21192
|
+
config.templates?.forEach((t) => {
|
|
21193
|
+
this.registerTemplate(t.name, t.component, t.defaultConfig);
|
|
21194
|
+
});
|
|
21195
|
+
this._initialized = true;
|
|
21036
21196
|
}
|
|
21037
|
-
|
|
21038
|
-
|
|
21039
|
-
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
|
|
21197
|
+
/**
|
|
21198
|
+
* Registra un template de skeleton personalizado.
|
|
21199
|
+
*
|
|
21200
|
+
* @param name Nombre unico del template
|
|
21201
|
+
* @param component Componente a usar
|
|
21202
|
+
* @param defaultConfig Configuracion por defecto opcional
|
|
21203
|
+
*/
|
|
21204
|
+
registerTemplate(name, component, defaultConfig) {
|
|
21205
|
+
this._templates.update((map) => {
|
|
21206
|
+
const newMap = new Map(map);
|
|
21207
|
+
newMap.set(name, { name, component, defaultConfig });
|
|
21208
|
+
return newMap;
|
|
21209
|
+
});
|
|
21045
21210
|
}
|
|
21046
|
-
|
|
21047
|
-
|
|
21048
|
-
|
|
21049
|
-
|
|
21050
|
-
|
|
21051
|
-
|
|
21052
|
-
|
|
21053
|
-
|
|
21211
|
+
/**
|
|
21212
|
+
* Obtiene un template de skeleton registrado.
|
|
21213
|
+
*
|
|
21214
|
+
* @param name Nombre del template
|
|
21215
|
+
* @returns Template registrado o undefined si no existe
|
|
21216
|
+
*/
|
|
21217
|
+
getTemplate(name) {
|
|
21218
|
+
return this._templates().get(name);
|
|
21219
|
+
}
|
|
21220
|
+
/**
|
|
21221
|
+
* Verifica si un template esta registrado.
|
|
21222
|
+
*
|
|
21223
|
+
* @param name Nombre del template
|
|
21224
|
+
* @returns true si el template existe
|
|
21225
|
+
*/
|
|
21226
|
+
hasTemplate(name) {
|
|
21227
|
+
return this._templates().has(name);
|
|
21228
|
+
}
|
|
21229
|
+
/**
|
|
21230
|
+
* Obtiene todos los nombres de templates registrados.
|
|
21231
|
+
*
|
|
21232
|
+
* @returns Array de nombres de templates
|
|
21233
|
+
*/
|
|
21234
|
+
getTemplateNames() {
|
|
21235
|
+
return Array.from(this._templates().keys());
|
|
21236
|
+
}
|
|
21237
|
+
/**
|
|
21238
|
+
* Registra un estado de carga nombrado.
|
|
21239
|
+
* Util para indicadores de carga globales.
|
|
21240
|
+
*
|
|
21241
|
+
* @param key Identificador unico del estado
|
|
21242
|
+
* @param isLoading Estado de carga
|
|
21243
|
+
*/
|
|
21244
|
+
setLoadingState(key, isLoading) {
|
|
21245
|
+
this._loadingStates.update((map) => {
|
|
21246
|
+
const newMap = new Map(map);
|
|
21247
|
+
if (isLoading) {
|
|
21248
|
+
newMap.set(key, true);
|
|
21249
|
+
}
|
|
21250
|
+
else {
|
|
21251
|
+
newMap.delete(key);
|
|
21252
|
+
}
|
|
21253
|
+
return newMap;
|
|
21254
|
+
});
|
|
21255
|
+
}
|
|
21256
|
+
/**
|
|
21257
|
+
* Obtiene el estado de carga para una clave especifica.
|
|
21258
|
+
*
|
|
21259
|
+
* @param key Identificador del estado
|
|
21260
|
+
* @returns Estado de carga actual
|
|
21261
|
+
*/
|
|
21262
|
+
getLoadingState(key) {
|
|
21263
|
+
return this._loadingStates().get(key) ?? false;
|
|
21264
|
+
}
|
|
21265
|
+
/**
|
|
21266
|
+
* Crea un signal computado para un estado de carga especifico.
|
|
21267
|
+
*
|
|
21268
|
+
* @param key Identificador del estado
|
|
21269
|
+
* @returns Signal reactivo del estado de carga
|
|
21270
|
+
*/
|
|
21271
|
+
loadingState(key) {
|
|
21272
|
+
return computed(() => this._loadingStates().get(key) ?? false);
|
|
21273
|
+
}
|
|
21274
|
+
/**
|
|
21275
|
+
* Obtiene todas las claves de estados de carga activos.
|
|
21276
|
+
*
|
|
21277
|
+
* @returns Array de claves con estado de carga activo
|
|
21278
|
+
*/
|
|
21279
|
+
getActiveLoadingKeys() {
|
|
21280
|
+
const states = this._loadingStates();
|
|
21281
|
+
return Array.from(states.entries())
|
|
21282
|
+
.filter(([, isLoading]) => isLoading)
|
|
21283
|
+
.map(([key]) => key);
|
|
21284
|
+
}
|
|
21285
|
+
/**
|
|
21286
|
+
* Limpia todos los estados de carga.
|
|
21287
|
+
*/
|
|
21288
|
+
clearLoadingStates() {
|
|
21289
|
+
this._loadingStates.set(new Map());
|
|
21290
|
+
}
|
|
21291
|
+
/**
|
|
21292
|
+
* Limpia un estado de carga especifico.
|
|
21293
|
+
*
|
|
21294
|
+
* @param key Identificador del estado a limpiar
|
|
21295
|
+
*/
|
|
21296
|
+
clearLoadingState(key) {
|
|
21297
|
+
this.setLoadingState(key, false);
|
|
21298
|
+
}
|
|
21299
|
+
/**
|
|
21300
|
+
* Ejecuta una funcion async con tracking de estado de carga.
|
|
21301
|
+
*
|
|
21302
|
+
* @param key Identificador del estado
|
|
21303
|
+
* @param fn Funcion async a ejecutar
|
|
21304
|
+
* @returns Resultado de la funcion
|
|
21305
|
+
*/
|
|
21306
|
+
async withLoading(key, fn) {
|
|
21307
|
+
this.setLoadingState(key, true);
|
|
21308
|
+
try {
|
|
21309
|
+
return await fn();
|
|
21310
|
+
}
|
|
21311
|
+
finally {
|
|
21312
|
+
this.setLoadingState(key, false);
|
|
21313
|
+
}
|
|
21314
|
+
}
|
|
21315
|
+
/**
|
|
21316
|
+
* Obtiene la configuracion de animacion por defecto.
|
|
21317
|
+
*/
|
|
21318
|
+
get animated() {
|
|
21319
|
+
return this._config().animated ?? true;
|
|
21320
|
+
}
|
|
21321
|
+
/**
|
|
21322
|
+
* Obtiene el delay por defecto.
|
|
21323
|
+
*/
|
|
21324
|
+
get defaultDelay() {
|
|
21325
|
+
return this._config().defaultDelay ?? 0;
|
|
21326
|
+
}
|
|
21327
|
+
/**
|
|
21328
|
+
* Obtiene el tiempo minimo por defecto.
|
|
21329
|
+
*/
|
|
21330
|
+
get defaultMinTime() {
|
|
21331
|
+
return this._config().defaultMinTime ?? 300;
|
|
21332
|
+
}
|
|
21333
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SkeletonService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
21334
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SkeletonService, providedIn: 'root' }); }
|
|
21335
|
+
}
|
|
21336
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SkeletonService, decorators: [{
|
|
21337
|
+
type: Injectable,
|
|
21338
|
+
args: [{ providedIn: 'root' }]
|
|
21339
|
+
}] });
|
|
21340
|
+
|
|
21341
|
+
/**
|
|
21342
|
+
* Componente wrapper para listas con infinite scroll.
|
|
21343
|
+
*
|
|
21344
|
+
* @example
|
|
21345
|
+
* <!-- Uso basico con data source -->
|
|
21346
|
+
* <val-infinite-list
|
|
21347
|
+
* [props]="{
|
|
21348
|
+
* dataSource: { loadFn: loadUsers, trackBy: trackByUserId },
|
|
21349
|
+
* itemTemplate: userTemplate,
|
|
21350
|
+
* pageSize: 20,
|
|
21351
|
+
* threshold: '150px'
|
|
21352
|
+
* }"
|
|
21353
|
+
* ></val-infinite-list>
|
|
21354
|
+
*
|
|
21355
|
+
* <ng-template #userTemplate let-user let-index="index">
|
|
21356
|
+
* <val-card [props]="{ title: user.name, subtitle: user.email }">
|
|
21357
|
+
* <p>{{ user.bio }}</p>
|
|
21358
|
+
* </val-card>
|
|
21359
|
+
* </ng-template>
|
|
21360
|
+
*
|
|
21361
|
+
* @example
|
|
21362
|
+
* <!-- Con pull-to-refresh y estado vacio personalizado -->
|
|
21363
|
+
* <val-infinite-list
|
|
21364
|
+
* [props]="{
|
|
21365
|
+
* dataSource: { loadFn: loadMessages },
|
|
21366
|
+
* itemTemplate: messageTemplate,
|
|
21367
|
+
* direction: 'both',
|
|
21368
|
+
* enableRefresh: true,
|
|
21369
|
+
* emptyState: {
|
|
21370
|
+
* icon: 'chatbubbles-outline',
|
|
21371
|
+
* title: 'Sin mensajes',
|
|
21372
|
+
* message: 'Inicia una conversacion'
|
|
21373
|
+
* },
|
|
21374
|
+
* skeleton: { template: 'list', count: 5 }
|
|
21375
|
+
* }"
|
|
21376
|
+
* (refresh)="onRefresh($event)"
|
|
21377
|
+
* ></val-infinite-list>
|
|
21378
|
+
*/
|
|
21379
|
+
class InfiniteListComponent {
|
|
21380
|
+
constructor() {
|
|
21381
|
+
this.skeletonService = inject(SkeletonService);
|
|
21382
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
21383
|
+
// === Events ===
|
|
21384
|
+
this.loadMore = new EventEmitter();
|
|
21385
|
+
this.refresh = new EventEmitter();
|
|
21386
|
+
this.stateChange = new EventEmitter();
|
|
21387
|
+
this.itemsChange = new EventEmitter();
|
|
21388
|
+
this.errorOccurred = new EventEmitter();
|
|
21389
|
+
// === Reactive State ===
|
|
21390
|
+
this.items = signal([]);
|
|
21391
|
+
this.state = signal('idle');
|
|
21392
|
+
this.hasMoreBottom = signal(true);
|
|
21393
|
+
this.hasMoreTop = signal(false);
|
|
21394
|
+
this.error = signal(null);
|
|
21395
|
+
this.isInitialLoad = signal(true);
|
|
21396
|
+
this.currentPage = 0;
|
|
21397
|
+
this.currentCursor = null;
|
|
21398
|
+
/** Progreso de carga (0-1 si totalCount conocido) */
|
|
21399
|
+
this.loadProgress = computed(() => {
|
|
21400
|
+
if (!this.props?.dataSource?.totalCount)
|
|
21401
|
+
return null;
|
|
21402
|
+
return this.items().length / this.props.dataSource.totalCount;
|
|
21403
|
+
});
|
|
21404
|
+
/** Anuncio de estado para lectores de pantalla */
|
|
21405
|
+
this.statusAnnouncement = computed(() => {
|
|
21406
|
+
switch (this.state()) {
|
|
21407
|
+
case 'loading':
|
|
21408
|
+
return 'Cargando items...';
|
|
21409
|
+
case 'error':
|
|
21410
|
+
return `Error: ${this.error()?.message || 'Ocurrio un error'}`;
|
|
21411
|
+
case 'complete':
|
|
21412
|
+
return 'Todos los items han sido cargados';
|
|
21413
|
+
default:
|
|
21414
|
+
return '';
|
|
21415
|
+
}
|
|
21416
|
+
});
|
|
21417
|
+
}
|
|
21418
|
+
/** Props combinados con defaults */
|
|
21419
|
+
get mergedProps() {
|
|
21420
|
+
return { ...DEFAULT_INFINITE_LIST_METADATA, ...this.props };
|
|
21421
|
+
}
|
|
21422
|
+
/** Config del refresher */
|
|
21423
|
+
get refresherConfig() {
|
|
21424
|
+
return this.mergedProps.refreshConfig ?? {};
|
|
21425
|
+
}
|
|
21426
|
+
/** Componente de skeleton a usar */
|
|
21427
|
+
get skeletonComponent() {
|
|
21428
|
+
const templateName = this.mergedProps.skeleton?.template || 'list';
|
|
21429
|
+
const template = this.skeletonService.getTemplate(templateName);
|
|
21430
|
+
return template?.component ?? null;
|
|
21431
|
+
}
|
|
21432
|
+
/** Inputs para el skeleton */
|
|
21433
|
+
get skeletonInputs() {
|
|
21434
|
+
return {
|
|
21435
|
+
config: {
|
|
21436
|
+
count: this.mergedProps.skeleton?.count ?? 3,
|
|
21437
|
+
animated: true,
|
|
21438
|
+
...this.mergedProps.skeleton?.config,
|
|
21439
|
+
},
|
|
21440
|
+
};
|
|
21441
|
+
}
|
|
21442
|
+
ngOnInit() {
|
|
21443
|
+
// Cargar items iniciales del dataSource si existen
|
|
21444
|
+
if (this.props.dataSource.items?.length) {
|
|
21445
|
+
this.items.set([...this.props.dataSource.items]);
|
|
21446
|
+
this.isInitialLoad.set(false);
|
|
21447
|
+
}
|
|
21448
|
+
// Auto-cargar si esta habilitado
|
|
21449
|
+
if (this.mergedProps.autoLoad && !this.items().length) {
|
|
21450
|
+
this.loadInitial();
|
|
21451
|
+
}
|
|
21452
|
+
}
|
|
21453
|
+
ngOnDestroy() {
|
|
21454
|
+
// Cleanup
|
|
21455
|
+
}
|
|
21456
|
+
/** Funcion de tracking para ngFor */
|
|
21457
|
+
trackByFn(index, item) {
|
|
21458
|
+
if (this.props.dataSource.trackBy) {
|
|
21459
|
+
return this.props.dataSource.trackBy(index, item);
|
|
21460
|
+
}
|
|
21461
|
+
return index;
|
|
21462
|
+
}
|
|
21463
|
+
/** Si debe mostrar el scroll inferior */
|
|
21464
|
+
shouldShowBottomScroll() {
|
|
21465
|
+
const dir = this.mergedProps.direction;
|
|
21466
|
+
return (dir === 'bottom' || dir === 'both') && this.items().length > 0;
|
|
21467
|
+
}
|
|
21468
|
+
/** Carga inicial de datos */
|
|
21469
|
+
async loadInitial() {
|
|
21470
|
+
if (!this.props.dataSource.loadFn)
|
|
21471
|
+
return;
|
|
21472
|
+
this.state.set('loading');
|
|
21473
|
+
this.stateChange.emit('loading');
|
|
21474
|
+
this.error.set(null);
|
|
21475
|
+
try {
|
|
21476
|
+
const params = {
|
|
21477
|
+
direction: 'bottom',
|
|
21478
|
+
page: 0,
|
|
21479
|
+
pageSize: this.mergedProps.pageSize ?? 20,
|
|
21480
|
+
};
|
|
21481
|
+
const result = await this.executeLoad(params);
|
|
21482
|
+
this.items.set(result.items);
|
|
21483
|
+
this.hasMoreBottom.set(result.hasMore);
|
|
21484
|
+
this.currentPage = 1;
|
|
21485
|
+
this.currentCursor = result.cursor;
|
|
21486
|
+
this.isInitialLoad.set(false);
|
|
21487
|
+
this.state.set('idle');
|
|
21488
|
+
this.stateChange.emit('idle');
|
|
21489
|
+
this.itemsChange.emit(this.items());
|
|
21490
|
+
}
|
|
21491
|
+
catch (err) {
|
|
21492
|
+
this.handleError(err);
|
|
21493
|
+
}
|
|
21494
|
+
}
|
|
21495
|
+
/** Cargar mas items en la parte inferior */
|
|
21496
|
+
async loadBottom() {
|
|
21497
|
+
if (!this.hasMoreBottom() || this.state() === 'loading')
|
|
21498
|
+
return;
|
|
21499
|
+
if (!this.props.dataSource.loadFn)
|
|
21500
|
+
return;
|
|
21501
|
+
this.state.set('loading');
|
|
21502
|
+
this.stateChange.emit('loading');
|
|
21503
|
+
try {
|
|
21504
|
+
const params = {
|
|
21505
|
+
direction: 'bottom',
|
|
21506
|
+
page: this.currentPage,
|
|
21507
|
+
pageSize: this.mergedProps.pageSize ?? 20,
|
|
21508
|
+
cursor: this.currentCursor,
|
|
21509
|
+
lastItem: this.items()[this.items().length - 1],
|
|
21510
|
+
};
|
|
21511
|
+
const result = await this.executeLoad(params);
|
|
21512
|
+
this.items.update((current) => [...current, ...result.items]);
|
|
21513
|
+
this.hasMoreBottom.set(result.hasMore);
|
|
21514
|
+
this.currentPage++;
|
|
21515
|
+
this.currentCursor = result.cursor;
|
|
21516
|
+
this.state.set(result.hasMore ? 'idle' : 'complete');
|
|
21517
|
+
this.stateChange.emit(result.hasMore ? 'idle' : 'complete');
|
|
21518
|
+
this.itemsChange.emit(this.items());
|
|
21519
|
+
}
|
|
21520
|
+
catch (err) {
|
|
21521
|
+
this.handleError(err);
|
|
21522
|
+
}
|
|
21523
|
+
}
|
|
21524
|
+
/** Cargar mas items en la parte superior */
|
|
21525
|
+
async loadTop() {
|
|
21526
|
+
if (!this.hasMoreTop() || this.state() === 'loading')
|
|
21527
|
+
return;
|
|
21528
|
+
if (!this.props.dataSource.loadFn)
|
|
21529
|
+
return;
|
|
21530
|
+
this.state.set('loading');
|
|
21531
|
+
try {
|
|
21532
|
+
const params = {
|
|
21533
|
+
direction: 'top',
|
|
21534
|
+
page: 0,
|
|
21535
|
+
pageSize: this.mergedProps.pageSize ?? 20,
|
|
21536
|
+
firstItem: this.items()[0],
|
|
21537
|
+
};
|
|
21538
|
+
const result = await this.executeLoad(params);
|
|
21539
|
+
this.items.update((current) => [...result.items, ...current]);
|
|
21540
|
+
this.hasMoreTop.set(result.hasMore);
|
|
21541
|
+
this.state.set('idle');
|
|
21542
|
+
this.itemsChange.emit(this.items());
|
|
21543
|
+
}
|
|
21544
|
+
catch (err) {
|
|
21545
|
+
this.handleError(err);
|
|
21546
|
+
}
|
|
21547
|
+
}
|
|
21548
|
+
/** Refresh - recargar desde cero */
|
|
21549
|
+
async refreshList() {
|
|
21550
|
+
this.currentPage = 0;
|
|
21551
|
+
this.currentCursor = null;
|
|
21552
|
+
this.hasMoreBottom.set(true);
|
|
21553
|
+
this.items.set([]);
|
|
21554
|
+
await this.loadInitial();
|
|
21555
|
+
}
|
|
21556
|
+
/** Reintentar despues de error */
|
|
21557
|
+
async retry() {
|
|
21558
|
+
this.error.set(null);
|
|
21559
|
+
if (this.items().length === 0) {
|
|
21560
|
+
await this.loadInitial();
|
|
21561
|
+
}
|
|
21562
|
+
else {
|
|
21563
|
+
await this.loadBottom();
|
|
21564
|
+
}
|
|
21565
|
+
}
|
|
21566
|
+
/** Reset completo */
|
|
21567
|
+
async reset() {
|
|
21568
|
+
this.items.set([]);
|
|
21569
|
+
this.currentPage = 0;
|
|
21570
|
+
this.currentCursor = null;
|
|
21571
|
+
this.hasMoreBottom.set(true);
|
|
21572
|
+
this.hasMoreTop.set(false);
|
|
21573
|
+
this.error.set(null);
|
|
21574
|
+
this.isInitialLoad.set(true);
|
|
21575
|
+
this.state.set('idle');
|
|
21576
|
+
if (this.mergedProps.autoLoad) {
|
|
21577
|
+
await this.loadInitial();
|
|
21578
|
+
}
|
|
21579
|
+
}
|
|
21580
|
+
/** Agregar items al inicio */
|
|
21581
|
+
prependItems(newItems) {
|
|
21582
|
+
this.items.update((current) => [...newItems, ...current]);
|
|
21583
|
+
this.itemsChange.emit(this.items());
|
|
21584
|
+
}
|
|
21585
|
+
/** Agregar items al final */
|
|
21586
|
+
appendItems(newItems) {
|
|
21587
|
+
this.items.update((current) => [...current, ...newItems]);
|
|
21588
|
+
this.itemsChange.emit(this.items());
|
|
21589
|
+
}
|
|
21590
|
+
/** Actualizar un item por indice */
|
|
21591
|
+
updateItem(index, item) {
|
|
21592
|
+
this.items.update((current) => {
|
|
21593
|
+
const updated = [...current];
|
|
21594
|
+
updated[index] = item;
|
|
21595
|
+
return updated;
|
|
21596
|
+
});
|
|
21597
|
+
this.itemsChange.emit(this.items());
|
|
21598
|
+
}
|
|
21599
|
+
/** Remover un item por indice */
|
|
21600
|
+
removeItem(index) {
|
|
21601
|
+
this.items.update((current) => current.filter((_, i) => i !== index));
|
|
21602
|
+
this.itemsChange.emit(this.items());
|
|
21603
|
+
}
|
|
21604
|
+
/** Handler para evento de infinite scroll */
|
|
21605
|
+
async onInfiniteScroll(event) {
|
|
21606
|
+
await this.loadBottom();
|
|
21607
|
+
event.target.complete();
|
|
21608
|
+
}
|
|
21609
|
+
/** Handler para evento de refresh */
|
|
21610
|
+
async onRefreshTriggered(event) {
|
|
21611
|
+
this.refresh.emit(event);
|
|
21612
|
+
await this.refreshList();
|
|
21613
|
+
event.complete();
|
|
21614
|
+
}
|
|
21615
|
+
async executeLoad(params) {
|
|
21616
|
+
const loadFn = this.props.dataSource.loadFn;
|
|
21617
|
+
const result = loadFn(params);
|
|
21618
|
+
if (isObservable(result)) {
|
|
21619
|
+
return await firstValueFrom(result);
|
|
21620
|
+
}
|
|
21621
|
+
return await result;
|
|
21622
|
+
}
|
|
21623
|
+
handleError(err) {
|
|
21624
|
+
this.error.set(err);
|
|
21625
|
+
this.state.set('error');
|
|
21626
|
+
this.stateChange.emit('error');
|
|
21627
|
+
this.errorOccurred.emit(err);
|
|
21628
|
+
console.error('[InfiniteList] Error loading items:', err);
|
|
21629
|
+
}
|
|
21630
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InfiniteListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21631
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: InfiniteListComponent, isStandalone: true, selector: "val-infinite-list", inputs: { props: "props" }, outputs: { loadMore: "loadMore", refresh: "refresh", stateChange: "stateChange", itemsChange: "itemsChange", errorOccurred: "errorOccurred" }, viewQueries: [{ propertyName: "infiniteScroll", first: true, predicate: IonInfiniteScroll, descendants: true }], ngImport: i0, template: `
|
|
21632
|
+
<!-- Pull to refresh wrapper -->
|
|
21633
|
+
@if (mergedProps.enableRefresh) {
|
|
21634
|
+
<val-refresher [props]="refresherConfig" (refresh)="onRefreshTriggered($event)">
|
|
21635
|
+
<ng-container *ngTemplateOutlet="listContent"></ng-container>
|
|
21636
|
+
</val-refresher>
|
|
21637
|
+
} @else {
|
|
21638
|
+
<ng-container *ngTemplateOutlet="listContent"></ng-container>
|
|
21639
|
+
}
|
|
21640
|
+
|
|
21641
|
+
<!-- Main list content template -->
|
|
21642
|
+
<ng-template #listContent>
|
|
21643
|
+
<div
|
|
21644
|
+
class="infinite-list-container"
|
|
21645
|
+
[class]="mergedProps.cssClass"
|
|
21646
|
+
[style.max-height]="mergedProps.maxHeight"
|
|
21647
|
+
[style.overflow-y]="mergedProps.maxHeight ? 'auto' : 'visible'"
|
|
21648
|
+
role="feed"
|
|
21649
|
+
[attr.aria-busy]="state() === 'loading'"
|
|
21650
|
+
[attr.aria-label]="mergedProps.ariaLabel"
|
|
21651
|
+
[attr.aria-description]="mergedProps.ariaDescription"
|
|
21652
|
+
>
|
|
21653
|
+
<!-- Loading state (initial) -->
|
|
21654
|
+
@if (state() === 'loading' && items().length === 0) {
|
|
21655
|
+
<div class="infinite-list-skeleton">
|
|
21656
|
+
@if (mergedProps.skeleton?.customTemplate) {
|
|
21657
|
+
<ng-container *ngTemplateOutlet="mergedProps.skeleton.customTemplate"></ng-container>
|
|
21658
|
+
} @else {
|
|
21659
|
+
<ng-container *ngComponentOutlet="skeletonComponent; inputs: skeletonInputs"></ng-container>
|
|
21660
|
+
}
|
|
21661
|
+
</div>
|
|
21662
|
+
}
|
|
21663
|
+
|
|
21664
|
+
<!-- Empty state -->
|
|
21665
|
+
@if (state() === 'idle' && items().length === 0 && !isInitialLoad()) {
|
|
21666
|
+
<div class="infinite-list-empty">
|
|
21667
|
+
@if (mergedProps.emptyState?.template) {
|
|
21668
|
+
<ng-container *ngTemplateOutlet="mergedProps.emptyState.template"></ng-container>
|
|
21669
|
+
} @else {
|
|
21670
|
+
@if (mergedProps.emptyState?.icon) {
|
|
21671
|
+
<ion-icon [name]="mergedProps.emptyState.icon" size="large"></ion-icon>
|
|
21672
|
+
}
|
|
21673
|
+
@if (mergedProps.emptyState?.title) {
|
|
21674
|
+
<h3>{{ mergedProps.emptyState.title }}</h3>
|
|
21675
|
+
}
|
|
21676
|
+
@if (mergedProps.emptyState?.message) {
|
|
21677
|
+
<p>{{ mergedProps.emptyState.message }}</p>
|
|
21678
|
+
}
|
|
21679
|
+
}
|
|
21680
|
+
</div>
|
|
21681
|
+
}
|
|
21682
|
+
|
|
21683
|
+
<!-- Error state -->
|
|
21684
|
+
@if (state() === 'error') {
|
|
21685
|
+
<div class="infinite-list-error">
|
|
21686
|
+
@if (mergedProps.errorState?.template) {
|
|
21687
|
+
<ng-container
|
|
21688
|
+
*ngTemplateOutlet="mergedProps.errorState.template; context: { error: error(), retry: retry.bind(this) }"
|
|
21689
|
+
></ng-container>
|
|
21690
|
+
} @else {
|
|
21691
|
+
@if (mergedProps.errorState?.icon) {
|
|
21692
|
+
<ion-icon [name]="mergedProps.errorState.icon" color="danger" size="large"></ion-icon>
|
|
21693
|
+
} @else {
|
|
21694
|
+
<ion-icon name="alert-circle-outline" color="danger" size="large"></ion-icon>
|
|
21695
|
+
}
|
|
21696
|
+
<h3>{{ mergedProps.errorState?.title || 'Error' }}</h3>
|
|
21697
|
+
<p>{{ mergedProps.errorState?.message || error()?.message || 'Ocurrio un error' }}</p>
|
|
21698
|
+
@if (mergedProps.errorState?.showRetry !== false) {
|
|
21699
|
+
<ion-button fill="outline" (click)="retry()">
|
|
21700
|
+
{{ mergedProps.errorState?.retryText || 'Reintentar' }}
|
|
21701
|
+
</ion-button>
|
|
21702
|
+
}
|
|
21703
|
+
}
|
|
21704
|
+
</div>
|
|
21705
|
+
}
|
|
21706
|
+
|
|
21707
|
+
<!-- Items list -->
|
|
21708
|
+
@if (items().length > 0) {
|
|
21709
|
+
<div class="infinite-list-items" [class.with-dividers]="mergedProps.showDividers">
|
|
21710
|
+
@for (item of items(); track trackByFn($index, item); let i = $index; let first = $first; let last = $last) {
|
|
21711
|
+
<article
|
|
21712
|
+
class="infinite-list-item"
|
|
21713
|
+
[attr.aria-setsize]="mergedProps.dataSource.totalCount || null"
|
|
21714
|
+
[attr.aria-posinset]="i + 1"
|
|
21715
|
+
>
|
|
21716
|
+
<ng-container
|
|
21717
|
+
*ngTemplateOutlet="
|
|
21718
|
+
mergedProps.itemTemplate;
|
|
21719
|
+
context: { $implicit: item, index: i, first: first, last: last, count: items().length }
|
|
21720
|
+
"
|
|
21721
|
+
></ng-container>
|
|
21722
|
+
</article>
|
|
21723
|
+
}
|
|
21724
|
+
</div>
|
|
21725
|
+
}
|
|
21726
|
+
|
|
21727
|
+
<!-- Bottom infinite scroll -->
|
|
21728
|
+
@if (shouldShowBottomScroll()) {
|
|
21729
|
+
@if (mergedProps.useLoadMoreButton) {
|
|
21730
|
+
<div class="infinite-list-load-more">
|
|
21731
|
+
@if (hasMoreBottom()) {
|
|
21732
|
+
<ion-button
|
|
21733
|
+
fill="outline"
|
|
21734
|
+
[color]="mergedProps.color"
|
|
21735
|
+
[disabled]="state() === 'loading'"
|
|
21736
|
+
(click)="loadBottom()"
|
|
21737
|
+
>
|
|
21738
|
+
@if (state() === 'loading') {
|
|
21739
|
+
<ion-spinner [name]="mergedProps.spinnerType" slot="start"></ion-spinner>
|
|
21740
|
+
}
|
|
21741
|
+
{{ mergedProps.loadMoreText || 'Cargar mas' }}
|
|
21742
|
+
</ion-button>
|
|
21743
|
+
} @else {
|
|
21744
|
+
<ion-text color="medium">{{ mergedProps.noMoreText || 'No hay mas items' }}</ion-text>
|
|
21745
|
+
}
|
|
21746
|
+
</div>
|
|
21747
|
+
} @else {
|
|
21748
|
+
<ion-infinite-scroll
|
|
21749
|
+
[threshold]="mergedProps.threshold"
|
|
21750
|
+
[disabled]="!hasMoreBottom()"
|
|
21751
|
+
(ionInfinite)="onInfiniteScroll($event)"
|
|
21752
|
+
>
|
|
21753
|
+
<ion-infinite-scroll-content
|
|
21754
|
+
[loadingSpinner]="mergedProps.spinnerType"
|
|
21755
|
+
[loadingText]="state() === 'loading' ? 'Cargando...' : ''"
|
|
21756
|
+
></ion-infinite-scroll-content>
|
|
21757
|
+
</ion-infinite-scroll>
|
|
21758
|
+
}
|
|
21759
|
+
}
|
|
21760
|
+
|
|
21761
|
+
<!-- No more items indicator -->
|
|
21762
|
+
@if (!hasMoreBottom() && items().length > 0 && !mergedProps.useLoadMoreButton) {
|
|
21763
|
+
<div class="infinite-list-end">
|
|
21764
|
+
<ion-text color="medium">{{ mergedProps.noMoreText || 'No hay mas items' }}</ion-text>
|
|
21765
|
+
</div>
|
|
21766
|
+
}
|
|
21767
|
+
</div>
|
|
21768
|
+
</ng-template>
|
|
21769
|
+
|
|
21770
|
+
<!-- Live region for accessibility announcements -->
|
|
21771
|
+
<div class="sr-only" role="status" aria-live="polite" [attr.aria-atomic]="true">
|
|
21772
|
+
{{ statusAnnouncement() }}
|
|
21773
|
+
</div>
|
|
21774
|
+
`, isInline: true, styles: [":host{display:block}.infinite-list-container{width:100%}.infinite-list-skeleton,.infinite-list-empty,.infinite-list-error{padding:24px 16px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px}.infinite-list-empty ion-icon,.infinite-list-error ion-icon{font-size:48px;opacity:.6}.infinite-list-empty h3,.infinite-list-error h3{margin:0;font-size:18px;font-weight:600}.infinite-list-empty p,.infinite-list-error p{margin:0;color:var(--ion-color-medium);font-size:14px}.infinite-list-items{&.with-dividers .infinite-list-item:not(:last-child){border-bottom:1px solid var(--ion-color-light-shade, #d7d8da)}}.infinite-list-load-more{display:flex;justify-content:center;padding:16px}.infinite-list-end{display:flex;justify-content:center;padding:16px;font-size:14px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IonInfiniteScroll, selector: "ion-infinite-scroll", inputs: ["disabled", "position", "threshold"] }, { kind: "component", type: IonInfiniteScrollContent, selector: "ion-infinite-scroll-content", inputs: ["loadingSpinner", "loadingText"] }, { 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: RefresherComponent, selector: "val-refresher", inputs: ["props"], outputs: ["refresh", "pullProgressChange", "stateChange"] }] }); }
|
|
21775
|
+
}
|
|
21776
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InfiniteListComponent, decorators: [{
|
|
21777
|
+
type: Component,
|
|
21778
|
+
args: [{ selector: 'val-infinite-list', standalone: true, imports: [
|
|
21779
|
+
CommonModule,
|
|
21780
|
+
IonInfiniteScroll,
|
|
21781
|
+
IonInfiniteScrollContent,
|
|
21782
|
+
IonButton,
|
|
21783
|
+
IonSpinner,
|
|
21784
|
+
IonIcon,
|
|
21785
|
+
IonText,
|
|
21786
|
+
IonList,
|
|
21787
|
+
IonItem,
|
|
21788
|
+
RefresherComponent,
|
|
21789
|
+
], template: `
|
|
21790
|
+
<!-- Pull to refresh wrapper -->
|
|
21791
|
+
@if (mergedProps.enableRefresh) {
|
|
21792
|
+
<val-refresher [props]="refresherConfig" (refresh)="onRefreshTriggered($event)">
|
|
21793
|
+
<ng-container *ngTemplateOutlet="listContent"></ng-container>
|
|
21794
|
+
</val-refresher>
|
|
21795
|
+
} @else {
|
|
21796
|
+
<ng-container *ngTemplateOutlet="listContent"></ng-container>
|
|
21797
|
+
}
|
|
21798
|
+
|
|
21799
|
+
<!-- Main list content template -->
|
|
21800
|
+
<ng-template #listContent>
|
|
21801
|
+
<div
|
|
21802
|
+
class="infinite-list-container"
|
|
21803
|
+
[class]="mergedProps.cssClass"
|
|
21804
|
+
[style.max-height]="mergedProps.maxHeight"
|
|
21805
|
+
[style.overflow-y]="mergedProps.maxHeight ? 'auto' : 'visible'"
|
|
21806
|
+
role="feed"
|
|
21807
|
+
[attr.aria-busy]="state() === 'loading'"
|
|
21808
|
+
[attr.aria-label]="mergedProps.ariaLabel"
|
|
21809
|
+
[attr.aria-description]="mergedProps.ariaDescription"
|
|
21810
|
+
>
|
|
21811
|
+
<!-- Loading state (initial) -->
|
|
21812
|
+
@if (state() === 'loading' && items().length === 0) {
|
|
21813
|
+
<div class="infinite-list-skeleton">
|
|
21814
|
+
@if (mergedProps.skeleton?.customTemplate) {
|
|
21815
|
+
<ng-container *ngTemplateOutlet="mergedProps.skeleton.customTemplate"></ng-container>
|
|
21816
|
+
} @else {
|
|
21817
|
+
<ng-container *ngComponentOutlet="skeletonComponent; inputs: skeletonInputs"></ng-container>
|
|
21818
|
+
}
|
|
21819
|
+
</div>
|
|
21820
|
+
}
|
|
21821
|
+
|
|
21822
|
+
<!-- Empty state -->
|
|
21823
|
+
@if (state() === 'idle' && items().length === 0 && !isInitialLoad()) {
|
|
21824
|
+
<div class="infinite-list-empty">
|
|
21825
|
+
@if (mergedProps.emptyState?.template) {
|
|
21826
|
+
<ng-container *ngTemplateOutlet="mergedProps.emptyState.template"></ng-container>
|
|
21827
|
+
} @else {
|
|
21828
|
+
@if (mergedProps.emptyState?.icon) {
|
|
21829
|
+
<ion-icon [name]="mergedProps.emptyState.icon" size="large"></ion-icon>
|
|
21830
|
+
}
|
|
21831
|
+
@if (mergedProps.emptyState?.title) {
|
|
21832
|
+
<h3>{{ mergedProps.emptyState.title }}</h3>
|
|
21833
|
+
}
|
|
21834
|
+
@if (mergedProps.emptyState?.message) {
|
|
21835
|
+
<p>{{ mergedProps.emptyState.message }}</p>
|
|
21836
|
+
}
|
|
21837
|
+
}
|
|
21838
|
+
</div>
|
|
21839
|
+
}
|
|
21840
|
+
|
|
21841
|
+
<!-- Error state -->
|
|
21842
|
+
@if (state() === 'error') {
|
|
21843
|
+
<div class="infinite-list-error">
|
|
21844
|
+
@if (mergedProps.errorState?.template) {
|
|
21845
|
+
<ng-container
|
|
21846
|
+
*ngTemplateOutlet="mergedProps.errorState.template; context: { error: error(), retry: retry.bind(this) }"
|
|
21847
|
+
></ng-container>
|
|
21848
|
+
} @else {
|
|
21849
|
+
@if (mergedProps.errorState?.icon) {
|
|
21850
|
+
<ion-icon [name]="mergedProps.errorState.icon" color="danger" size="large"></ion-icon>
|
|
21851
|
+
} @else {
|
|
21852
|
+
<ion-icon name="alert-circle-outline" color="danger" size="large"></ion-icon>
|
|
21853
|
+
}
|
|
21854
|
+
<h3>{{ mergedProps.errorState?.title || 'Error' }}</h3>
|
|
21855
|
+
<p>{{ mergedProps.errorState?.message || error()?.message || 'Ocurrio un error' }}</p>
|
|
21856
|
+
@if (mergedProps.errorState?.showRetry !== false) {
|
|
21857
|
+
<ion-button fill="outline" (click)="retry()">
|
|
21858
|
+
{{ mergedProps.errorState?.retryText || 'Reintentar' }}
|
|
21859
|
+
</ion-button>
|
|
21860
|
+
}
|
|
21861
|
+
}
|
|
21862
|
+
</div>
|
|
21863
|
+
}
|
|
21864
|
+
|
|
21865
|
+
<!-- Items list -->
|
|
21866
|
+
@if (items().length > 0) {
|
|
21867
|
+
<div class="infinite-list-items" [class.with-dividers]="mergedProps.showDividers">
|
|
21868
|
+
@for (item of items(); track trackByFn($index, item); let i = $index; let first = $first; let last = $last) {
|
|
21869
|
+
<article
|
|
21870
|
+
class="infinite-list-item"
|
|
21871
|
+
[attr.aria-setsize]="mergedProps.dataSource.totalCount || null"
|
|
21872
|
+
[attr.aria-posinset]="i + 1"
|
|
21873
|
+
>
|
|
21874
|
+
<ng-container
|
|
21875
|
+
*ngTemplateOutlet="
|
|
21876
|
+
mergedProps.itemTemplate;
|
|
21877
|
+
context: { $implicit: item, index: i, first: first, last: last, count: items().length }
|
|
21878
|
+
"
|
|
21879
|
+
></ng-container>
|
|
21880
|
+
</article>
|
|
21881
|
+
}
|
|
21882
|
+
</div>
|
|
21883
|
+
}
|
|
21884
|
+
|
|
21885
|
+
<!-- Bottom infinite scroll -->
|
|
21886
|
+
@if (shouldShowBottomScroll()) {
|
|
21887
|
+
@if (mergedProps.useLoadMoreButton) {
|
|
21888
|
+
<div class="infinite-list-load-more">
|
|
21889
|
+
@if (hasMoreBottom()) {
|
|
21890
|
+
<ion-button
|
|
21891
|
+
fill="outline"
|
|
21892
|
+
[color]="mergedProps.color"
|
|
21893
|
+
[disabled]="state() === 'loading'"
|
|
21894
|
+
(click)="loadBottom()"
|
|
21895
|
+
>
|
|
21896
|
+
@if (state() === 'loading') {
|
|
21897
|
+
<ion-spinner [name]="mergedProps.spinnerType" slot="start"></ion-spinner>
|
|
21898
|
+
}
|
|
21899
|
+
{{ mergedProps.loadMoreText || 'Cargar mas' }}
|
|
21900
|
+
</ion-button>
|
|
21901
|
+
} @else {
|
|
21902
|
+
<ion-text color="medium">{{ mergedProps.noMoreText || 'No hay mas items' }}</ion-text>
|
|
21903
|
+
}
|
|
21904
|
+
</div>
|
|
21905
|
+
} @else {
|
|
21906
|
+
<ion-infinite-scroll
|
|
21907
|
+
[threshold]="mergedProps.threshold"
|
|
21908
|
+
[disabled]="!hasMoreBottom()"
|
|
21909
|
+
(ionInfinite)="onInfiniteScroll($event)"
|
|
21910
|
+
>
|
|
21911
|
+
<ion-infinite-scroll-content
|
|
21912
|
+
[loadingSpinner]="mergedProps.spinnerType"
|
|
21913
|
+
[loadingText]="state() === 'loading' ? 'Cargando...' : ''"
|
|
21914
|
+
></ion-infinite-scroll-content>
|
|
21915
|
+
</ion-infinite-scroll>
|
|
21916
|
+
}
|
|
21917
|
+
}
|
|
21918
|
+
|
|
21919
|
+
<!-- No more items indicator -->
|
|
21920
|
+
@if (!hasMoreBottom() && items().length > 0 && !mergedProps.useLoadMoreButton) {
|
|
21921
|
+
<div class="infinite-list-end">
|
|
21922
|
+
<ion-text color="medium">{{ mergedProps.noMoreText || 'No hay mas items' }}</ion-text>
|
|
21923
|
+
</div>
|
|
21924
|
+
}
|
|
21925
|
+
</div>
|
|
21926
|
+
</ng-template>
|
|
21927
|
+
|
|
21928
|
+
<!-- Live region for accessibility announcements -->
|
|
21929
|
+
<div class="sr-only" role="status" aria-live="polite" [attr.aria-atomic]="true">
|
|
21930
|
+
{{ statusAnnouncement() }}
|
|
21931
|
+
</div>
|
|
21932
|
+
`, styles: [":host{display:block}.infinite-list-container{width:100%}.infinite-list-skeleton,.infinite-list-empty,.infinite-list-error{padding:24px 16px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px}.infinite-list-empty ion-icon,.infinite-list-error ion-icon{font-size:48px;opacity:.6}.infinite-list-empty h3,.infinite-list-error h3{margin:0;font-size:18px;font-weight:600}.infinite-list-empty p,.infinite-list-error p{margin:0;color:var(--ion-color-medium);font-size:14px}.infinite-list-items{&.with-dividers .infinite-list-item:not(:last-child){border-bottom:1px solid var(--ion-color-light-shade, #d7d8da)}}.infinite-list-load-more{display:flex;justify-content:center;padding:16px}.infinite-list-end{display:flex;justify-content:center;padding:16px;font-size:14px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
21933
|
+
}], propDecorators: { infiniteScroll: [{
|
|
21934
|
+
type: ViewChild,
|
|
21935
|
+
args: [IonInfiniteScroll]
|
|
21936
|
+
}], props: [{
|
|
21937
|
+
type: Input
|
|
21938
|
+
}], loadMore: [{
|
|
21939
|
+
type: Output
|
|
21940
|
+
}], refresh: [{
|
|
21941
|
+
type: Output
|
|
21942
|
+
}], stateChange: [{
|
|
21943
|
+
type: Output
|
|
21944
|
+
}], itemsChange: [{
|
|
21945
|
+
type: Output
|
|
21946
|
+
}], errorOccurred: [{
|
|
21947
|
+
type: Output
|
|
21948
|
+
}] } });
|
|
21949
|
+
|
|
21950
|
+
class LayoutComponent {
|
|
21951
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21952
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: LayoutComponent, isStandalone: true, selector: "val-layout", ngImport: i0, template: `
|
|
21953
|
+
<div class="layout-container">
|
|
21954
|
+
<ng-content></ng-content>
|
|
21955
|
+
</div>
|
|
21956
|
+
`, isInline: true, styles: [":root{--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #9e9e9e;--ion-color-medium-rgb: 158, 158, 158;--ion-color-medium-contrast: #000000;--ion-color-medium-contrast-rgb: 0, 0, 0;--ion-color-medium-shade: #8b8b8b;--ion-color-medium-tint: #a8a8a8;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-medium)}@media (prefers-color-scheme: dark){:root{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}.layout-container{margin:0 auto;padding:0;width:100%;box-sizing:border-box;margin-bottom:1rem;padding-top:.5rem}@media (max-width: 768px){.layout-container{max-width:100%}}@media (min-width: 768px){.layout-container{margin:0 auto;max-width:33.75rem;margin-bottom:1.5rem}}@media (min-width: 1200px){.layout-container{margin:0 auto;max-width:45rem}}\n"] }); }
|
|
21957
|
+
}
|
|
21958
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LayoutComponent, decorators: [{
|
|
21959
|
+
type: Component,
|
|
21960
|
+
args: [{ selector: 'val-layout', standalone: true, imports: [], template: `
|
|
21961
|
+
<div class="layout-container">
|
|
21962
|
+
<ng-content></ng-content>
|
|
21963
|
+
</div>
|
|
21964
|
+
`, styles: [":root{--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #9e9e9e;--ion-color-medium-rgb: 158, 158, 158;--ion-color-medium-contrast: #000000;--ion-color-medium-contrast-rgb: 0, 0, 0;--ion-color-medium-shade: #8b8b8b;--ion-color-medium-tint: #a8a8a8;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-medium)}@media (prefers-color-scheme: dark){:root{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}.layout-container{margin:0 auto;padding:0;width:100%;box-sizing:border-box;margin-bottom:1rem;padding-top:.5rem}@media (max-width: 768px){.layout-container{max-width:100%}}@media (min-width: 768px){.layout-container{margin:0 auto;max-width:33.75rem;margin-bottom:1.5rem}}@media (min-width: 1200px){.layout-container{margin:0 auto;max-width:45rem}}\n"] }]
|
|
21965
|
+
}] });
|
|
21966
|
+
|
|
21967
|
+
class SimpleComponent {
|
|
21968
|
+
constructor() {
|
|
21969
|
+
this.onClick = new EventEmitter();
|
|
21970
|
+
this.onScrollEvent = new EventEmitter();
|
|
21971
|
+
this.theme = inject(ThemeService);
|
|
21972
|
+
}
|
|
21973
|
+
onClickHandler(token) {
|
|
21974
|
+
this.onClick.emit(token);
|
|
21975
|
+
}
|
|
21976
|
+
onScrollHandler($event) {
|
|
21977
|
+
this.onScrollEvent.emit(true);
|
|
21978
|
+
}
|
|
21979
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21980
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SimpleComponent, isStandalone: true, selector: "val-simple", inputs: { props: "props" }, outputs: { onClick: "onClick", onScrollEvent: "onScrollEvent" }, ngImport: i0, template: `
|
|
21981
|
+
<val-header [props]="props.header" />
|
|
21982
|
+
|
|
21983
|
+
<ion-content
|
|
21984
|
+
[fullscreen]="true"
|
|
21985
|
+
[ngStyle]="{
|
|
21986
|
+
'--background': theme.IsDark ? 'var(--ion-background-color)' : props.background,
|
|
21987
|
+
}"
|
|
21988
|
+
[scrollEvents]="true"
|
|
21989
|
+
(ionScroll)="onScrollHandler($event)"
|
|
21990
|
+
>
|
|
21991
|
+
<ion-header collapse="condense">
|
|
21992
|
+
<ion-toolbar style="--background: transparent;">
|
|
21993
|
+
<ion-title style="padding: 0;" size="large">{{ props.pageTitle }}</ion-title>
|
|
21994
|
+
</ion-toolbar>
|
|
21995
|
+
</ion-header>
|
|
21996
|
+
@if (props.pageDescription) {
|
|
21997
|
+
<div class="description-container">
|
|
21998
|
+
<val-expandable-text
|
|
21999
|
+
[props]="{
|
|
22000
|
+
limit: 180,
|
|
22001
|
+
content: props.pageDescription,
|
|
22002
|
+
color: 'primary',
|
|
22003
|
+
expandText: 'más',
|
|
22004
|
+
}"
|
|
22005
|
+
/>
|
|
22006
|
+
</div>
|
|
22007
|
+
}
|
|
22008
|
+
@if (props.link) {
|
|
22009
|
+
<val-link [props]="props.link" (onClick)="onClickHandler($event)"></val-link>
|
|
22010
|
+
}
|
|
22011
|
+
@if (props.withDivider) {
|
|
22012
|
+
<val-divider [props]="{ fill: 'solid', size: 'medium', color: 'dark' }" />
|
|
22013
|
+
}
|
|
22014
|
+
<val-layout>
|
|
22015
|
+
<ng-content select="[inner-container]"></ng-content>
|
|
22016
|
+
</val-layout>
|
|
22017
|
+
</ion-content>
|
|
22018
|
+
<ng-content select="[outter-container]"></ng-content>
|
|
22019
|
+
`, isInline: true, styles: [".description-container{padding:0 16px}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: HeaderComponent, selector: "val-header", inputs: ["props"], outputs: ["onClick"] }, { kind: "component", type: LayoutComponent, selector: "val-layout" }, { kind: "component", type: DividerComponent, selector: "val-divider", inputs: ["props"] }, { kind: "component", type: LinkComponent, selector: "val-link", inputs: ["props"], outputs: ["onClick"] }, { kind: "component", type: ExpandableTextComponent, selector: "val-expandable-text", inputs: ["props"] }] }); }
|
|
22020
|
+
}
|
|
22021
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleComponent, decorators: [{
|
|
22022
|
+
type: Component,
|
|
22023
|
+
args: [{ selector: 'val-simple', standalone: true, imports: [
|
|
22024
|
+
NgStyle,
|
|
22025
|
+
IonHeader,
|
|
22026
|
+
IonToolbar,
|
|
22027
|
+
IonTitle,
|
|
22028
|
+
IonContent,
|
|
22029
|
+
HeaderComponent,
|
|
22030
|
+
LayoutComponent,
|
|
22031
|
+
DividerComponent,
|
|
22032
|
+
LinkComponent,
|
|
22033
|
+
ExpandableTextComponent,
|
|
22034
|
+
], template: `
|
|
22035
|
+
<val-header [props]="props.header" />
|
|
22036
|
+
|
|
22037
|
+
<ion-content
|
|
22038
|
+
[fullscreen]="true"
|
|
22039
|
+
[ngStyle]="{
|
|
22040
|
+
'--background': theme.IsDark ? 'var(--ion-background-color)' : props.background,
|
|
22041
|
+
}"
|
|
22042
|
+
[scrollEvents]="true"
|
|
22043
|
+
(ionScroll)="onScrollHandler($event)"
|
|
22044
|
+
>
|
|
22045
|
+
<ion-header collapse="condense">
|
|
22046
|
+
<ion-toolbar style="--background: transparent;">
|
|
22047
|
+
<ion-title style="padding: 0;" size="large">{{ props.pageTitle }}</ion-title>
|
|
22048
|
+
</ion-toolbar>
|
|
22049
|
+
</ion-header>
|
|
22050
|
+
@if (props.pageDescription) {
|
|
22051
|
+
<div class="description-container">
|
|
22052
|
+
<val-expandable-text
|
|
22053
|
+
[props]="{
|
|
22054
|
+
limit: 180,
|
|
22055
|
+
content: props.pageDescription,
|
|
22056
|
+
color: 'primary',
|
|
22057
|
+
expandText: 'más',
|
|
22058
|
+
}"
|
|
22059
|
+
/>
|
|
22060
|
+
</div>
|
|
22061
|
+
}
|
|
22062
|
+
@if (props.link) {
|
|
22063
|
+
<val-link [props]="props.link" (onClick)="onClickHandler($event)"></val-link>
|
|
22064
|
+
}
|
|
22065
|
+
@if (props.withDivider) {
|
|
22066
|
+
<val-divider [props]="{ fill: 'solid', size: 'medium', color: 'dark' }" />
|
|
22067
|
+
}
|
|
22068
|
+
<val-layout>
|
|
22069
|
+
<ng-content select="[inner-container]"></ng-content>
|
|
22070
|
+
</val-layout>
|
|
22071
|
+
</ion-content>
|
|
22072
|
+
<ng-content select="[outter-container]"></ng-content>
|
|
22073
|
+
`, styles: [".description-container{padding:0 16px}\n"] }]
|
|
22074
|
+
}], propDecorators: { props: [{
|
|
22075
|
+
type: Input
|
|
22076
|
+
}], onClick: [{
|
|
22077
|
+
type: Output
|
|
22078
|
+
}], onScrollEvent: [{
|
|
22079
|
+
type: Output
|
|
22080
|
+
}] } });
|
|
22081
|
+
|
|
22082
|
+
/**
|
|
22083
|
+
* val-page-template
|
|
22084
|
+
*
|
|
22085
|
+
* A page template component with title, expandable description,
|
|
22086
|
+
* content projection, and optional back navigation button.
|
|
22087
|
+
*
|
|
22088
|
+
* @example
|
|
22089
|
+
* <val-page-template
|
|
22090
|
+
* [props]="{
|
|
22091
|
+
* pageTitle: 'Getting Started',
|
|
22092
|
+
* pageDescription: 'Learn how to use our components...',
|
|
22093
|
+
* showBackButton: true
|
|
22094
|
+
* }"
|
|
22095
|
+
* >
|
|
22096
|
+
* <div extra-description>
|
|
22097
|
+
* <p>Additional info here</p>
|
|
22098
|
+
* </div>
|
|
22099
|
+
*
|
|
22100
|
+
* <!-- Main content -->
|
|
22101
|
+
* <my-content></my-content>
|
|
22102
|
+
*
|
|
22103
|
+
* <div extra-footer>
|
|
22104
|
+
* <p>Footer content</p>
|
|
22105
|
+
* </div>
|
|
22106
|
+
* </val-page-template>
|
|
22107
|
+
*
|
|
22108
|
+
* @input props - Page template configuration
|
|
22109
|
+
* @output onBack - Emits when back button is clicked
|
|
22110
|
+
*/
|
|
22111
|
+
class PageTemplateComponent {
|
|
22112
|
+
constructor() {
|
|
22113
|
+
this.nav = inject(NavController);
|
|
22114
|
+
/**
|
|
22115
|
+
* Page template configuration.
|
|
22116
|
+
*/
|
|
22117
|
+
this.props = {};
|
|
22118
|
+
/**
|
|
22119
|
+
* Emits when the back button is clicked.
|
|
22120
|
+
*/
|
|
22121
|
+
this.onBack = new EventEmitter();
|
|
22122
|
+
}
|
|
22123
|
+
/**
|
|
22124
|
+
* Handles back navigation.
|
|
22125
|
+
*/
|
|
22126
|
+
handleBack() {
|
|
22127
|
+
this.onBack.emit();
|
|
22128
|
+
this.nav.back();
|
|
22129
|
+
}
|
|
22130
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageTemplateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
22131
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PageTemplateComponent, isStandalone: true, selector: "val-page-template", inputs: { props: "props" }, outputs: { onBack: "onBack" }, ngImport: i0, template: `
|
|
22132
|
+
@if (props.pageTitle) {
|
|
22133
|
+
<ion-header [class.ion-no-border]="true">
|
|
22134
|
+
<ion-toolbar style="--background: transparent;">
|
|
22135
|
+
<ion-title class="page-title" size="large" style="--padding-start: 0; --padding-end: 0;">{{ props.pageTitle }}</ion-title>
|
|
22136
|
+
</ion-toolbar>
|
|
22137
|
+
</ion-header>
|
|
22138
|
+
}
|
|
22139
|
+
<ion-grid>
|
|
22140
|
+
<ion-row class="ion-justify-content-center description-row">
|
|
22141
|
+
<ion-col size="12" size-md="10" size-lg="8">
|
|
22142
|
+
@if (props.pageDescription) {
|
|
22143
|
+
<div class="description-container">
|
|
22144
|
+
<val-expandable-text
|
|
22145
|
+
[props]="{
|
|
22146
|
+
limit: props.descriptionLimit || 180,
|
|
21054
22147
|
content: props.pageDescription,
|
|
21055
22148
|
color: props.descriptionColor || 'dark',
|
|
21056
22149
|
}"
|
|
@@ -21104,7 +22197,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
21104
22197
|
@if (props.pageTitle) {
|
|
21105
22198
|
<ion-header [class.ion-no-border]="true">
|
|
21106
22199
|
<ion-toolbar style="--background: transparent;">
|
|
21107
|
-
<ion-title class="page-title" size="large">{{ props.pageTitle }}</ion-title>
|
|
22200
|
+
<ion-title class="page-title" size="large" style="--padding-start: 0; --padding-end: 0;">{{ props.pageTitle }}</ion-title>
|
|
21108
22201
|
</ion-toolbar>
|
|
21109
22202
|
</ion-header>
|
|
21110
22203
|
}
|
|
@@ -27946,913 +29039,2337 @@ class AuthService {
|
|
|
27946
29039
|
}
|
|
27947
29040
|
}
|
|
27948
29041
|
/**
|
|
27949
|
-
* Elimina el dispositivo del backend y borra el token FCM.
|
|
29042
|
+
* Elimina el dispositivo del backend y borra el token FCM.
|
|
29043
|
+
*/
|
|
29044
|
+
async unregisterDevice() {
|
|
29045
|
+
if (!this.config.enableDeviceRegistration || !this.messagingService) {
|
|
29046
|
+
return;
|
|
29047
|
+
}
|
|
29048
|
+
try {
|
|
29049
|
+
const token = this.messagingService.currentToken;
|
|
29050
|
+
if (token) {
|
|
29051
|
+
// Delete from backend (fire and forget)
|
|
29052
|
+
this.http.request('DELETE', `${this.config.apiUrl}/v2/users/me/devices/by-token`, {
|
|
29053
|
+
body: { token }
|
|
29054
|
+
}).pipe(catchError(() => of(null))).subscribe();
|
|
29055
|
+
// Delete from FCM
|
|
29056
|
+
await this.messagingService.deleteToken();
|
|
29057
|
+
console.log('[ValtechAuth] Device unregistered');
|
|
29058
|
+
}
|
|
29059
|
+
}
|
|
29060
|
+
catch {
|
|
29061
|
+
// Ignorar errores en cleanup
|
|
29062
|
+
}
|
|
29063
|
+
}
|
|
29064
|
+
/**
|
|
29065
|
+
* Detecta información de la plataforma del dispositivo.
|
|
29066
|
+
*/
|
|
29067
|
+
detectPlatformInfo() {
|
|
29068
|
+
const ua = navigator.userAgent;
|
|
29069
|
+
// Detectar navegador
|
|
29070
|
+
let browser = 'Unknown';
|
|
29071
|
+
if (ua.includes('Firefox')) {
|
|
29072
|
+
browser = 'Firefox';
|
|
29073
|
+
}
|
|
29074
|
+
else if (ua.includes('Edg')) {
|
|
29075
|
+
browser = 'Edge';
|
|
29076
|
+
}
|
|
29077
|
+
else if (ua.includes('Chrome')) {
|
|
29078
|
+
browser = 'Chrome';
|
|
29079
|
+
}
|
|
29080
|
+
else if (ua.includes('Safari')) {
|
|
29081
|
+
browser = 'Safari';
|
|
29082
|
+
}
|
|
29083
|
+
// Detectar OS
|
|
29084
|
+
let os = 'Unknown';
|
|
29085
|
+
if (ua.includes('Windows')) {
|
|
29086
|
+
os = 'Windows';
|
|
29087
|
+
}
|
|
29088
|
+
else if (ua.includes('Mac OS')) {
|
|
29089
|
+
os = 'macOS';
|
|
29090
|
+
}
|
|
29091
|
+
else if (ua.includes('Linux')) {
|
|
29092
|
+
os = 'Linux';
|
|
29093
|
+
}
|
|
29094
|
+
else if (ua.includes('Android')) {
|
|
29095
|
+
os = 'Android';
|
|
29096
|
+
}
|
|
29097
|
+
else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) {
|
|
29098
|
+
os = 'iOS';
|
|
29099
|
+
}
|
|
29100
|
+
return { platform: 'web', browser, os };
|
|
29101
|
+
}
|
|
29102
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$8.HttpClient }, { token: i1$1.Router }, { token: AuthStateService }, { token: TokenService }, { token: AuthStorageService }, { token: AuthSyncService }, { token: FirebaseService }, { token: OAuthService }, { token: MessagingService, optional: true }, { token: I18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
29103
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthService, providedIn: 'root' }); }
|
|
29104
|
+
}
|
|
29105
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthService, decorators: [{
|
|
29106
|
+
type: Injectable,
|
|
29107
|
+
args: [{ providedIn: 'root' }]
|
|
29108
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
29109
|
+
type: Inject,
|
|
29110
|
+
args: [VALTECH_AUTH_CONFIG]
|
|
29111
|
+
}] }, { type: i1$8.HttpClient }, { type: i1$1.Router }, { type: AuthStateService }, { type: TokenService }, { type: AuthStorageService }, { type: AuthSyncService }, { type: FirebaseService }, { type: OAuthService }, { type: MessagingService, decorators: [{
|
|
29112
|
+
type: Optional
|
|
29113
|
+
}] }, { type: I18nService, decorators: [{
|
|
29114
|
+
type: Optional
|
|
29115
|
+
}] }] });
|
|
29116
|
+
|
|
29117
|
+
/**
|
|
29118
|
+
* Notifications Service
|
|
29119
|
+
*
|
|
29120
|
+
* Servicio para leer notificaciones desde Firestore.
|
|
29121
|
+
* El backend escribe las notificaciones, el frontend solo las lee y actualiza estado.
|
|
29122
|
+
*
|
|
29123
|
+
* Se auto-inicializa cuando AuthService tiene un usuario autenticado.
|
|
29124
|
+
* También puede inicializarse manualmente con `initialize(userId)`.
|
|
29125
|
+
*/
|
|
29126
|
+
/**
|
|
29127
|
+
* Servicio para leer notificaciones desde Firestore.
|
|
29128
|
+
*
|
|
29129
|
+
* Se auto-inicializa cuando AuthService tiene un usuario autenticado.
|
|
29130
|
+
* No requiere llamar a `initialize()` manualmente si AuthService está configurado.
|
|
29131
|
+
*
|
|
29132
|
+
* @example
|
|
29133
|
+
* ```typescript
|
|
29134
|
+
* // Con AuthService configurado: auto-inicialización
|
|
29135
|
+
* // Solo inyectar y usar directamente
|
|
29136
|
+
* private notifications = inject(NotificationsService);
|
|
29137
|
+
*
|
|
29138
|
+
* notifications$ = this.notifications.getAll();
|
|
29139
|
+
* unreadCount$ = this.notifications.getUnreadCount();
|
|
29140
|
+
*
|
|
29141
|
+
* // Sin AuthService: inicialización manual
|
|
29142
|
+
* this.notifications.initialize(userId);
|
|
29143
|
+
* ```
|
|
29144
|
+
*/
|
|
29145
|
+
class NotificationsService {
|
|
29146
|
+
constructor(injector, collectionFactory) {
|
|
29147
|
+
this.injector = injector;
|
|
29148
|
+
this.collectionFactory = collectionFactory;
|
|
29149
|
+
this.collection = null;
|
|
29150
|
+
this.currentUserId = null;
|
|
29151
|
+
// Inyección opcional - AuthService puede no estar configurado
|
|
29152
|
+
this.authService = null;
|
|
29153
|
+
// Intentar obtener AuthService de forma lazy (puede no estar configurado)
|
|
29154
|
+
this.setupAutoInitialization();
|
|
29155
|
+
}
|
|
29156
|
+
/**
|
|
29157
|
+
* Configura auto-inicialización observando el estado de AuthService.
|
|
29158
|
+
* Se ejecuta en el contexto del injector para poder usar effect().
|
|
29159
|
+
*/
|
|
29160
|
+
setupAutoInitialization() {
|
|
29161
|
+
// Obtener AuthService de forma lazy
|
|
29162
|
+
try {
|
|
29163
|
+
this.authService = this.injector.get(AuthService, null);
|
|
29164
|
+
}
|
|
29165
|
+
catch {
|
|
29166
|
+
// AuthService no está configurado, no hay auto-inicialización
|
|
29167
|
+
return;
|
|
29168
|
+
}
|
|
29169
|
+
if (!this.authService) {
|
|
29170
|
+
return;
|
|
29171
|
+
}
|
|
29172
|
+
// Usar runInInjectionContext para poder crear el effect
|
|
29173
|
+
runInInjectionContext(this.injector, () => {
|
|
29174
|
+
effect(() => {
|
|
29175
|
+
const user = this.authService.user();
|
|
29176
|
+
if (user?.userId && this.currentUserId !== user.userId) {
|
|
29177
|
+
// Usuario autenticado: inicializar con su ID
|
|
29178
|
+
this.initialize(user.userId);
|
|
29179
|
+
}
|
|
29180
|
+
else if (!user && this.currentUserId) {
|
|
29181
|
+
// Logout: limpiar estado
|
|
29182
|
+
this.reset();
|
|
29183
|
+
}
|
|
29184
|
+
});
|
|
29185
|
+
});
|
|
29186
|
+
}
|
|
29187
|
+
/**
|
|
29188
|
+
* Inicializa el servicio para un usuario específico.
|
|
29189
|
+
*
|
|
29190
|
+
* NOTA: Se llama automáticamente si AuthService está configurado.
|
|
29191
|
+
* Solo usar manualmente si AuthService no está disponible o se necesita
|
|
29192
|
+
* un userId diferente al del usuario autenticado.
|
|
29193
|
+
*/
|
|
29194
|
+
initialize(userId) {
|
|
29195
|
+
if (!this.collectionFactory) {
|
|
29196
|
+
console.warn('[Notifications] FirestoreCollectionFactory not available. Ensure provideValtechFirebase() is configured.');
|
|
29197
|
+
return;
|
|
29198
|
+
}
|
|
29199
|
+
this.currentUserId = userId;
|
|
29200
|
+
// Path relativo - FirestoreService agrega automáticamente apps/{appId}/
|
|
29201
|
+
// NO agregar apps/ aquí para evitar doble prefijo
|
|
29202
|
+
const basePath = `users/${userId}/notifications`;
|
|
29203
|
+
this.collection = this.collectionFactory.create(basePath, { timestamps: true });
|
|
29204
|
+
console.log('[Notifications] Initialized with path:', basePath);
|
|
29205
|
+
}
|
|
29206
|
+
/**
|
|
29207
|
+
* Verifica si el servicio está inicializado.
|
|
29208
|
+
*/
|
|
29209
|
+
get isReady() {
|
|
29210
|
+
return this.collection !== null;
|
|
29211
|
+
}
|
|
29212
|
+
/**
|
|
29213
|
+
* Obtiene el ID del usuario actual.
|
|
29214
|
+
*/
|
|
29215
|
+
get userId() {
|
|
29216
|
+
return this.currentUserId;
|
|
29217
|
+
}
|
|
29218
|
+
// ===========================================================================
|
|
29219
|
+
// LECTURA
|
|
29220
|
+
// ===========================================================================
|
|
29221
|
+
/**
|
|
29222
|
+
* Obtiene todas las notificaciones ordenadas por fecha descendente.
|
|
29223
|
+
* Real-time: se actualiza automáticamente cuando cambian los datos.
|
|
29224
|
+
*/
|
|
29225
|
+
getAll() {
|
|
29226
|
+
if (!this.collection)
|
|
29227
|
+
return of([]);
|
|
29228
|
+
return this.collection.watchAll({
|
|
29229
|
+
orderBy: [{ field: 'createdAt', direction: 'desc' }],
|
|
29230
|
+
});
|
|
29231
|
+
}
|
|
29232
|
+
/**
|
|
29233
|
+
* Obtiene solo notificaciones no leídas.
|
|
29234
|
+
*/
|
|
29235
|
+
getUnread() {
|
|
29236
|
+
return this.getAll().pipe(map$1(notifications => notifications.filter(n => !n.isRead)));
|
|
29237
|
+
}
|
|
29238
|
+
/**
|
|
29239
|
+
* Cuenta notificaciones no leídas.
|
|
29240
|
+
* Útil para badges en UI.
|
|
29241
|
+
*/
|
|
29242
|
+
getUnreadCount() {
|
|
29243
|
+
return this.getUnread().pipe(map$1(n => n.length));
|
|
29244
|
+
}
|
|
29245
|
+
/**
|
|
29246
|
+
* Obtiene una notificación por ID.
|
|
29247
|
+
*/
|
|
29248
|
+
async getById(notificationId) {
|
|
29249
|
+
if (!this.collection)
|
|
29250
|
+
return null;
|
|
29251
|
+
return this.collection.getById(notificationId);
|
|
29252
|
+
}
|
|
29253
|
+
// ===========================================================================
|
|
29254
|
+
// ACCIONES
|
|
29255
|
+
// ===========================================================================
|
|
29256
|
+
/**
|
|
29257
|
+
* Marca una notificación como leída.
|
|
29258
|
+
*/
|
|
29259
|
+
async markAsRead(notificationId) {
|
|
29260
|
+
if (!this.collection)
|
|
29261
|
+
return;
|
|
29262
|
+
await this.collection.update(notificationId, { isRead: true });
|
|
29263
|
+
}
|
|
29264
|
+
/**
|
|
29265
|
+
* Marca todas las notificaciones como leídas.
|
|
27950
29266
|
*/
|
|
27951
|
-
async
|
|
27952
|
-
if (!this.
|
|
29267
|
+
async markAllAsRead() {
|
|
29268
|
+
if (!this.collection)
|
|
27953
29269
|
return;
|
|
27954
|
-
|
|
27955
|
-
|
|
27956
|
-
|
|
27957
|
-
|
|
27958
|
-
// Delete from backend (fire and forget)
|
|
27959
|
-
this.http.request('DELETE', `${this.config.apiUrl}/v2/users/me/devices/by-token`, {
|
|
27960
|
-
body: { token }
|
|
27961
|
-
}).pipe(catchError(() => of(null))).subscribe();
|
|
27962
|
-
// Delete from FCM
|
|
27963
|
-
await this.messagingService.deleteToken();
|
|
27964
|
-
console.log('[ValtechAuth] Device unregistered');
|
|
27965
|
-
}
|
|
27966
|
-
}
|
|
27967
|
-
catch {
|
|
27968
|
-
// Ignorar errores en cleanup
|
|
27969
|
-
}
|
|
29270
|
+
const unread = await this.collection.query({
|
|
29271
|
+
where: [{ field: 'isRead', operator: '==', value: false }],
|
|
29272
|
+
});
|
|
29273
|
+
await Promise.all(unread.map(n => this.collection.update(n.id, { isRead: true })));
|
|
27970
29274
|
}
|
|
27971
29275
|
/**
|
|
27972
|
-
*
|
|
29276
|
+
* Elimina una notificación.
|
|
27973
29277
|
*/
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
|
|
27978
|
-
if (ua.includes('Firefox')) {
|
|
27979
|
-
browser = 'Firefox';
|
|
27980
|
-
}
|
|
27981
|
-
else if (ua.includes('Edg')) {
|
|
27982
|
-
browser = 'Edge';
|
|
27983
|
-
}
|
|
27984
|
-
else if (ua.includes('Chrome')) {
|
|
27985
|
-
browser = 'Chrome';
|
|
27986
|
-
}
|
|
27987
|
-
else if (ua.includes('Safari')) {
|
|
27988
|
-
browser = 'Safari';
|
|
27989
|
-
}
|
|
27990
|
-
// Detectar OS
|
|
27991
|
-
let os = 'Unknown';
|
|
27992
|
-
if (ua.includes('Windows')) {
|
|
27993
|
-
os = 'Windows';
|
|
27994
|
-
}
|
|
27995
|
-
else if (ua.includes('Mac OS')) {
|
|
27996
|
-
os = 'macOS';
|
|
27997
|
-
}
|
|
27998
|
-
else if (ua.includes('Linux')) {
|
|
27999
|
-
os = 'Linux';
|
|
28000
|
-
}
|
|
28001
|
-
else if (ua.includes('Android')) {
|
|
28002
|
-
os = 'Android';
|
|
28003
|
-
}
|
|
28004
|
-
else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) {
|
|
28005
|
-
os = 'iOS';
|
|
28006
|
-
}
|
|
28007
|
-
return { platform: 'web', browser, os };
|
|
29278
|
+
async delete(notificationId) {
|
|
29279
|
+
if (!this.collection)
|
|
29280
|
+
return;
|
|
29281
|
+
await this.collection.delete(notificationId);
|
|
28008
29282
|
}
|
|
28009
|
-
|
|
28010
|
-
|
|
29283
|
+
/**
|
|
29284
|
+
* Elimina todas las notificaciones del usuario.
|
|
29285
|
+
*/
|
|
29286
|
+
async deleteAll() {
|
|
29287
|
+
if (!this.collection)
|
|
29288
|
+
return;
|
|
29289
|
+
const all = await this.collection.getAll();
|
|
29290
|
+
await Promise.all(all.map(n => this.collection.delete(n.id)));
|
|
29291
|
+
}
|
|
29292
|
+
/**
|
|
29293
|
+
* Limpia el estado del servicio.
|
|
29294
|
+
* Útil para logout.
|
|
29295
|
+
*/
|
|
29296
|
+
reset() {
|
|
29297
|
+
this.collection = null;
|
|
29298
|
+
this.currentUserId = null;
|
|
29299
|
+
}
|
|
29300
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationsService, deps: [{ token: i0.Injector }, { token: FirestoreCollectionFactory, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
29301
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationsService, providedIn: 'root' }); }
|
|
28011
29302
|
}
|
|
28012
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
29303
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationsService, decorators: [{
|
|
28013
29304
|
type: Injectable,
|
|
28014
29305
|
args: [{ providedIn: 'root' }]
|
|
28015
|
-
}], ctorParameters: () => [{ type:
|
|
28016
|
-
type: Inject,
|
|
28017
|
-
args: [VALTECH_AUTH_CONFIG]
|
|
28018
|
-
}] }, { type: i1$8.HttpClient }, { type: i1$1.Router }, { type: AuthStateService }, { type: TokenService }, { type: AuthStorageService }, { type: AuthSyncService }, { type: FirebaseService }, { type: OAuthService }, { type: MessagingService, decorators: [{
|
|
28019
|
-
type: Optional
|
|
28020
|
-
}] }, { type: I18nService, decorators: [{
|
|
29306
|
+
}], ctorParameters: () => [{ type: i0.Injector }, { type: FirestoreCollectionFactory, decorators: [{
|
|
28021
29307
|
type: Optional
|
|
28022
29308
|
}] }] });
|
|
28023
29309
|
|
|
28024
29310
|
/**
|
|
28025
|
-
*
|
|
29311
|
+
* Analytics Types
|
|
29312
|
+
*
|
|
29313
|
+
* Tipos e interfaces para el servicio de Firebase Analytics (GA4).
|
|
29314
|
+
*/
|
|
29315
|
+
|
|
29316
|
+
/**
|
|
29317
|
+
* Firebase Services
|
|
29318
|
+
*
|
|
29319
|
+
* Servicios reutilizables para integración con Firebase.
|
|
29320
|
+
*
|
|
29321
|
+
* @example
|
|
29322
|
+
* ```typescript
|
|
29323
|
+
* // En main.ts
|
|
29324
|
+
* import { provideValtechFirebase } from 'valtech-components';
|
|
29325
|
+
*
|
|
29326
|
+
* bootstrapApplication(AppComponent, {
|
|
29327
|
+
* providers: [
|
|
29328
|
+
* provideValtechFirebase({
|
|
29329
|
+
* firebase: environment.firebase,
|
|
29330
|
+
* persistence: true,
|
|
29331
|
+
* }),
|
|
29332
|
+
* ],
|
|
29333
|
+
* });
|
|
29334
|
+
*
|
|
29335
|
+
* // En componentes
|
|
29336
|
+
* import { FirebaseService, FirestoreService } from 'valtech-components';
|
|
29337
|
+
*
|
|
29338
|
+
* @Component({...})
|
|
29339
|
+
* export class MyComponent {
|
|
29340
|
+
* private firebase = inject(FirebaseService);
|
|
29341
|
+
* private firestore = inject(FirestoreService);
|
|
29342
|
+
* }
|
|
29343
|
+
* ```
|
|
29344
|
+
*/
|
|
29345
|
+
// Tipos
|
|
29346
|
+
|
|
29347
|
+
/**
|
|
29348
|
+
* Guard que verifica si el usuario está autenticado.
|
|
29349
|
+
* Redirige a loginRoute si no está autenticado.
|
|
29350
|
+
*
|
|
29351
|
+
* @example
|
|
29352
|
+
* ```typescript
|
|
29353
|
+
* import { authGuard } from 'valtech-components';
|
|
29354
|
+
*
|
|
29355
|
+
* const routes: Routes = [
|
|
29356
|
+
* {
|
|
29357
|
+
* path: 'dashboard',
|
|
29358
|
+
* canActivate: [authGuard],
|
|
29359
|
+
* loadComponent: () => import('./dashboard.page'),
|
|
29360
|
+
* },
|
|
29361
|
+
* ];
|
|
29362
|
+
* ```
|
|
29363
|
+
*/
|
|
29364
|
+
const authGuard = () => {
|
|
29365
|
+
const authService = inject(AuthService);
|
|
29366
|
+
const router = inject(Router);
|
|
29367
|
+
const config = inject(VALTECH_AUTH_CONFIG);
|
|
29368
|
+
if (authService.isAuthenticated()) {
|
|
29369
|
+
return true;
|
|
29370
|
+
}
|
|
29371
|
+
return router.createUrlTree([config.loginRoute]);
|
|
29372
|
+
};
|
|
29373
|
+
/**
|
|
29374
|
+
* Guard que verifica si el usuario NO está autenticado.
|
|
29375
|
+
* Redirige a homeRoute si ya está autenticado.
|
|
29376
|
+
* Útil para páginas de login/registro.
|
|
29377
|
+
*
|
|
29378
|
+
* @example
|
|
29379
|
+
* ```typescript
|
|
29380
|
+
* import { guestGuard } from 'valtech-components';
|
|
29381
|
+
*
|
|
29382
|
+
* const routes: Routes = [
|
|
29383
|
+
* {
|
|
29384
|
+
* path: 'login',
|
|
29385
|
+
* canActivate: [guestGuard],
|
|
29386
|
+
* loadComponent: () => import('./login.page'),
|
|
29387
|
+
* },
|
|
29388
|
+
* ];
|
|
29389
|
+
* ```
|
|
29390
|
+
*/
|
|
29391
|
+
const guestGuard = () => {
|
|
29392
|
+
const authService = inject(AuthService);
|
|
29393
|
+
const router = inject(Router);
|
|
29394
|
+
const config = inject(VALTECH_AUTH_CONFIG);
|
|
29395
|
+
if (!authService.isAuthenticated()) {
|
|
29396
|
+
return true;
|
|
29397
|
+
}
|
|
29398
|
+
return router.createUrlTree([config.homeRoute]);
|
|
29399
|
+
};
|
|
29400
|
+
/**
|
|
29401
|
+
* Factory para crear guard de permisos.
|
|
29402
|
+
* Verifica si el usuario tiene el permiso especificado.
|
|
29403
|
+
*
|
|
29404
|
+
* @param permissions - Permiso o lista de permisos requeridos (OR)
|
|
29405
|
+
* @returns Guard function
|
|
29406
|
+
*
|
|
29407
|
+
* @example
|
|
29408
|
+
* ```typescript
|
|
29409
|
+
* import { authGuard, permissionGuard } from 'valtech-components';
|
|
29410
|
+
*
|
|
29411
|
+
* const routes: Routes = [
|
|
29412
|
+
* {
|
|
29413
|
+
* path: 'templates',
|
|
29414
|
+
* canActivate: [authGuard, permissionGuard('templates:read')],
|
|
29415
|
+
* loadComponent: () => import('./templates.page'),
|
|
29416
|
+
* },
|
|
29417
|
+
* {
|
|
29418
|
+
* path: 'admin',
|
|
29419
|
+
* canActivate: [authGuard, permissionGuard(['admin:*', 'super_admin'])],
|
|
29420
|
+
* loadComponent: () => import('./admin.page'),
|
|
29421
|
+
* },
|
|
29422
|
+
* ];
|
|
29423
|
+
* ```
|
|
29424
|
+
*/
|
|
29425
|
+
function permissionGuard(permissions) {
|
|
29426
|
+
return () => {
|
|
29427
|
+
const authService = inject(AuthService);
|
|
29428
|
+
const router = inject(Router);
|
|
29429
|
+
const config = inject(VALTECH_AUTH_CONFIG);
|
|
29430
|
+
const permArray = Array.isArray(permissions) ? permissions : [permissions];
|
|
29431
|
+
if (authService.hasAnyPermission(permArray)) {
|
|
29432
|
+
return true;
|
|
29433
|
+
}
|
|
29434
|
+
console.warn(`[ValtechAuth] Permiso denegado. Requerido: ${permArray.join(' o ')}`);
|
|
29435
|
+
return router.createUrlTree([config.unauthorizedRoute]);
|
|
29436
|
+
};
|
|
29437
|
+
}
|
|
29438
|
+
/**
|
|
29439
|
+
* Guard que lee permisos desde route.data.
|
|
29440
|
+
* Permite configurar permisos directamente en la definición de rutas.
|
|
29441
|
+
*
|
|
29442
|
+
* @example
|
|
29443
|
+
* ```typescript
|
|
29444
|
+
* import { authGuard, permissionGuardFromRoute } from 'valtech-components';
|
|
29445
|
+
*
|
|
29446
|
+
* const routes: Routes = [
|
|
29447
|
+
* {
|
|
29448
|
+
* path: 'admin/users',
|
|
29449
|
+
* canActivate: [authGuard, permissionGuardFromRoute],
|
|
29450
|
+
* data: {
|
|
29451
|
+
* permissions: ['users:read', 'users:manage'],
|
|
29452
|
+
* requireAll: false // true = AND, false = OR (default)
|
|
29453
|
+
* },
|
|
29454
|
+
* loadComponent: () => import('./users.page'),
|
|
29455
|
+
* },
|
|
29456
|
+
* ];
|
|
29457
|
+
* ```
|
|
29458
|
+
*/
|
|
29459
|
+
const permissionGuardFromRoute = (route) => {
|
|
29460
|
+
const authService = inject(AuthService);
|
|
29461
|
+
const router = inject(Router);
|
|
29462
|
+
const config = inject(VALTECH_AUTH_CONFIG);
|
|
29463
|
+
const permissions = route.data['permissions'];
|
|
29464
|
+
const requireAll = route.data['requireAll'];
|
|
29465
|
+
if (!permissions || permissions.length === 0) {
|
|
29466
|
+
return true;
|
|
29467
|
+
}
|
|
29468
|
+
const hasAccess = requireAll
|
|
29469
|
+
? authService.hasAllPermissions(permissions)
|
|
29470
|
+
: authService.hasAnyPermission(permissions);
|
|
29471
|
+
if (hasAccess) {
|
|
29472
|
+
return true;
|
|
29473
|
+
}
|
|
29474
|
+
console.warn(`[ValtechAuth] Permiso denegado. Requerido: ${permissions.join(requireAll ? ' y ' : ' o ')}`);
|
|
29475
|
+
return router.createUrlTree([config.unauthorizedRoute]);
|
|
29476
|
+
};
|
|
29477
|
+
/**
|
|
29478
|
+
* Guard que verifica si el usuario es super admin.
|
|
28026
29479
|
*
|
|
28027
|
-
*
|
|
28028
|
-
*
|
|
29480
|
+
* @example
|
|
29481
|
+
* ```typescript
|
|
29482
|
+
* import { authGuard, superAdminGuard } from 'valtech-components';
|
|
28029
29483
|
*
|
|
28030
|
-
*
|
|
28031
|
-
*
|
|
29484
|
+
* const routes: Routes = [
|
|
29485
|
+
* {
|
|
29486
|
+
* path: 'super-admin',
|
|
29487
|
+
* canActivate: [authGuard, superAdminGuard],
|
|
29488
|
+
* loadComponent: () => import('./super-admin.page'),
|
|
29489
|
+
* },
|
|
29490
|
+
* ];
|
|
29491
|
+
* ```
|
|
28032
29492
|
*/
|
|
29493
|
+
const superAdminGuard = () => {
|
|
29494
|
+
const authService = inject(AuthService);
|
|
29495
|
+
const router = inject(Router);
|
|
29496
|
+
const config = inject(VALTECH_AUTH_CONFIG);
|
|
29497
|
+
if (authService.isSuperAdmin()) {
|
|
29498
|
+
return true;
|
|
29499
|
+
}
|
|
29500
|
+
console.warn('[ValtechAuth] Acceso de super admin requerido');
|
|
29501
|
+
return router.createUrlTree([config.unauthorizedRoute]);
|
|
29502
|
+
};
|
|
28033
29503
|
/**
|
|
28034
|
-
*
|
|
29504
|
+
* Guard que verifica si el usuario tiene un rol específico.
|
|
28035
29505
|
*
|
|
28036
|
-
*
|
|
28037
|
-
*
|
|
29506
|
+
* @param roles - Rol o lista de roles requeridos (OR)
|
|
29507
|
+
* @returns Guard function
|
|
28038
29508
|
*
|
|
28039
29509
|
* @example
|
|
28040
29510
|
* ```typescript
|
|
28041
|
-
*
|
|
28042
|
-
* // Solo inyectar y usar directamente
|
|
28043
|
-
* private notifications = inject(NotificationsService);
|
|
29511
|
+
* import { authGuard, roleGuard } from 'valtech-components';
|
|
28044
29512
|
*
|
|
28045
|
-
*
|
|
28046
|
-
*
|
|
29513
|
+
* const routes: Routes = [
|
|
29514
|
+
* {
|
|
29515
|
+
* path: 'editor',
|
|
29516
|
+
* canActivate: [authGuard, roleGuard(['editor', 'admin'])],
|
|
29517
|
+
* loadComponent: () => import('./editor.page'),
|
|
29518
|
+
* },
|
|
29519
|
+
* ];
|
|
29520
|
+
* ```
|
|
29521
|
+
*/
|
|
29522
|
+
function roleGuard(roles) {
|
|
29523
|
+
return () => {
|
|
29524
|
+
const authService = inject(AuthService);
|
|
29525
|
+
const router = inject(Router);
|
|
29526
|
+
const config = inject(VALTECH_AUTH_CONFIG);
|
|
29527
|
+
const roleArray = Array.isArray(roles) ? roles : [roles];
|
|
29528
|
+
const hasRole = roleArray.some((role) => authService.hasRole(role));
|
|
29529
|
+
if (hasRole) {
|
|
29530
|
+
return true;
|
|
29531
|
+
}
|
|
29532
|
+
console.warn(`[ValtechAuth] Rol requerido: ${roleArray.join(' o ')}`);
|
|
29533
|
+
return router.createUrlTree([config.unauthorizedRoute]);
|
|
29534
|
+
};
|
|
29535
|
+
}
|
|
29536
|
+
|
|
29537
|
+
/**
|
|
29538
|
+
* Servicio para gestión de dispositivos del usuario.
|
|
29539
|
+
* Permite listar, aprobar, bloquear y eliminar dispositivos registrados.
|
|
28047
29540
|
*
|
|
28048
|
-
*
|
|
28049
|
-
*
|
|
29541
|
+
* @example
|
|
29542
|
+
* ```typescript
|
|
29543
|
+
* import { DeviceService } from 'valtech-components';
|
|
29544
|
+
*
|
|
29545
|
+
* @Component({...})
|
|
29546
|
+
* export class DevicesPage {
|
|
29547
|
+
* private deviceService = inject(DeviceService);
|
|
29548
|
+
*
|
|
29549
|
+
* devices = signal<DeviceInfo[]>([]);
|
|
29550
|
+
*
|
|
29551
|
+
* async ngOnInit() {
|
|
29552
|
+
* const devices = await firstValueFrom(this.deviceService.listDevices());
|
|
29553
|
+
* this.devices.set(devices);
|
|
29554
|
+
* }
|
|
29555
|
+
*
|
|
29556
|
+
* async blockDevice(deviceId: string) {
|
|
29557
|
+
* await firstValueFrom(this.deviceService.blockDevice(deviceId));
|
|
29558
|
+
* // Recargar lista
|
|
29559
|
+
* }
|
|
29560
|
+
* }
|
|
28050
29561
|
* ```
|
|
28051
29562
|
*/
|
|
28052
|
-
class
|
|
28053
|
-
constructor(
|
|
28054
|
-
this.
|
|
28055
|
-
this.
|
|
28056
|
-
|
|
28057
|
-
|
|
28058
|
-
|
|
28059
|
-
this.authService = null;
|
|
28060
|
-
// Intentar obtener AuthService de forma lazy (puede no estar configurado)
|
|
28061
|
-
this.setupAutoInitialization();
|
|
29563
|
+
class DeviceService {
|
|
29564
|
+
constructor(config, http) {
|
|
29565
|
+
this.config = config;
|
|
29566
|
+
this.http = http;
|
|
29567
|
+
}
|
|
29568
|
+
get baseUrl() {
|
|
29569
|
+
return `${this.config.apiUrl}/v2/users/me/devices`;
|
|
28062
29570
|
}
|
|
28063
29571
|
/**
|
|
28064
|
-
*
|
|
28065
|
-
* Se ejecuta en el contexto del injector para poder usar effect().
|
|
29572
|
+
* Lista todos los dispositivos registrados del usuario.
|
|
28066
29573
|
*/
|
|
28067
|
-
|
|
28068
|
-
|
|
28069
|
-
try {
|
|
28070
|
-
this.authService = this.injector.get(AuthService, null);
|
|
28071
|
-
}
|
|
28072
|
-
catch {
|
|
28073
|
-
// AuthService no está configurado, no hay auto-inicialización
|
|
28074
|
-
return;
|
|
28075
|
-
}
|
|
28076
|
-
if (!this.authService) {
|
|
28077
|
-
return;
|
|
28078
|
-
}
|
|
28079
|
-
// Usar runInInjectionContext para poder crear el effect
|
|
28080
|
-
runInInjectionContext(this.injector, () => {
|
|
28081
|
-
effect(() => {
|
|
28082
|
-
const user = this.authService.user();
|
|
28083
|
-
if (user?.userId && this.currentUserId !== user.userId) {
|
|
28084
|
-
// Usuario autenticado: inicializar con su ID
|
|
28085
|
-
this.initialize(user.userId);
|
|
28086
|
-
}
|
|
28087
|
-
else if (!user && this.currentUserId) {
|
|
28088
|
-
// Logout: limpiar estado
|
|
28089
|
-
this.reset();
|
|
28090
|
-
}
|
|
28091
|
-
});
|
|
28092
|
-
});
|
|
29574
|
+
listDevices() {
|
|
29575
|
+
return this.http.get(this.baseUrl).pipe(map$1(response => response.devices));
|
|
28093
29576
|
}
|
|
28094
29577
|
/**
|
|
28095
|
-
*
|
|
29578
|
+
* Obtiene información de un dispositivo específico.
|
|
29579
|
+
*/
|
|
29580
|
+
getDevice(deviceId) {
|
|
29581
|
+
return this.http.get(`${this.baseUrl}/${deviceId}`).pipe(map$1(response => response.device));
|
|
29582
|
+
}
|
|
29583
|
+
/**
|
|
29584
|
+
* Bloquea un dispositivo.
|
|
29585
|
+
* Revoca todas las sesiones activas de ese dispositivo.
|
|
29586
|
+
*/
|
|
29587
|
+
blockDevice(deviceId) {
|
|
29588
|
+
return this.http.post(`${this.baseUrl}/${deviceId}/block`, {});
|
|
29589
|
+
}
|
|
29590
|
+
/**
|
|
29591
|
+
* Aprueba un dispositivo pendiente.
|
|
29592
|
+
* Cambia el estado de pending_approval a active.
|
|
29593
|
+
*/
|
|
29594
|
+
approveDevice(deviceId) {
|
|
29595
|
+
return this.http.post(`${this.baseUrl}/${deviceId}/approve`, {});
|
|
29596
|
+
}
|
|
29597
|
+
/**
|
|
29598
|
+
* Elimina un dispositivo registrado.
|
|
29599
|
+
*/
|
|
29600
|
+
deleteDevice(deviceId) {
|
|
29601
|
+
return this.http.delete(`${this.baseUrl}/${deviceId}`);
|
|
29602
|
+
}
|
|
29603
|
+
/**
|
|
29604
|
+
* Valida un token de acción SIN ejecutarlo.
|
|
29605
|
+
* Útil para mostrar confirmación al usuario antes de ejecutar.
|
|
29606
|
+
* Este endpoint NO requiere autenticación.
|
|
28096
29607
|
*
|
|
28097
|
-
*
|
|
28098
|
-
*
|
|
28099
|
-
*
|
|
29608
|
+
* @param token Token JWT de acción
|
|
29609
|
+
* @returns Información del token si es válido
|
|
29610
|
+
*
|
|
29611
|
+
* @example
|
|
29612
|
+
* ```typescript
|
|
29613
|
+
* const token = this.route.snapshot.queryParams['token'];
|
|
29614
|
+
* if (token) {
|
|
29615
|
+
* const validation = await firstValueFrom(this.deviceService.validateAction(token));
|
|
29616
|
+
* if (validation.valid) {
|
|
29617
|
+
* // Mostrar confirmación al usuario
|
|
29618
|
+
* console.log(`Acción: ${validation.actionType}`);
|
|
29619
|
+
* }
|
|
29620
|
+
* }
|
|
29621
|
+
* ```
|
|
28100
29622
|
*/
|
|
28101
|
-
|
|
28102
|
-
|
|
28103
|
-
|
|
28104
|
-
|
|
28105
|
-
|
|
28106
|
-
|
|
28107
|
-
|
|
28108
|
-
|
|
28109
|
-
|
|
28110
|
-
|
|
28111
|
-
|
|
29623
|
+
validateAction(token) {
|
|
29624
|
+
return this.http.post(`${this.config.apiUrl}/v2/actions/validate`, { token });
|
|
29625
|
+
}
|
|
29626
|
+
/**
|
|
29627
|
+
* Ejecuta una acción de dispositivo desde un token de email.
|
|
29628
|
+
* Este endpoint NO requiere autenticación.
|
|
29629
|
+
* El token viene en la URL del email de alerta de nuevo inicio de sesión.
|
|
29630
|
+
*
|
|
29631
|
+
* @param token Token JWT de acción (24h, un solo uso)
|
|
29632
|
+
*
|
|
29633
|
+
* @example
|
|
29634
|
+
* ```typescript
|
|
29635
|
+
* // En la página de dispositivos, al detectar ?token=xxx en la URL:
|
|
29636
|
+
* const token = this.route.snapshot.queryParams['token'];
|
|
29637
|
+
* if (token) {
|
|
29638
|
+
* const result = await firstValueFrom(this.deviceService.executeAction(token));
|
|
29639
|
+
* if (result.success) {
|
|
29640
|
+
* console.log(`Dispositivo bloqueado, ${result.sessionsRevoked} sesiones cerradas`);
|
|
29641
|
+
* }
|
|
29642
|
+
* }
|
|
29643
|
+
* ```
|
|
29644
|
+
*/
|
|
29645
|
+
executeAction(token) {
|
|
29646
|
+
// Usa el endpoint unificado de acciones
|
|
29647
|
+
return this.http.post(`${this.config.apiUrl}/v2/actions/execute`, { token }).pipe(map$1(response => ({
|
|
29648
|
+
operationId: response.operationId,
|
|
29649
|
+
success: response.success,
|
|
29650
|
+
message: response.message,
|
|
29651
|
+
action: response.data?.['action'] || 'refuse',
|
|
29652
|
+
deviceId: response.data?.['deviceId'] || '',
|
|
29653
|
+
sessionsRevoked: response.data?.['sessionsRevoked'],
|
|
29654
|
+
})));
|
|
29655
|
+
}
|
|
29656
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeviceService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$8.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
29657
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeviceService, providedIn: 'root' }); }
|
|
29658
|
+
}
|
|
29659
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeviceService, decorators: [{
|
|
29660
|
+
type: Injectable,
|
|
29661
|
+
args: [{ providedIn: 'root' }]
|
|
29662
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
29663
|
+
type: Inject,
|
|
29664
|
+
args: [VALTECH_AUTH_CONFIG]
|
|
29665
|
+
}] }, { type: i1$8.HttpClient }] });
|
|
29666
|
+
|
|
29667
|
+
/**
|
|
29668
|
+
* Servicio para gestión de sesiones activas del usuario.
|
|
29669
|
+
* Permite listar y revocar sesiones.
|
|
29670
|
+
*
|
|
29671
|
+
* @example
|
|
29672
|
+
* ```typescript
|
|
29673
|
+
* import { SessionService } from 'valtech-components';
|
|
29674
|
+
*
|
|
29675
|
+
* @Component({...})
|
|
29676
|
+
* export class SessionsPage {
|
|
29677
|
+
* private sessionService = inject(SessionService);
|
|
29678
|
+
*
|
|
29679
|
+
* sessions = signal<SessionInfo[]>([]);
|
|
29680
|
+
*
|
|
29681
|
+
* async ngOnInit() {
|
|
29682
|
+
* const sessions = await firstValueFrom(this.sessionService.listSessions());
|
|
29683
|
+
* this.sessions.set(sessions);
|
|
29684
|
+
* }
|
|
29685
|
+
*
|
|
29686
|
+
* async revokeSession(sessionId: string) {
|
|
29687
|
+
* await firstValueFrom(this.sessionService.revokeSession(sessionId));
|
|
29688
|
+
* // Recargar lista
|
|
29689
|
+
* }
|
|
29690
|
+
*
|
|
29691
|
+
* async revokeAllOthers() {
|
|
29692
|
+
* const result = await firstValueFrom(this.sessionService.revokeAllSessions());
|
|
29693
|
+
* console.log(`${result.sessionsRevoked} sesiones cerradas`);
|
|
29694
|
+
* }
|
|
29695
|
+
* }
|
|
29696
|
+
* ```
|
|
29697
|
+
*/
|
|
29698
|
+
class SessionService {
|
|
29699
|
+
constructor(config, http) {
|
|
29700
|
+
this.config = config;
|
|
29701
|
+
this.http = http;
|
|
28112
29702
|
}
|
|
28113
|
-
|
|
28114
|
-
|
|
28115
|
-
*/
|
|
28116
|
-
get isReady() {
|
|
28117
|
-
return this.collection !== null;
|
|
29703
|
+
get baseUrl() {
|
|
29704
|
+
return `${this.config.apiUrl}/v2/users/me/sessions`;
|
|
28118
29705
|
}
|
|
28119
29706
|
/**
|
|
28120
|
-
*
|
|
29707
|
+
* Lista todas las sesiones activas del usuario.
|
|
29708
|
+
* La sesión actual está marcada con isCurrent=true.
|
|
28121
29709
|
*/
|
|
28122
|
-
|
|
28123
|
-
return this.
|
|
29710
|
+
listSessions() {
|
|
29711
|
+
return this.http.get(this.baseUrl).pipe(map$1(response => response.sessions));
|
|
28124
29712
|
}
|
|
28125
|
-
// ===========================================================================
|
|
28126
|
-
// LECTURA
|
|
28127
|
-
// ===========================================================================
|
|
28128
29713
|
/**
|
|
28129
|
-
*
|
|
28130
|
-
*
|
|
29714
|
+
* Revoca una sesión específica.
|
|
29715
|
+
* Fuerza el cierre de sesión en ese dispositivo/navegador.
|
|
28131
29716
|
*/
|
|
28132
|
-
|
|
28133
|
-
|
|
28134
|
-
return of([]);
|
|
28135
|
-
return this.collection.watchAll({
|
|
28136
|
-
orderBy: [{ field: 'createdAt', direction: 'desc' }],
|
|
28137
|
-
});
|
|
29717
|
+
revokeSession(sessionId) {
|
|
29718
|
+
return this.http.delete(`${this.baseUrl}/${sessionId}`);
|
|
28138
29719
|
}
|
|
28139
29720
|
/**
|
|
28140
|
-
*
|
|
29721
|
+
* Revoca todas las sesiones excepto la actual.
|
|
29722
|
+
* Útil para "cerrar sesión en todos los dispositivos".
|
|
29723
|
+
*
|
|
29724
|
+
* @returns Número de sesiones revocadas
|
|
28141
29725
|
*/
|
|
28142
|
-
|
|
28143
|
-
return this.
|
|
29726
|
+
revokeAllSessions() {
|
|
29727
|
+
return this.http.delete(this.baseUrl);
|
|
28144
29728
|
}
|
|
28145
|
-
|
|
28146
|
-
|
|
28147
|
-
|
|
28148
|
-
|
|
28149
|
-
|
|
28150
|
-
|
|
29729
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SessionService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$8.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
29730
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SessionService, providedIn: 'root' }); }
|
|
29731
|
+
}
|
|
29732
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SessionService, decorators: [{
|
|
29733
|
+
type: Injectable,
|
|
29734
|
+
args: [{ providedIn: 'root' }]
|
|
29735
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
29736
|
+
type: Inject,
|
|
29737
|
+
args: [VALTECH_AUTH_CONFIG]
|
|
29738
|
+
}] }, { type: i1$8.HttpClient }] });
|
|
29739
|
+
|
|
29740
|
+
/**
|
|
29741
|
+
* Componente de callback para OAuth.
|
|
29742
|
+
*
|
|
29743
|
+
* Este componente procesa la respuesta del servidor OAuth y envía
|
|
29744
|
+
* los tokens a la ventana padre via postMessage.
|
|
29745
|
+
*
|
|
29746
|
+
* Debe agregarse a las rutas de la aplicación:
|
|
29747
|
+
* ```typescript
|
|
29748
|
+
* // app.routes.ts
|
|
29749
|
+
* import { OAuthCallbackComponent } from 'valtech-components';
|
|
29750
|
+
*
|
|
29751
|
+
* export const routes: Routes = [
|
|
29752
|
+
* { path: 'auth/oauth/callback', component: OAuthCallbackComponent },
|
|
29753
|
+
* // ... otras rutas
|
|
29754
|
+
* ];
|
|
29755
|
+
* ```
|
|
29756
|
+
*
|
|
29757
|
+
* El backend redirige a esta ruta con los tokens en query params:
|
|
29758
|
+
* `/auth/oauth/callback?access_token=xxx&refresh_token=xxx&expires_in=900`
|
|
29759
|
+
*
|
|
29760
|
+
* O con error:
|
|
29761
|
+
* `/auth/oauth/callback?error=INVALID_CODE&error_description=...`
|
|
29762
|
+
*/
|
|
29763
|
+
class OAuthCallbackComponent {
|
|
29764
|
+
constructor() {
|
|
29765
|
+
this.message = 'Procesando autenticación...';
|
|
28151
29766
|
}
|
|
28152
|
-
|
|
28153
|
-
|
|
28154
|
-
*/
|
|
28155
|
-
async getById(notificationId) {
|
|
28156
|
-
if (!this.collection)
|
|
28157
|
-
return null;
|
|
28158
|
-
return this.collection.getById(notificationId);
|
|
29767
|
+
ngOnInit() {
|
|
29768
|
+
this.processCallback();
|
|
28159
29769
|
}
|
|
28160
|
-
|
|
28161
|
-
|
|
28162
|
-
|
|
28163
|
-
|
|
28164
|
-
|
|
28165
|
-
|
|
28166
|
-
|
|
28167
|
-
|
|
29770
|
+
processCallback() {
|
|
29771
|
+
const params = new URLSearchParams(window.location.search);
|
|
29772
|
+
// Verificar si hay error
|
|
29773
|
+
const error = params.get('error');
|
|
29774
|
+
if (error) {
|
|
29775
|
+
this.sendToParent({
|
|
29776
|
+
type: 'oauth-callback',
|
|
29777
|
+
error: {
|
|
29778
|
+
code: error,
|
|
29779
|
+
message: params.get('error_description') || 'Error de autenticación',
|
|
29780
|
+
},
|
|
29781
|
+
});
|
|
29782
|
+
this.message = 'Error de autenticación';
|
|
29783
|
+
this.closeAfterDelay();
|
|
28168
29784
|
return;
|
|
28169
|
-
|
|
28170
|
-
|
|
28171
|
-
|
|
28172
|
-
|
|
28173
|
-
|
|
28174
|
-
|
|
28175
|
-
if (!
|
|
29785
|
+
}
|
|
29786
|
+
// Extraer tokens
|
|
29787
|
+
const accessToken = params.get('access_token');
|
|
29788
|
+
const refreshToken = params.get('refresh_token');
|
|
29789
|
+
const expiresIn = params.get('expires_in');
|
|
29790
|
+
const firebaseToken = params.get('firebase_token');
|
|
29791
|
+
if (!accessToken || !refreshToken) {
|
|
29792
|
+
this.sendToParent({
|
|
29793
|
+
type: 'oauth-callback',
|
|
29794
|
+
error: {
|
|
29795
|
+
code: 'MISSING_TOKENS',
|
|
29796
|
+
message: 'No se recibieron los tokens de autenticación',
|
|
29797
|
+
},
|
|
29798
|
+
});
|
|
29799
|
+
this.message = 'Error: tokens no recibidos';
|
|
29800
|
+
this.closeAfterDelay();
|
|
28176
29801
|
return;
|
|
28177
|
-
|
|
28178
|
-
|
|
29802
|
+
}
|
|
29803
|
+
// Extraer roles y permisos (pueden venir como JSON en base64)
|
|
29804
|
+
let roles;
|
|
29805
|
+
let permissions;
|
|
29806
|
+
const rolesParam = params.get('roles');
|
|
29807
|
+
const permissionsParam = params.get('permissions');
|
|
29808
|
+
if (rolesParam) {
|
|
29809
|
+
try {
|
|
29810
|
+
roles = JSON.parse(atob(rolesParam));
|
|
29811
|
+
}
|
|
29812
|
+
catch {
|
|
29813
|
+
roles = rolesParam.split(',');
|
|
29814
|
+
}
|
|
29815
|
+
}
|
|
29816
|
+
if (permissionsParam) {
|
|
29817
|
+
try {
|
|
29818
|
+
permissions = JSON.parse(atob(permissionsParam));
|
|
29819
|
+
}
|
|
29820
|
+
catch {
|
|
29821
|
+
permissions = permissionsParam.split(',');
|
|
29822
|
+
}
|
|
29823
|
+
}
|
|
29824
|
+
// Enviar tokens a la ventana padre
|
|
29825
|
+
const result = {
|
|
29826
|
+
accessToken,
|
|
29827
|
+
refreshToken,
|
|
29828
|
+
expiresIn: expiresIn ? parseInt(expiresIn, 10) : 900,
|
|
29829
|
+
firebaseToken: firebaseToken || undefined,
|
|
29830
|
+
roles,
|
|
29831
|
+
permissions,
|
|
29832
|
+
isNewUser: params.get('is_new_user') === 'true',
|
|
29833
|
+
linked: params.get('linked') === 'true',
|
|
29834
|
+
};
|
|
29835
|
+
this.sendToParent({
|
|
29836
|
+
type: 'oauth-callback',
|
|
29837
|
+
tokens: result,
|
|
28179
29838
|
});
|
|
28180
|
-
|
|
28181
|
-
|
|
28182
|
-
/**
|
|
28183
|
-
* Elimina una notificación.
|
|
28184
|
-
*/
|
|
28185
|
-
async delete(notificationId) {
|
|
28186
|
-
if (!this.collection)
|
|
28187
|
-
return;
|
|
28188
|
-
await this.collection.delete(notificationId);
|
|
29839
|
+
this.message = 'Autenticación exitosa';
|
|
29840
|
+
this.closeAfterDelay();
|
|
28189
29841
|
}
|
|
28190
|
-
|
|
28191
|
-
|
|
28192
|
-
|
|
28193
|
-
|
|
28194
|
-
|
|
28195
|
-
|
|
28196
|
-
|
|
28197
|
-
|
|
29842
|
+
sendToParent(data) {
|
|
29843
|
+
if (window.opener) {
|
|
29844
|
+
// Enviar al opener (ventana que abrió el popup)
|
|
29845
|
+
window.opener.postMessage(data, window.location.origin);
|
|
29846
|
+
}
|
|
29847
|
+
else if (window.parent !== window) {
|
|
29848
|
+
// Enviar al parent (si estamos en iframe)
|
|
29849
|
+
window.parent.postMessage(data, window.location.origin);
|
|
29850
|
+
}
|
|
28198
29851
|
}
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
29852
|
+
closeAfterDelay() {
|
|
29853
|
+
// Dar tiempo para que el mensaje se envíe antes de cerrar
|
|
29854
|
+
setTimeout(() => {
|
|
29855
|
+
if (window.opener) {
|
|
29856
|
+
window.close();
|
|
29857
|
+
}
|
|
29858
|
+
}, 500);
|
|
28206
29859
|
}
|
|
28207
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
28208
|
-
static { this.ɵ
|
|
29860
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OAuthCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
29861
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: OAuthCallbackComponent, isStandalone: true, selector: "val-oauth-callback", ngImport: i0, template: `
|
|
29862
|
+
<div class="oauth-callback">
|
|
29863
|
+
<div class="oauth-callback__spinner"></div>
|
|
29864
|
+
<p class="oauth-callback__text">{{ message }}</p>
|
|
29865
|
+
</div>
|
|
29866
|
+
`, isInline: true, styles: [".oauth-callback{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.oauth-callback__spinner{width:40px;height:40px;border:3px solid #f3f3f3;border-top:3px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:16px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.oauth-callback__text{color:#666;font-size:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
|
|
28209
29867
|
}
|
|
28210
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
28211
|
-
type:
|
|
28212
|
-
args: [{
|
|
28213
|
-
|
|
28214
|
-
|
|
28215
|
-
|
|
28216
|
-
|
|
28217
|
-
|
|
28218
|
-
|
|
28219
|
-
*
|
|
28220
|
-
* Tipos e interfaces para el servicio de Firebase Analytics (GA4).
|
|
28221
|
-
*/
|
|
29868
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OAuthCallbackComponent, decorators: [{
|
|
29869
|
+
type: Component,
|
|
29870
|
+
args: [{ selector: 'val-oauth-callback', standalone: true, imports: [CommonModule], template: `
|
|
29871
|
+
<div class="oauth-callback">
|
|
29872
|
+
<div class="oauth-callback__spinner"></div>
|
|
29873
|
+
<p class="oauth-callback__text">{{ message }}</p>
|
|
29874
|
+
</div>
|
|
29875
|
+
`, styles: [".oauth-callback{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.oauth-callback__spinner{width:40px;height:40px;border:3px solid #f3f3f3;border-top:3px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:16px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.oauth-callback__text{color:#666;font-size:14px}\n"] }]
|
|
29876
|
+
}] });
|
|
28222
29877
|
|
|
28223
29878
|
/**
|
|
28224
|
-
*
|
|
29879
|
+
* Valtech Auth Service
|
|
28225
29880
|
*
|
|
28226
|
-
*
|
|
29881
|
+
* Servicio de autenticación reutilizable para aplicaciones Angular.
|
|
29882
|
+
* Proporciona autenticación con AuthV2, MFA, sincronización entre pestañas,
|
|
29883
|
+
* refresh proactivo de tokens, y registro automático de dispositivos para
|
|
29884
|
+
* push notifications.
|
|
28227
29885
|
*
|
|
28228
29886
|
* @example
|
|
28229
29887
|
* ```typescript
|
|
28230
29888
|
* // En main.ts
|
|
28231
|
-
* import {
|
|
29889
|
+
* import { bootstrapApplication } from '@angular/platform-browser';
|
|
29890
|
+
* import { provideValtechAuth, provideValtechFirebase } from 'valtech-components';
|
|
29891
|
+
* import { environment } from './environments/environment';
|
|
28232
29892
|
*
|
|
28233
29893
|
* bootstrapApplication(AppComponent, {
|
|
28234
29894
|
* providers: [
|
|
28235
|
-
* provideValtechFirebase(
|
|
28236
|
-
*
|
|
28237
|
-
*
|
|
29895
|
+
* provideValtechFirebase(environment.firebase),
|
|
29896
|
+
* provideValtechAuth({
|
|
29897
|
+
* apiUrl: environment.apiUrl,
|
|
29898
|
+
* enableFirebaseIntegration: true,
|
|
29899
|
+
* enableDeviceRegistration: true, // Auto-registra dispositivos para push
|
|
28238
29900
|
* }),
|
|
28239
29901
|
* ],
|
|
28240
29902
|
* });
|
|
28241
29903
|
*
|
|
29904
|
+
* // En app.routes.ts
|
|
29905
|
+
* import { authGuard, guestGuard, permissionGuard } from 'valtech-components';
|
|
29906
|
+
*
|
|
29907
|
+
* const routes: Routes = [
|
|
29908
|
+
* { path: 'login', canActivate: [guestGuard], loadComponent: () => import('./login.page') },
|
|
29909
|
+
* { path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('./dashboard.page') },
|
|
29910
|
+
* { path: 'admin', canActivate: [authGuard, permissionGuard('admin:*')], loadComponent: () => import('./admin.page') },
|
|
29911
|
+
* ];
|
|
29912
|
+
*
|
|
28242
29913
|
* // En componentes
|
|
28243
|
-
* import {
|
|
29914
|
+
* import { AuthService } from 'valtech-components';
|
|
28244
29915
|
*
|
|
28245
29916
|
* @Component({...})
|
|
28246
|
-
* export class
|
|
28247
|
-
* private
|
|
28248
|
-
* private firestore = inject(FirestoreService);
|
|
28249
|
-
* }
|
|
28250
|
-
* ```
|
|
28251
|
-
*/
|
|
28252
|
-
// Tipos
|
|
28253
|
-
|
|
28254
|
-
/**
|
|
28255
|
-
* Guard que verifica si el usuario está autenticado.
|
|
28256
|
-
* Redirige a loginRoute si no está autenticado.
|
|
29917
|
+
* export class LoginComponent {
|
|
29918
|
+
* private auth = inject(AuthService);
|
|
28257
29919
|
*
|
|
28258
|
-
*
|
|
28259
|
-
*
|
|
28260
|
-
*
|
|
29920
|
+
* async login() {
|
|
29921
|
+
* await firstValueFrom(this.auth.signin({ email, password }));
|
|
29922
|
+
* if (this.auth.mfaPending().required) {
|
|
29923
|
+
* // Mostrar UI de MFA
|
|
29924
|
+
* } else {
|
|
29925
|
+
* this.router.navigate(['/dashboard']);
|
|
29926
|
+
* }
|
|
29927
|
+
* }
|
|
28261
29928
|
*
|
|
28262
|
-
*
|
|
28263
|
-
* {
|
|
28264
|
-
*
|
|
28265
|
-
*
|
|
28266
|
-
*
|
|
28267
|
-
*
|
|
28268
|
-
*
|
|
28269
|
-
* ```
|
|
28270
|
-
*/
|
|
28271
|
-
const authGuard = () => {
|
|
28272
|
-
const authService = inject(AuthService);
|
|
28273
|
-
const router = inject(Router);
|
|
28274
|
-
const config = inject(VALTECH_AUTH_CONFIG);
|
|
28275
|
-
if (authService.isAuthenticated()) {
|
|
28276
|
-
return true;
|
|
28277
|
-
}
|
|
28278
|
-
return router.createUrlTree([config.loginRoute]);
|
|
28279
|
-
};
|
|
28280
|
-
/**
|
|
28281
|
-
* Guard que verifica si el usuario NO está autenticado.
|
|
28282
|
-
* Redirige a homeRoute si ya está autenticado.
|
|
28283
|
-
* Útil para páginas de login/registro.
|
|
29929
|
+
* // Habilitar notificaciones push (solicita permisos + registra dispositivo)
|
|
29930
|
+
* async enableNotifications() {
|
|
29931
|
+
* const result = await this.auth.enableNotifications();
|
|
29932
|
+
* if (result.granted) {
|
|
29933
|
+
* console.log('Notificaciones habilitadas');
|
|
29934
|
+
* }
|
|
29935
|
+
* }
|
|
28284
29936
|
*
|
|
28285
|
-
*
|
|
28286
|
-
*
|
|
28287
|
-
*
|
|
29937
|
+
* // Verificar estado de permisos
|
|
29938
|
+
* get canReceiveNotifications(): boolean {
|
|
29939
|
+
* return this.auth.getNotificationPermissionState() === 'granted';
|
|
29940
|
+
* }
|
|
28288
29941
|
*
|
|
28289
|
-
*
|
|
28290
|
-
* {
|
|
28291
|
-
*
|
|
28292
|
-
*
|
|
28293
|
-
* loadComponent: () => import('./login.page'),
|
|
28294
|
-
* },
|
|
28295
|
-
* ];
|
|
29942
|
+
* // En template: usar signals directamente
|
|
29943
|
+
* // {{ auth.user()?.email }}
|
|
29944
|
+
* // @if (auth.hasPermission('templates:edit')) { ... }
|
|
29945
|
+
* }
|
|
28296
29946
|
* ```
|
|
28297
29947
|
*/
|
|
28298
|
-
|
|
28299
|
-
|
|
28300
|
-
const router = inject(Router);
|
|
28301
|
-
const config = inject(VALTECH_AUTH_CONFIG);
|
|
28302
|
-
if (!authService.isAuthenticated()) {
|
|
28303
|
-
return true;
|
|
28304
|
-
}
|
|
28305
|
-
return router.createUrlTree([config.homeRoute]);
|
|
28306
|
-
};
|
|
29948
|
+
// Tipos
|
|
29949
|
+
|
|
28307
29950
|
/**
|
|
28308
|
-
*
|
|
28309
|
-
* Verifica si el usuario tiene el permiso especificado.
|
|
29951
|
+
* Directiva estructural simplificada para estados de carga.
|
|
28310
29952
|
*
|
|
28311
|
-
*
|
|
28312
|
-
*
|
|
29953
|
+
* Soporta multiples fuentes de estado de carga:
|
|
29954
|
+
* - Angular Signal<boolean>
|
|
29955
|
+
* - RxJS Observable<boolean>
|
|
29956
|
+
* - Promise<any> (cargando hasta que se resuelve)
|
|
29957
|
+
* - boolean literal
|
|
28313
29958
|
*
|
|
28314
29959
|
* @example
|
|
28315
|
-
*
|
|
28316
|
-
*
|
|
28317
|
-
*
|
|
28318
|
-
*
|
|
28319
|
-
* {
|
|
28320
|
-
* path: 'templates',
|
|
28321
|
-
* canActivate: [authGuard, permissionGuard('templates:read')],
|
|
28322
|
-
* loadComponent: () => import('./templates.page'),
|
|
28323
|
-
* },
|
|
28324
|
-
* {
|
|
28325
|
-
* path: 'admin',
|
|
28326
|
-
* canActivate: [authGuard, permissionGuard(['admin:*', 'super_admin'])],
|
|
28327
|
-
* loadComponent: () => import('./admin.page'),
|
|
28328
|
-
* },
|
|
28329
|
-
* ];
|
|
28330
|
-
* ```
|
|
28331
|
-
*/
|
|
28332
|
-
function permissionGuard(permissions) {
|
|
28333
|
-
return () => {
|
|
28334
|
-
const authService = inject(AuthService);
|
|
28335
|
-
const router = inject(Router);
|
|
28336
|
-
const config = inject(VALTECH_AUTH_CONFIG);
|
|
28337
|
-
const permArray = Array.isArray(permissions) ? permissions : [permissions];
|
|
28338
|
-
if (authService.hasAnyPermission(permArray)) {
|
|
28339
|
-
return true;
|
|
28340
|
-
}
|
|
28341
|
-
console.warn(`[ValtechAuth] Permiso denegado. Requerido: ${permArray.join(' o ')}`);
|
|
28342
|
-
return router.createUrlTree([config.unauthorizedRoute]);
|
|
28343
|
-
};
|
|
28344
|
-
}
|
|
28345
|
-
/**
|
|
28346
|
-
* Guard que lee permisos desde route.data.
|
|
28347
|
-
* Permite configurar permisos directamente en la definición de rutas.
|
|
29960
|
+
* <!-- Uso simple con signal -->
|
|
29961
|
+
* <ng-container *valLoading="isLoading(); skeleton: 'list'">
|
|
29962
|
+
* <app-list [items]="items()"></app-list>
|
|
29963
|
+
* </ng-container>
|
|
28348
29964
|
*
|
|
28349
29965
|
* @example
|
|
28350
|
-
*
|
|
28351
|
-
*
|
|
29966
|
+
* <!-- Con configuracion -->
|
|
29967
|
+
* <ng-container *valLoading="loading$; skeleton: 'grid-cards'; count: 8">
|
|
29968
|
+
* <app-grid [data]="data"></app-grid>
|
|
29969
|
+
* </ng-container>
|
|
28352
29970
|
*
|
|
28353
|
-
*
|
|
28354
|
-
*
|
|
28355
|
-
*
|
|
28356
|
-
*
|
|
28357
|
-
*
|
|
28358
|
-
*
|
|
28359
|
-
*
|
|
28360
|
-
*
|
|
28361
|
-
* loadComponent: () => import('./users.page'),
|
|
28362
|
-
* },
|
|
28363
|
-
* ];
|
|
28364
|
-
* ```
|
|
29971
|
+
* @example
|
|
29972
|
+
* <!-- Con template personalizado -->
|
|
29973
|
+
* <ng-container *valLoading="isLoading(); skeletonTpl: customSkeleton">
|
|
29974
|
+
* <app-content></app-content>
|
|
29975
|
+
* </ng-container>
|
|
29976
|
+
* <ng-template #customSkeleton>
|
|
29977
|
+
* <val-skeleton [props]="{ type: 'card' }"></val-skeleton>
|
|
29978
|
+
* </ng-template>
|
|
28365
29979
|
*/
|
|
28366
|
-
|
|
28367
|
-
|
|
28368
|
-
|
|
28369
|
-
|
|
28370
|
-
|
|
28371
|
-
|
|
28372
|
-
|
|
28373
|
-
|
|
29980
|
+
class LoadingDirective {
|
|
29981
|
+
constructor() {
|
|
29982
|
+
this.templateRef = inject((TemplateRef));
|
|
29983
|
+
this.viewContainer = inject(ViewContainerRef);
|
|
29984
|
+
this.skeletonService = inject(SkeletonService);
|
|
29985
|
+
this.destroyRef = inject(DestroyRef);
|
|
29986
|
+
this._loading = signal(false);
|
|
29987
|
+
this.hasContentView = false;
|
|
29988
|
+
this.skeletonComponentRef = null;
|
|
29989
|
+
/** Template de skeleton a usar */
|
|
29990
|
+
this.skeleton = 'list';
|
|
29991
|
+
/** Template personalizado para skeleton */
|
|
29992
|
+
this.skeletonTpl = null;
|
|
29993
|
+
/** Cantidad de items skeleton */
|
|
29994
|
+
this.count = 3;
|
|
29995
|
+
/** Animacion habilitada */
|
|
29996
|
+
this.animated = true;
|
|
29997
|
+
/** Mostrar spinner en lugar de skeleton */
|
|
29998
|
+
this.spinner = false;
|
|
29999
|
+
effect(() => {
|
|
30000
|
+
const isLoading = this._loading();
|
|
30001
|
+
this.updateView(isLoading);
|
|
30002
|
+
});
|
|
28374
30003
|
}
|
|
28375
|
-
|
|
28376
|
-
|
|
28377
|
-
|
|
28378
|
-
|
|
28379
|
-
|
|
30004
|
+
/**
|
|
30005
|
+
* Input principal - puede ser boolean, Signal, Observable o Promise.
|
|
30006
|
+
*/
|
|
30007
|
+
set loading(source) {
|
|
30008
|
+
this.resolveLoadingSource(source);
|
|
28380
30009
|
}
|
|
28381
|
-
|
|
28382
|
-
|
|
28383
|
-
|
|
28384
|
-
/**
|
|
28385
|
-
* Guard que verifica si el usuario es super admin.
|
|
28386
|
-
*
|
|
28387
|
-
* @example
|
|
28388
|
-
* ```typescript
|
|
28389
|
-
* import { authGuard, superAdminGuard } from 'valtech-components';
|
|
28390
|
-
*
|
|
28391
|
-
* const routes: Routes = [
|
|
28392
|
-
* {
|
|
28393
|
-
* path: 'super-admin',
|
|
28394
|
-
* canActivate: [authGuard, superAdminGuard],
|
|
28395
|
-
* loadComponent: () => import('./super-admin.page'),
|
|
28396
|
-
* },
|
|
28397
|
-
* ];
|
|
28398
|
-
* ```
|
|
28399
|
-
*/
|
|
28400
|
-
const superAdminGuard = () => {
|
|
28401
|
-
const authService = inject(AuthService);
|
|
28402
|
-
const router = inject(Router);
|
|
28403
|
-
const config = inject(VALTECH_AUTH_CONFIG);
|
|
28404
|
-
if (authService.isSuperAdmin()) {
|
|
28405
|
-
return true;
|
|
30010
|
+
ngOnDestroy() {
|
|
30011
|
+
this.viewContainer.clear();
|
|
30012
|
+
this.skeletonComponentRef = null;
|
|
28406
30013
|
}
|
|
28407
|
-
|
|
28408
|
-
|
|
28409
|
-
|
|
30014
|
+
resolveLoadingSource(source) {
|
|
30015
|
+
if (typeof source === 'boolean') {
|
|
30016
|
+
this._loading.set(source);
|
|
30017
|
+
}
|
|
30018
|
+
else if (isSignal(source)) {
|
|
30019
|
+
// Sincronizar signal con effect
|
|
30020
|
+
effect(() => {
|
|
30021
|
+
this._loading.set(source());
|
|
30022
|
+
}, { injector: this.viewContainer.injector });
|
|
30023
|
+
}
|
|
30024
|
+
else if (isObservable(source)) {
|
|
30025
|
+
// Convertir observable a signal
|
|
30026
|
+
const signalValue = toSignal(source, {
|
|
30027
|
+
initialValue: true,
|
|
30028
|
+
injector: this.viewContainer.injector,
|
|
30029
|
+
});
|
|
30030
|
+
effect(() => {
|
|
30031
|
+
this._loading.set(signalValue());
|
|
30032
|
+
}, { injector: this.viewContainer.injector });
|
|
30033
|
+
}
|
|
30034
|
+
else if (source instanceof Promise) {
|
|
30035
|
+
this._loading.set(true);
|
|
30036
|
+
source.finally(() => this._loading.set(false));
|
|
30037
|
+
}
|
|
30038
|
+
}
|
|
30039
|
+
updateView(isLoading) {
|
|
30040
|
+
if (isLoading) {
|
|
30041
|
+
this.showSkeleton();
|
|
30042
|
+
}
|
|
30043
|
+
else {
|
|
30044
|
+
this.showContent();
|
|
30045
|
+
}
|
|
30046
|
+
}
|
|
30047
|
+
showSkeleton() {
|
|
30048
|
+
// Limpiar contenido previo
|
|
30049
|
+
this.viewContainer.clear();
|
|
30050
|
+
this.hasContentView = false;
|
|
30051
|
+
if (this.skeletonTpl) {
|
|
30052
|
+
// Usar template personalizado
|
|
30053
|
+
this.viewContainer.createEmbeddedView(this.skeletonTpl);
|
|
30054
|
+
}
|
|
30055
|
+
else if (this.spinner) {
|
|
30056
|
+
// Mostrar spinner (usar val-content-loader si esta disponible)
|
|
30057
|
+
this.showSpinner();
|
|
30058
|
+
}
|
|
30059
|
+
else {
|
|
30060
|
+
// Usar template de skeleton registrado
|
|
30061
|
+
this.showSkeletonTemplate();
|
|
30062
|
+
}
|
|
30063
|
+
}
|
|
30064
|
+
showSkeletonTemplate() {
|
|
30065
|
+
const template = this.skeletonService.getTemplate(this.skeleton);
|
|
30066
|
+
if (template) {
|
|
30067
|
+
this.skeletonComponentRef = this.viewContainer.createComponent(template.component);
|
|
30068
|
+
// Combinar config por defecto con inputs
|
|
30069
|
+
const config = {
|
|
30070
|
+
...template.defaultConfig,
|
|
30071
|
+
count: this.count,
|
|
30072
|
+
animated: this.animated,
|
|
30073
|
+
gap: this.gap,
|
|
30074
|
+
variant: this.variant,
|
|
30075
|
+
};
|
|
30076
|
+
this.skeletonComponentRef.instance.config = config;
|
|
30077
|
+
}
|
|
30078
|
+
else {
|
|
30079
|
+
// Fallback: mostrar texto de carga si no hay template
|
|
30080
|
+
console.warn(`[valLoading] Template '${this.skeleton}' not found. Using fallback.`);
|
|
30081
|
+
this.showFallback();
|
|
30082
|
+
}
|
|
30083
|
+
}
|
|
30084
|
+
showSpinner() {
|
|
30085
|
+
// Crear un div con spinner simple
|
|
30086
|
+
const element = document.createElement('div');
|
|
30087
|
+
element.className = 'val-loading-spinner';
|
|
30088
|
+
element.innerHTML = `
|
|
30089
|
+
<ion-spinner name="crescent"></ion-spinner>
|
|
30090
|
+
`;
|
|
30091
|
+
element.style.cssText = 'display: flex; justify-content: center; padding: 20px;';
|
|
30092
|
+
const hostElement = this.viewContainer.element.nativeElement;
|
|
30093
|
+
hostElement.parentElement?.insertBefore(element, hostElement);
|
|
30094
|
+
}
|
|
30095
|
+
showFallback() {
|
|
30096
|
+
// Fallback simple si no hay template registrado
|
|
30097
|
+
const element = document.createElement('div');
|
|
30098
|
+
element.className = 'val-loading-fallback';
|
|
30099
|
+
element.style.cssText =
|
|
30100
|
+
'display: flex; flex-direction: column; gap: 12px; padding: 16px;';
|
|
30101
|
+
for (let i = 0; i < this.count; i++) {
|
|
30102
|
+
const skeleton = document.createElement('div');
|
|
30103
|
+
skeleton.style.cssText = `
|
|
30104
|
+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
30105
|
+
background-size: 200% 100%;
|
|
30106
|
+
animation: skeleton-loading 1.5s infinite;
|
|
30107
|
+
height: 56px;
|
|
30108
|
+
border-radius: 8px;
|
|
30109
|
+
`;
|
|
30110
|
+
element.appendChild(skeleton);
|
|
30111
|
+
}
|
|
30112
|
+
const hostElement = this.viewContainer.element.nativeElement;
|
|
30113
|
+
hostElement.parentElement?.insertBefore(element, hostElement);
|
|
30114
|
+
}
|
|
30115
|
+
showContent() {
|
|
30116
|
+
// Limpiar skeleton
|
|
30117
|
+
this.viewContainer.clear();
|
|
30118
|
+
this.skeletonComponentRef = null;
|
|
30119
|
+
// Mostrar contenido real
|
|
30120
|
+
if (!this.hasContentView) {
|
|
30121
|
+
this.viewContainer.createEmbeddedView(this.templateRef);
|
|
30122
|
+
this.hasContentView = true;
|
|
30123
|
+
}
|
|
30124
|
+
}
|
|
30125
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
30126
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: LoadingDirective, isStandalone: true, selector: "[valLoading]", inputs: { skeleton: ["valLoadingSkeleton", "skeleton"], skeletonTpl: ["valLoadingSkeletonTpl", "skeletonTpl"], count: ["valLoadingCount", "count"], animated: ["valLoadingAnimated", "animated"], gap: ["valLoadingGap", "gap"], variant: ["valLoadingVariant", "variant"], spinner: ["valLoadingSpinner", "spinner"], loading: ["valLoading", "loading"] }, ngImport: i0 }); }
|
|
30127
|
+
}
|
|
30128
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingDirective, decorators: [{
|
|
30129
|
+
type: Directive,
|
|
30130
|
+
args: [{
|
|
30131
|
+
selector: '[valLoading]',
|
|
30132
|
+
standalone: true,
|
|
30133
|
+
}]
|
|
30134
|
+
}], ctorParameters: () => [], propDecorators: { skeleton: [{
|
|
30135
|
+
type: Input,
|
|
30136
|
+
args: ['valLoadingSkeleton']
|
|
30137
|
+
}], skeletonTpl: [{
|
|
30138
|
+
type: Input,
|
|
30139
|
+
args: ['valLoadingSkeletonTpl']
|
|
30140
|
+
}], count: [{
|
|
30141
|
+
type: Input,
|
|
30142
|
+
args: ['valLoadingCount']
|
|
30143
|
+
}], animated: [{
|
|
30144
|
+
type: Input,
|
|
30145
|
+
args: ['valLoadingAnimated']
|
|
30146
|
+
}], gap: [{
|
|
30147
|
+
type: Input,
|
|
30148
|
+
args: ['valLoadingGap']
|
|
30149
|
+
}], variant: [{
|
|
30150
|
+
type: Input,
|
|
30151
|
+
args: ['valLoadingVariant']
|
|
30152
|
+
}], spinner: [{
|
|
30153
|
+
type: Input,
|
|
30154
|
+
args: ['valLoadingSpinner']
|
|
30155
|
+
}], loading: [{
|
|
30156
|
+
type: Input,
|
|
30157
|
+
args: ['valLoading']
|
|
30158
|
+
}] } });
|
|
30159
|
+
|
|
28410
30160
|
/**
|
|
28411
|
-
*
|
|
28412
|
-
*
|
|
28413
|
-
* @param roles - Rol o lista de roles requeridos (OR)
|
|
28414
|
-
* @returns Guard function
|
|
30161
|
+
* Template de skeleton para listas.
|
|
28415
30162
|
*
|
|
28416
30163
|
* @example
|
|
28417
|
-
*
|
|
28418
|
-
* import { authGuard, roleGuard } from 'valtech-components';
|
|
30164
|
+
* <val-skeleton-list [config]="{ count: 5 }"></val-skeleton-list>
|
|
28419
30165
|
*
|
|
28420
|
-
*
|
|
28421
|
-
*
|
|
28422
|
-
* path: 'editor',
|
|
28423
|
-
* canActivate: [authGuard, roleGuard(['editor', 'admin'])],
|
|
28424
|
-
* loadComponent: () => import('./editor.page'),
|
|
28425
|
-
* },
|
|
28426
|
-
* ];
|
|
28427
|
-
* ```
|
|
30166
|
+
* @example
|
|
30167
|
+
* <val-skeleton-list [config]="{ count: 3, variant: 'avatar', gap: '16px' }"></val-skeleton-list>
|
|
28428
30168
|
*/
|
|
28429
|
-
|
|
28430
|
-
|
|
28431
|
-
|
|
28432
|
-
|
|
28433
|
-
|
|
28434
|
-
|
|
28435
|
-
|
|
28436
|
-
|
|
28437
|
-
|
|
30169
|
+
class ListSkeletonComponent {
|
|
30170
|
+
constructor() {
|
|
30171
|
+
this.config = { count: 3 };
|
|
30172
|
+
}
|
|
30173
|
+
get items() {
|
|
30174
|
+
return Array(this.config.count ?? 3).fill(0);
|
|
30175
|
+
}
|
|
30176
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30177
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ListSkeletonComponent, isStandalone: true, selector: "val-skeleton-list", inputs: { config: "config" }, ngImport: i0, template: `
|
|
30178
|
+
<div class="skeleton-list" [style.gap]="config.gap || '12px'" [class]="config.cssClass">
|
|
30179
|
+
@for (item of items; track $index) {
|
|
30180
|
+
@if (config.variant === 'avatar') {
|
|
30181
|
+
<div class="skeleton-list-item-avatar">
|
|
30182
|
+
<val-skeleton
|
|
30183
|
+
[props]="{
|
|
30184
|
+
type: 'avatar',
|
|
30185
|
+
animated: config.animated !== false
|
|
30186
|
+
}"
|
|
30187
|
+
></val-skeleton>
|
|
30188
|
+
<div class="skeleton-content">
|
|
30189
|
+
<val-skeleton
|
|
30190
|
+
[props]="{
|
|
30191
|
+
type: 'text',
|
|
30192
|
+
width: '70%',
|
|
30193
|
+
height: '16px',
|
|
30194
|
+
animated: config.animated !== false
|
|
30195
|
+
}"
|
|
30196
|
+
></val-skeleton>
|
|
30197
|
+
<val-skeleton
|
|
30198
|
+
[props]="{
|
|
30199
|
+
type: 'text',
|
|
30200
|
+
width: '50%',
|
|
30201
|
+
height: '14px',
|
|
30202
|
+
animated: config.animated !== false
|
|
30203
|
+
}"
|
|
30204
|
+
></val-skeleton>
|
|
30205
|
+
</div>
|
|
30206
|
+
</div>
|
|
30207
|
+
} @else {
|
|
30208
|
+
<val-skeleton
|
|
30209
|
+
[props]="{
|
|
30210
|
+
type: 'list-item',
|
|
30211
|
+
animated: config.animated !== false
|
|
30212
|
+
}"
|
|
30213
|
+
></val-skeleton>
|
|
28438
30214
|
}
|
|
28439
|
-
|
|
28440
|
-
|
|
28441
|
-
|
|
30215
|
+
}
|
|
30216
|
+
</div>
|
|
30217
|
+
`, isInline: true, styles: [".skeleton-list{display:flex;flex-direction:column;width:100%}.skeleton-list-item-avatar{display:flex;align-items:center;gap:12px;padding:8px 0}.skeleton-content{display:flex;flex-direction:column;gap:6px;flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
|
|
28442
30218
|
}
|
|
30219
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListSkeletonComponent, decorators: [{
|
|
30220
|
+
type: Component,
|
|
30221
|
+
args: [{ selector: 'val-skeleton-list', standalone: true, imports: [CommonModule, SkeletonComponent], template: `
|
|
30222
|
+
<div class="skeleton-list" [style.gap]="config.gap || '12px'" [class]="config.cssClass">
|
|
30223
|
+
@for (item of items; track $index) {
|
|
30224
|
+
@if (config.variant === 'avatar') {
|
|
30225
|
+
<div class="skeleton-list-item-avatar">
|
|
30226
|
+
<val-skeleton
|
|
30227
|
+
[props]="{
|
|
30228
|
+
type: 'avatar',
|
|
30229
|
+
animated: config.animated !== false
|
|
30230
|
+
}"
|
|
30231
|
+
></val-skeleton>
|
|
30232
|
+
<div class="skeleton-content">
|
|
30233
|
+
<val-skeleton
|
|
30234
|
+
[props]="{
|
|
30235
|
+
type: 'text',
|
|
30236
|
+
width: '70%',
|
|
30237
|
+
height: '16px',
|
|
30238
|
+
animated: config.animated !== false
|
|
30239
|
+
}"
|
|
30240
|
+
></val-skeleton>
|
|
30241
|
+
<val-skeleton
|
|
30242
|
+
[props]="{
|
|
30243
|
+
type: 'text',
|
|
30244
|
+
width: '50%',
|
|
30245
|
+
height: '14px',
|
|
30246
|
+
animated: config.animated !== false
|
|
30247
|
+
}"
|
|
30248
|
+
></val-skeleton>
|
|
30249
|
+
</div>
|
|
30250
|
+
</div>
|
|
30251
|
+
} @else {
|
|
30252
|
+
<val-skeleton
|
|
30253
|
+
[props]="{
|
|
30254
|
+
type: 'list-item',
|
|
30255
|
+
animated: config.animated !== false
|
|
30256
|
+
}"
|
|
30257
|
+
></val-skeleton>
|
|
30258
|
+
}
|
|
30259
|
+
}
|
|
30260
|
+
</div>
|
|
30261
|
+
`, styles: [".skeleton-list{display:flex;flex-direction:column;width:100%}.skeleton-list-item-avatar{display:flex;align-items:center;gap:12px;padding:8px 0}.skeleton-content{display:flex;flex-direction:column;gap:6px;flex:1}\n"] }]
|
|
30262
|
+
}], propDecorators: { config: [{
|
|
30263
|
+
type: Input
|
|
30264
|
+
}] } });
|
|
28443
30265
|
|
|
28444
30266
|
/**
|
|
28445
|
-
*
|
|
28446
|
-
* Permite listar, aprobar, bloquear y eliminar dispositivos registrados.
|
|
30267
|
+
* Template de skeleton para grids de cards.
|
|
28447
30268
|
*
|
|
28448
30269
|
* @example
|
|
28449
|
-
*
|
|
28450
|
-
* import { DeviceService } from 'valtech-components';
|
|
28451
|
-
*
|
|
28452
|
-
* @Component({...})
|
|
28453
|
-
* export class DevicesPage {
|
|
28454
|
-
* private deviceService = inject(DeviceService);
|
|
28455
|
-
*
|
|
28456
|
-
* devices = signal<DeviceInfo[]>([]);
|
|
28457
|
-
*
|
|
28458
|
-
* async ngOnInit() {
|
|
28459
|
-
* const devices = await firstValueFrom(this.deviceService.listDevices());
|
|
28460
|
-
* this.devices.set(devices);
|
|
28461
|
-
* }
|
|
30270
|
+
* <val-skeleton-grid [config]="{ count: 6 }"></val-skeleton-grid>
|
|
28462
30271
|
*
|
|
28463
|
-
*
|
|
28464
|
-
*
|
|
28465
|
-
* // Recargar lista
|
|
28466
|
-
* }
|
|
28467
|
-
* }
|
|
28468
|
-
* ```
|
|
30272
|
+
* @example
|
|
30273
|
+
* <val-skeleton-grid [config]="{ count: 8, variant: 'cards', columns: 4 }"></val-skeleton-grid>
|
|
28469
30274
|
*/
|
|
28470
|
-
class
|
|
28471
|
-
constructor(
|
|
28472
|
-
this.config =
|
|
28473
|
-
this.http = http;
|
|
30275
|
+
class GridSkeletonComponent {
|
|
30276
|
+
constructor() {
|
|
30277
|
+
this.config = { count: 6 };
|
|
28474
30278
|
}
|
|
28475
|
-
get
|
|
28476
|
-
return
|
|
30279
|
+
get items() {
|
|
30280
|
+
return Array(this.config.count ?? 6).fill(0);
|
|
28477
30281
|
}
|
|
28478
|
-
|
|
28479
|
-
|
|
28480
|
-
|
|
28481
|
-
|
|
28482
|
-
return
|
|
30282
|
+
get gridColumns() {
|
|
30283
|
+
if (this.config.columns) {
|
|
30284
|
+
return `repeat(${this.config.columns}, 1fr)`;
|
|
30285
|
+
}
|
|
30286
|
+
return '';
|
|
28483
30287
|
}
|
|
28484
|
-
|
|
28485
|
-
|
|
28486
|
-
|
|
28487
|
-
|
|
28488
|
-
|
|
30288
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30289
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: GridSkeletonComponent, isStandalone: true, selector: "val-skeleton-grid", inputs: { config: "config" }, ngImport: i0, template: `
|
|
30290
|
+
<div
|
|
30291
|
+
class="skeleton-grid"
|
|
30292
|
+
[class]="'variant-' + (config.variant || 'default') + ' ' + (config.cssClass || '')"
|
|
30293
|
+
[style.gap]="config.gap || '16px'"
|
|
30294
|
+
[style.grid-template-columns]="gridColumns"
|
|
30295
|
+
>
|
|
30296
|
+
@for (item of items; track $index) {
|
|
30297
|
+
@switch (config.variant) {
|
|
30298
|
+
@case ('cards') {
|
|
30299
|
+
<div class="skeleton-card">
|
|
30300
|
+
<val-skeleton
|
|
30301
|
+
[props]="{
|
|
30302
|
+
type: 'custom',
|
|
30303
|
+
width: '100%',
|
|
30304
|
+
height: '140px',
|
|
30305
|
+
borderRadius: '8px 8px 0 0',
|
|
30306
|
+
animated: config.animated !== false
|
|
30307
|
+
}"
|
|
30308
|
+
></val-skeleton>
|
|
30309
|
+
<div class="skeleton-card-content">
|
|
30310
|
+
<val-skeleton
|
|
30311
|
+
[props]="{
|
|
30312
|
+
type: 'text',
|
|
30313
|
+
width: '80%',
|
|
30314
|
+
height: '18px',
|
|
30315
|
+
animated: config.animated !== false
|
|
30316
|
+
}"
|
|
30317
|
+
></val-skeleton>
|
|
30318
|
+
<val-skeleton
|
|
30319
|
+
[props]="{
|
|
30320
|
+
type: 'text',
|
|
30321
|
+
width: '60%',
|
|
30322
|
+
height: '14px',
|
|
30323
|
+
animated: config.animated !== false
|
|
30324
|
+
}"
|
|
30325
|
+
></val-skeleton>
|
|
30326
|
+
</div>
|
|
30327
|
+
</div>
|
|
30328
|
+
}
|
|
30329
|
+
@case ('compact') {
|
|
30330
|
+
<val-skeleton
|
|
30331
|
+
[props]="{
|
|
30332
|
+
type: 'thumbnail',
|
|
30333
|
+
animated: config.animated !== false
|
|
30334
|
+
}"
|
|
30335
|
+
></val-skeleton>
|
|
30336
|
+
}
|
|
30337
|
+
@default {
|
|
30338
|
+
<val-skeleton
|
|
30339
|
+
[props]="{
|
|
30340
|
+
type: 'card',
|
|
30341
|
+
animated: config.animated !== false
|
|
30342
|
+
}"
|
|
30343
|
+
></val-skeleton>
|
|
30344
|
+
}
|
|
30345
|
+
}
|
|
30346
|
+
}
|
|
30347
|
+
</div>
|
|
30348
|
+
`, isInline: true, styles: [".skeleton-grid{display:grid;width:100%;&.variant-default{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}&.variant-cards{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}&.variant-compact{grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:12px}}.skeleton-card{border-radius:8px;overflow:hidden;background:var(--ion-color-light, #f4f5f8)}.skeleton-card-content{padding:12px;display:flex;flex-direction:column;gap:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
|
|
30349
|
+
}
|
|
30350
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridSkeletonComponent, decorators: [{
|
|
30351
|
+
type: Component,
|
|
30352
|
+
args: [{ selector: 'val-skeleton-grid', standalone: true, imports: [CommonModule, SkeletonComponent], template: `
|
|
30353
|
+
<div
|
|
30354
|
+
class="skeleton-grid"
|
|
30355
|
+
[class]="'variant-' + (config.variant || 'default') + ' ' + (config.cssClass || '')"
|
|
30356
|
+
[style.gap]="config.gap || '16px'"
|
|
30357
|
+
[style.grid-template-columns]="gridColumns"
|
|
30358
|
+
>
|
|
30359
|
+
@for (item of items; track $index) {
|
|
30360
|
+
@switch (config.variant) {
|
|
30361
|
+
@case ('cards') {
|
|
30362
|
+
<div class="skeleton-card">
|
|
30363
|
+
<val-skeleton
|
|
30364
|
+
[props]="{
|
|
30365
|
+
type: 'custom',
|
|
30366
|
+
width: '100%',
|
|
30367
|
+
height: '140px',
|
|
30368
|
+
borderRadius: '8px 8px 0 0',
|
|
30369
|
+
animated: config.animated !== false
|
|
30370
|
+
}"
|
|
30371
|
+
></val-skeleton>
|
|
30372
|
+
<div class="skeleton-card-content">
|
|
30373
|
+
<val-skeleton
|
|
30374
|
+
[props]="{
|
|
30375
|
+
type: 'text',
|
|
30376
|
+
width: '80%',
|
|
30377
|
+
height: '18px',
|
|
30378
|
+
animated: config.animated !== false
|
|
30379
|
+
}"
|
|
30380
|
+
></val-skeleton>
|
|
30381
|
+
<val-skeleton
|
|
30382
|
+
[props]="{
|
|
30383
|
+
type: 'text',
|
|
30384
|
+
width: '60%',
|
|
30385
|
+
height: '14px',
|
|
30386
|
+
animated: config.animated !== false
|
|
30387
|
+
}"
|
|
30388
|
+
></val-skeleton>
|
|
30389
|
+
</div>
|
|
30390
|
+
</div>
|
|
30391
|
+
}
|
|
30392
|
+
@case ('compact') {
|
|
30393
|
+
<val-skeleton
|
|
30394
|
+
[props]="{
|
|
30395
|
+
type: 'thumbnail',
|
|
30396
|
+
animated: config.animated !== false
|
|
30397
|
+
}"
|
|
30398
|
+
></val-skeleton>
|
|
30399
|
+
}
|
|
30400
|
+
@default {
|
|
30401
|
+
<val-skeleton
|
|
30402
|
+
[props]="{
|
|
30403
|
+
type: 'card',
|
|
30404
|
+
animated: config.animated !== false
|
|
30405
|
+
}"
|
|
30406
|
+
></val-skeleton>
|
|
30407
|
+
}
|
|
30408
|
+
}
|
|
30409
|
+
}
|
|
30410
|
+
</div>
|
|
30411
|
+
`, styles: [".skeleton-grid{display:grid;width:100%;&.variant-default{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}&.variant-cards{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}&.variant-compact{grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:12px}}.skeleton-card{border-radius:8px;overflow:hidden;background:var(--ion-color-light, #f4f5f8)}.skeleton-card-content{padding:12px;display:flex;flex-direction:column;gap:8px}\n"] }]
|
|
30412
|
+
}], propDecorators: { config: [{
|
|
30413
|
+
type: Input
|
|
30414
|
+
}] } });
|
|
30415
|
+
|
|
30416
|
+
/**
|
|
30417
|
+
* Template de skeleton para formularios.
|
|
30418
|
+
*
|
|
30419
|
+
* @example
|
|
30420
|
+
* <val-skeleton-form [config]="{ count: 4 }"></val-skeleton-form>
|
|
30421
|
+
*
|
|
30422
|
+
* @example
|
|
30423
|
+
* <val-skeleton-form [config]="{ count: 3, variant: 'compact' }"></val-skeleton-form>
|
|
30424
|
+
*/
|
|
30425
|
+
class FormSkeletonComponent {
|
|
30426
|
+
constructor() {
|
|
30427
|
+
this.config = { count: 3 };
|
|
28489
30428
|
}
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
* Revoca todas las sesiones activas de ese dispositivo.
|
|
28493
|
-
*/
|
|
28494
|
-
blockDevice(deviceId) {
|
|
28495
|
-
return this.http.post(`${this.baseUrl}/${deviceId}/block`, {});
|
|
30429
|
+
get fields() {
|
|
30430
|
+
return Array(this.config.count ?? 3).fill(0);
|
|
28496
30431
|
}
|
|
28497
|
-
|
|
28498
|
-
|
|
28499
|
-
* Cambia el estado de pending_approval a active.
|
|
28500
|
-
*/
|
|
28501
|
-
approveDevice(deviceId) {
|
|
28502
|
-
return this.http.post(`${this.baseUrl}/${deviceId}/approve`, {});
|
|
30432
|
+
get isCompact() {
|
|
30433
|
+
return this.config.variant === 'compact';
|
|
28503
30434
|
}
|
|
28504
|
-
|
|
28505
|
-
|
|
28506
|
-
*/
|
|
28507
|
-
deleteDevice(deviceId) {
|
|
28508
|
-
return this.http.delete(`${this.baseUrl}/${deviceId}`);
|
|
30435
|
+
get labelWidth() {
|
|
30436
|
+
return this.isCompact ? '60px' : '80px';
|
|
28509
30437
|
}
|
|
28510
|
-
|
|
28511
|
-
|
|
28512
|
-
* Útil para mostrar confirmación al usuario antes de ejecutar.
|
|
28513
|
-
* Este endpoint NO requiere autenticación.
|
|
28514
|
-
*
|
|
28515
|
-
* @param token Token JWT de acción
|
|
28516
|
-
* @returns Información del token si es válido
|
|
28517
|
-
*
|
|
28518
|
-
* @example
|
|
28519
|
-
* ```typescript
|
|
28520
|
-
* const token = this.route.snapshot.queryParams['token'];
|
|
28521
|
-
* if (token) {
|
|
28522
|
-
* const validation = await firstValueFrom(this.deviceService.validateAction(token));
|
|
28523
|
-
* if (validation.valid) {
|
|
28524
|
-
* // Mostrar confirmación al usuario
|
|
28525
|
-
* console.log(`Acción: ${validation.actionType}`);
|
|
28526
|
-
* }
|
|
28527
|
-
* }
|
|
28528
|
-
* ```
|
|
28529
|
-
*/
|
|
28530
|
-
validateAction(token) {
|
|
28531
|
-
return this.http.post(`${this.config.apiUrl}/v2/actions/validate`, { token });
|
|
30438
|
+
get inputHeight() {
|
|
30439
|
+
return this.isCompact ? '38px' : '44px';
|
|
28532
30440
|
}
|
|
28533
|
-
|
|
28534
|
-
|
|
28535
|
-
* Este endpoint NO requiere autenticación.
|
|
28536
|
-
* El token viene en la URL del email de alerta de nuevo inicio de sesión.
|
|
28537
|
-
*
|
|
28538
|
-
* @param token Token JWT de acción (24h, un solo uso)
|
|
28539
|
-
*
|
|
28540
|
-
* @example
|
|
28541
|
-
* ```typescript
|
|
28542
|
-
* // En la página de dispositivos, al detectar ?token=xxx en la URL:
|
|
28543
|
-
* const token = this.route.snapshot.queryParams['token'];
|
|
28544
|
-
* if (token) {
|
|
28545
|
-
* const result = await firstValueFrom(this.deviceService.executeAction(token));
|
|
28546
|
-
* if (result.success) {
|
|
28547
|
-
* console.log(`Dispositivo bloqueado, ${result.sessionsRevoked} sesiones cerradas`);
|
|
28548
|
-
* }
|
|
28549
|
-
* }
|
|
28550
|
-
* ```
|
|
28551
|
-
*/
|
|
28552
|
-
executeAction(token) {
|
|
28553
|
-
// Usa el endpoint unificado de acciones
|
|
28554
|
-
return this.http.post(`${this.config.apiUrl}/v2/actions/execute`, { token }).pipe(map$1(response => ({
|
|
28555
|
-
operationId: response.operationId,
|
|
28556
|
-
success: response.success,
|
|
28557
|
-
message: response.message,
|
|
28558
|
-
action: response.data?.['action'] || 'refuse',
|
|
28559
|
-
deviceId: response.data?.['deviceId'] || '',
|
|
28560
|
-
sessionsRevoked: response.data?.['sessionsRevoked'],
|
|
28561
|
-
})));
|
|
30441
|
+
get buttonWidth() {
|
|
30442
|
+
return this.isCompact ? '120px' : '100%';
|
|
28562
30443
|
}
|
|
28563
|
-
|
|
28564
|
-
|
|
30444
|
+
get buttonHeight() {
|
|
30445
|
+
return this.isCompact ? '38px' : '48px';
|
|
30446
|
+
}
|
|
30447
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FormSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30448
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FormSkeletonComponent, isStandalone: true, selector: "val-skeleton-form", inputs: { config: "config" }, ngImport: i0, template: `
|
|
30449
|
+
<div
|
|
30450
|
+
class="skeleton-form"
|
|
30451
|
+
[class]="'variant-' + (config.variant || 'default') + ' ' + (config.cssClass || '')"
|
|
30452
|
+
>
|
|
30453
|
+
@for (field of fields; track $index) {
|
|
30454
|
+
<div class="skeleton-field">
|
|
30455
|
+
<!-- Label -->
|
|
30456
|
+
<val-skeleton
|
|
30457
|
+
[props]="{
|
|
30458
|
+
type: 'text',
|
|
30459
|
+
width: labelWidth,
|
|
30460
|
+
height: '14px',
|
|
30461
|
+
animated: config.animated !== false
|
|
30462
|
+
}"
|
|
30463
|
+
></val-skeleton>
|
|
30464
|
+
<!-- Input -->
|
|
30465
|
+
<val-skeleton
|
|
30466
|
+
[props]="{
|
|
30467
|
+
type: 'custom',
|
|
30468
|
+
width: '100%',
|
|
30469
|
+
height: inputHeight,
|
|
30470
|
+
borderRadius: '8px',
|
|
30471
|
+
animated: config.animated !== false
|
|
30472
|
+
}"
|
|
30473
|
+
></val-skeleton>
|
|
30474
|
+
</div>
|
|
30475
|
+
}
|
|
30476
|
+
<!-- Submit button -->
|
|
30477
|
+
<div class="skeleton-button">
|
|
30478
|
+
<val-skeleton
|
|
30479
|
+
[props]="{
|
|
30480
|
+
type: 'custom',
|
|
30481
|
+
width: buttonWidth,
|
|
30482
|
+
height: buttonHeight,
|
|
30483
|
+
borderRadius: '8px',
|
|
30484
|
+
animated: config.animated !== false
|
|
30485
|
+
}"
|
|
30486
|
+
></val-skeleton>
|
|
30487
|
+
</div>
|
|
30488
|
+
</div>
|
|
30489
|
+
`, isInline: true, styles: [".skeleton-form{display:flex;flex-direction:column;width:100%;&.variant-default{gap:20px}&.variant-compact{gap:12px}}.skeleton-field{display:flex;flex-direction:column;gap:6px}.skeleton-button{margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
|
|
28565
30490
|
}
|
|
28566
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
28567
|
-
type:
|
|
28568
|
-
args: [{
|
|
28569
|
-
|
|
28570
|
-
|
|
28571
|
-
|
|
28572
|
-
|
|
30491
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FormSkeletonComponent, decorators: [{
|
|
30492
|
+
type: Component,
|
|
30493
|
+
args: [{ selector: 'val-skeleton-form', standalone: true, imports: [CommonModule, SkeletonComponent], template: `
|
|
30494
|
+
<div
|
|
30495
|
+
class="skeleton-form"
|
|
30496
|
+
[class]="'variant-' + (config.variant || 'default') + ' ' + (config.cssClass || '')"
|
|
30497
|
+
>
|
|
30498
|
+
@for (field of fields; track $index) {
|
|
30499
|
+
<div class="skeleton-field">
|
|
30500
|
+
<!-- Label -->
|
|
30501
|
+
<val-skeleton
|
|
30502
|
+
[props]="{
|
|
30503
|
+
type: 'text',
|
|
30504
|
+
width: labelWidth,
|
|
30505
|
+
height: '14px',
|
|
30506
|
+
animated: config.animated !== false
|
|
30507
|
+
}"
|
|
30508
|
+
></val-skeleton>
|
|
30509
|
+
<!-- Input -->
|
|
30510
|
+
<val-skeleton
|
|
30511
|
+
[props]="{
|
|
30512
|
+
type: 'custom',
|
|
30513
|
+
width: '100%',
|
|
30514
|
+
height: inputHeight,
|
|
30515
|
+
borderRadius: '8px',
|
|
30516
|
+
animated: config.animated !== false
|
|
30517
|
+
}"
|
|
30518
|
+
></val-skeleton>
|
|
30519
|
+
</div>
|
|
30520
|
+
}
|
|
30521
|
+
<!-- Submit button -->
|
|
30522
|
+
<div class="skeleton-button">
|
|
30523
|
+
<val-skeleton
|
|
30524
|
+
[props]="{
|
|
30525
|
+
type: 'custom',
|
|
30526
|
+
width: buttonWidth,
|
|
30527
|
+
height: buttonHeight,
|
|
30528
|
+
borderRadius: '8px',
|
|
30529
|
+
animated: config.animated !== false
|
|
30530
|
+
}"
|
|
30531
|
+
></val-skeleton>
|
|
30532
|
+
</div>
|
|
30533
|
+
</div>
|
|
30534
|
+
`, styles: [".skeleton-form{display:flex;flex-direction:column;width:100%;&.variant-default{gap:20px}&.variant-compact{gap:12px}}.skeleton-field{display:flex;flex-direction:column;gap:6px}.skeleton-button{margin-top:8px}\n"] }]
|
|
30535
|
+
}], propDecorators: { config: [{
|
|
30536
|
+
type: Input
|
|
30537
|
+
}] } });
|
|
28573
30538
|
|
|
28574
30539
|
/**
|
|
28575
|
-
*
|
|
28576
|
-
* Permite listar y revocar sesiones.
|
|
30540
|
+
* Template de skeleton para perfiles de usuario.
|
|
28577
30541
|
*
|
|
28578
30542
|
* @example
|
|
28579
|
-
*
|
|
28580
|
-
* import { SessionService } from 'valtech-components';
|
|
28581
|
-
*
|
|
28582
|
-
* @Component({...})
|
|
28583
|
-
* export class SessionsPage {
|
|
28584
|
-
* private sessionService = inject(SessionService);
|
|
30543
|
+
* <val-skeleton-profile></val-skeleton-profile>
|
|
28585
30544
|
*
|
|
28586
|
-
*
|
|
30545
|
+
* @example
|
|
30546
|
+
* <val-skeleton-profile [config]="{ variant: 'full' }"></val-skeleton-profile>
|
|
30547
|
+
*/
|
|
30548
|
+
class ProfileSkeletonComponent {
|
|
30549
|
+
constructor() {
|
|
30550
|
+
this.config = {};
|
|
30551
|
+
}
|
|
30552
|
+
get avatarSize() {
|
|
30553
|
+
return this.config.variant === 'full' ? '96px' : '48px';
|
|
30554
|
+
}
|
|
30555
|
+
get nameWidth() {
|
|
30556
|
+
return this.config.variant === 'full' ? '180px' : '160px';
|
|
30557
|
+
}
|
|
30558
|
+
get subtitleWidth() {
|
|
30559
|
+
return this.config.variant === 'full' ? '140px' : '120px';
|
|
30560
|
+
}
|
|
30561
|
+
get stats() {
|
|
30562
|
+
return [1, 2, 3];
|
|
30563
|
+
}
|
|
30564
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProfileSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30565
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ProfileSkeletonComponent, isStandalone: true, selector: "val-skeleton-profile", inputs: { config: "config" }, ngImport: i0, template: `
|
|
30566
|
+
<div
|
|
30567
|
+
class="skeleton-profile"
|
|
30568
|
+
[class]="'variant-' + (config.variant || 'default') + ' ' + (config.cssClass || '')"
|
|
30569
|
+
>
|
|
30570
|
+
<div class="profile-header">
|
|
30571
|
+
<!-- Avatar -->
|
|
30572
|
+
<val-skeleton
|
|
30573
|
+
[props]="{
|
|
30574
|
+
type: 'avatar',
|
|
30575
|
+
width: avatarSize,
|
|
30576
|
+
height: avatarSize,
|
|
30577
|
+
animated: config.animated !== false
|
|
30578
|
+
}"
|
|
30579
|
+
></val-skeleton>
|
|
30580
|
+
|
|
30581
|
+
<div class="profile-info">
|
|
30582
|
+
<!-- Name -->
|
|
30583
|
+
<val-skeleton
|
|
30584
|
+
[props]="{
|
|
30585
|
+
type: 'text',
|
|
30586
|
+
width: nameWidth,
|
|
30587
|
+
height: '20px',
|
|
30588
|
+
animated: config.animated !== false
|
|
30589
|
+
}"
|
|
30590
|
+
></val-skeleton>
|
|
30591
|
+
<!-- Subtitle -->
|
|
30592
|
+
<val-skeleton
|
|
30593
|
+
[props]="{
|
|
30594
|
+
type: 'text',
|
|
30595
|
+
width: subtitleWidth,
|
|
30596
|
+
height: '14px',
|
|
30597
|
+
animated: config.animated !== false
|
|
30598
|
+
}"
|
|
30599
|
+
></val-skeleton>
|
|
30600
|
+
@if (config.variant === 'full') {
|
|
30601
|
+
<!-- Extra info -->
|
|
30602
|
+
<val-skeleton
|
|
30603
|
+
[props]="{
|
|
30604
|
+
type: 'text',
|
|
30605
|
+
width: '100px',
|
|
30606
|
+
height: '12px',
|
|
30607
|
+
animated: config.animated !== false
|
|
30608
|
+
}"
|
|
30609
|
+
></val-skeleton>
|
|
30610
|
+
}
|
|
30611
|
+
</div>
|
|
30612
|
+
</div>
|
|
30613
|
+
|
|
30614
|
+
@if (config.variant === 'full') {
|
|
30615
|
+
<div class="profile-details">
|
|
30616
|
+
<val-skeleton
|
|
30617
|
+
[props]="{
|
|
30618
|
+
type: 'paragraph',
|
|
30619
|
+
lines: 3,
|
|
30620
|
+
animated: config.animated !== false
|
|
30621
|
+
}"
|
|
30622
|
+
></val-skeleton>
|
|
30623
|
+
|
|
30624
|
+
<div class="profile-stats">
|
|
30625
|
+
@for (stat of stats; track $index) {
|
|
30626
|
+
<div class="stat-item">
|
|
30627
|
+
<val-skeleton
|
|
30628
|
+
[props]="{
|
|
30629
|
+
type: 'text',
|
|
30630
|
+
width: '40px',
|
|
30631
|
+
height: '24px',
|
|
30632
|
+
animated: config.animated !== false
|
|
30633
|
+
}"
|
|
30634
|
+
></val-skeleton>
|
|
30635
|
+
<val-skeleton
|
|
30636
|
+
[props]="{
|
|
30637
|
+
type: 'text',
|
|
30638
|
+
width: '60px',
|
|
30639
|
+
height: '12px',
|
|
30640
|
+
animated: config.animated !== false
|
|
30641
|
+
}"
|
|
30642
|
+
></val-skeleton>
|
|
30643
|
+
</div>
|
|
30644
|
+
}
|
|
30645
|
+
</div>
|
|
30646
|
+
</div>
|
|
30647
|
+
}
|
|
30648
|
+
</div>
|
|
30649
|
+
`, isInline: true, styles: [".skeleton-profile{width:100%;&.variant-default .profile-header{display:flex;align-items:center;gap:12px}&.variant-full{.profile-header{display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center}.profile-info{align-items:center}.profile-details{margin-top:24px}}}.profile-info{display:flex;flex-direction:column;gap:6px}.profile-stats{display:flex;justify-content:center;gap:32px;margin-top:20px}.stat-item{display:flex;flex-direction:column;align-items:center;gap:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
|
|
30650
|
+
}
|
|
30651
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProfileSkeletonComponent, decorators: [{
|
|
30652
|
+
type: Component,
|
|
30653
|
+
args: [{ selector: 'val-skeleton-profile', standalone: true, imports: [CommonModule, SkeletonComponent], template: `
|
|
30654
|
+
<div
|
|
30655
|
+
class="skeleton-profile"
|
|
30656
|
+
[class]="'variant-' + (config.variant || 'default') + ' ' + (config.cssClass || '')"
|
|
30657
|
+
>
|
|
30658
|
+
<div class="profile-header">
|
|
30659
|
+
<!-- Avatar -->
|
|
30660
|
+
<val-skeleton
|
|
30661
|
+
[props]="{
|
|
30662
|
+
type: 'avatar',
|
|
30663
|
+
width: avatarSize,
|
|
30664
|
+
height: avatarSize,
|
|
30665
|
+
animated: config.animated !== false
|
|
30666
|
+
}"
|
|
30667
|
+
></val-skeleton>
|
|
30668
|
+
|
|
30669
|
+
<div class="profile-info">
|
|
30670
|
+
<!-- Name -->
|
|
30671
|
+
<val-skeleton
|
|
30672
|
+
[props]="{
|
|
30673
|
+
type: 'text',
|
|
30674
|
+
width: nameWidth,
|
|
30675
|
+
height: '20px',
|
|
30676
|
+
animated: config.animated !== false
|
|
30677
|
+
}"
|
|
30678
|
+
></val-skeleton>
|
|
30679
|
+
<!-- Subtitle -->
|
|
30680
|
+
<val-skeleton
|
|
30681
|
+
[props]="{
|
|
30682
|
+
type: 'text',
|
|
30683
|
+
width: subtitleWidth,
|
|
30684
|
+
height: '14px',
|
|
30685
|
+
animated: config.animated !== false
|
|
30686
|
+
}"
|
|
30687
|
+
></val-skeleton>
|
|
30688
|
+
@if (config.variant === 'full') {
|
|
30689
|
+
<!-- Extra info -->
|
|
30690
|
+
<val-skeleton
|
|
30691
|
+
[props]="{
|
|
30692
|
+
type: 'text',
|
|
30693
|
+
width: '100px',
|
|
30694
|
+
height: '12px',
|
|
30695
|
+
animated: config.animated !== false
|
|
30696
|
+
}"
|
|
30697
|
+
></val-skeleton>
|
|
30698
|
+
}
|
|
30699
|
+
</div>
|
|
30700
|
+
</div>
|
|
30701
|
+
|
|
30702
|
+
@if (config.variant === 'full') {
|
|
30703
|
+
<div class="profile-details">
|
|
30704
|
+
<val-skeleton
|
|
30705
|
+
[props]="{
|
|
30706
|
+
type: 'paragraph',
|
|
30707
|
+
lines: 3,
|
|
30708
|
+
animated: config.animated !== false
|
|
30709
|
+
}"
|
|
30710
|
+
></val-skeleton>
|
|
30711
|
+
|
|
30712
|
+
<div class="profile-stats">
|
|
30713
|
+
@for (stat of stats; track $index) {
|
|
30714
|
+
<div class="stat-item">
|
|
30715
|
+
<val-skeleton
|
|
30716
|
+
[props]="{
|
|
30717
|
+
type: 'text',
|
|
30718
|
+
width: '40px',
|
|
30719
|
+
height: '24px',
|
|
30720
|
+
animated: config.animated !== false
|
|
30721
|
+
}"
|
|
30722
|
+
></val-skeleton>
|
|
30723
|
+
<val-skeleton
|
|
30724
|
+
[props]="{
|
|
30725
|
+
type: 'text',
|
|
30726
|
+
width: '60px',
|
|
30727
|
+
height: '12px',
|
|
30728
|
+
animated: config.animated !== false
|
|
30729
|
+
}"
|
|
30730
|
+
></val-skeleton>
|
|
30731
|
+
</div>
|
|
30732
|
+
}
|
|
30733
|
+
</div>
|
|
30734
|
+
</div>
|
|
30735
|
+
}
|
|
30736
|
+
</div>
|
|
30737
|
+
`, styles: [".skeleton-profile{width:100%;&.variant-default .profile-header{display:flex;align-items:center;gap:12px}&.variant-full{.profile-header{display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center}.profile-info{align-items:center}.profile-details{margin-top:24px}}}.profile-info{display:flex;flex-direction:column;gap:6px}.profile-stats{display:flex;justify-content:center;gap:32px;margin-top:20px}.stat-item{display:flex;flex-direction:column;align-items:center;gap:4px}\n"] }]
|
|
30738
|
+
}], propDecorators: { config: [{
|
|
30739
|
+
type: Input
|
|
30740
|
+
}] } });
|
|
30741
|
+
|
|
30742
|
+
/**
|
|
30743
|
+
* Template de skeleton para tablas.
|
|
28587
30744
|
*
|
|
28588
|
-
*
|
|
28589
|
-
*
|
|
28590
|
-
|
|
28591
|
-
|
|
30745
|
+
* @example
|
|
30746
|
+
* <val-skeleton-table [config]="{ columns: 5, rows: 10 }"></val-skeleton-table>
|
|
30747
|
+
*/
|
|
30748
|
+
class TableSkeletonComponent {
|
|
30749
|
+
constructor() {
|
|
30750
|
+
this.config = { columns: 4, rows: 5 };
|
|
30751
|
+
}
|
|
30752
|
+
get columns() {
|
|
30753
|
+
return Array(this.config.columns ?? 4).fill(0);
|
|
30754
|
+
}
|
|
30755
|
+
get rows() {
|
|
30756
|
+
return Array(this.config.rows ?? 5).fill(0);
|
|
30757
|
+
}
|
|
30758
|
+
getColumnFlex(index) {
|
|
30759
|
+
// Primera columna mas ancha, ultima mas angosta
|
|
30760
|
+
const totalCols = this.config.columns ?? 4;
|
|
30761
|
+
if (index === 0)
|
|
30762
|
+
return '1.5';
|
|
30763
|
+
if (index === totalCols - 1)
|
|
30764
|
+
return '0.8';
|
|
30765
|
+
return '1';
|
|
30766
|
+
}
|
|
30767
|
+
getColumnContentWidth(index) {
|
|
30768
|
+
// Variar anchos para aspecto natural
|
|
30769
|
+
const widths = ['90%', '70%', '60%', '80%', '50%'];
|
|
30770
|
+
return widths[index % widths.length];
|
|
30771
|
+
}
|
|
30772
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TableSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30773
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TableSkeletonComponent, isStandalone: true, selector: "val-skeleton-table", inputs: { config: "config" }, ngImport: i0, template: `
|
|
30774
|
+
<div class="skeleton-table" [class]="config.cssClass">
|
|
30775
|
+
<!-- Header -->
|
|
30776
|
+
<div class="skeleton-table-header">
|
|
30777
|
+
@for (col of columns; track $index) {
|
|
30778
|
+
<div class="skeleton-table-cell" [style.flex]="getColumnFlex($index)">
|
|
30779
|
+
<val-skeleton
|
|
30780
|
+
[props]="{
|
|
30781
|
+
type: 'text',
|
|
30782
|
+
width: '80%',
|
|
30783
|
+
height: '16px',
|
|
30784
|
+
animated: config.animated !== false
|
|
30785
|
+
}"
|
|
30786
|
+
></val-skeleton>
|
|
30787
|
+
</div>
|
|
30788
|
+
}
|
|
30789
|
+
</div>
|
|
30790
|
+
|
|
30791
|
+
<!-- Rows -->
|
|
30792
|
+
@for (row of rows; track $index) {
|
|
30793
|
+
<div class="skeleton-table-row">
|
|
30794
|
+
@for (col of columns; track $index) {
|
|
30795
|
+
<div class="skeleton-table-cell" [style.flex]="getColumnFlex($index)">
|
|
30796
|
+
<val-skeleton
|
|
30797
|
+
[props]="{
|
|
30798
|
+
type: 'text',
|
|
30799
|
+
width: getColumnContentWidth($index),
|
|
30800
|
+
height: '14px',
|
|
30801
|
+
animated: config.animated !== false
|
|
30802
|
+
}"
|
|
30803
|
+
></val-skeleton>
|
|
30804
|
+
</div>
|
|
30805
|
+
}
|
|
30806
|
+
</div>
|
|
30807
|
+
}
|
|
30808
|
+
</div>
|
|
30809
|
+
`, isInline: true, styles: [".skeleton-table{width:100%;overflow:hidden;border-radius:8px;border:1px solid var(--ion-color-light-shade, #d7d8da)}.skeleton-table-header{display:flex;align-items:center;gap:16px;padding:14px 16px;background:var(--ion-color-light, #f4f5f8);border-bottom:1px solid var(--ion-color-light-shade, #d7d8da)}.skeleton-table-row{display:flex;align-items:center;gap:16px;padding:12px 16px;border-bottom:1px solid var(--ion-color-light-shade, #d7d8da);&:last-child{border-bottom:none}}.skeleton-table-cell{min-width:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
|
|
30810
|
+
}
|
|
30811
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TableSkeletonComponent, decorators: [{
|
|
30812
|
+
type: Component,
|
|
30813
|
+
args: [{ selector: 'val-skeleton-table', standalone: true, imports: [CommonModule, SkeletonComponent], template: `
|
|
30814
|
+
<div class="skeleton-table" [class]="config.cssClass">
|
|
30815
|
+
<!-- Header -->
|
|
30816
|
+
<div class="skeleton-table-header">
|
|
30817
|
+
@for (col of columns; track $index) {
|
|
30818
|
+
<div class="skeleton-table-cell" [style.flex]="getColumnFlex($index)">
|
|
30819
|
+
<val-skeleton
|
|
30820
|
+
[props]="{
|
|
30821
|
+
type: 'text',
|
|
30822
|
+
width: '80%',
|
|
30823
|
+
height: '16px',
|
|
30824
|
+
animated: config.animated !== false
|
|
30825
|
+
}"
|
|
30826
|
+
></val-skeleton>
|
|
30827
|
+
</div>
|
|
30828
|
+
}
|
|
30829
|
+
</div>
|
|
30830
|
+
|
|
30831
|
+
<!-- Rows -->
|
|
30832
|
+
@for (row of rows; track $index) {
|
|
30833
|
+
<div class="skeleton-table-row">
|
|
30834
|
+
@for (col of columns; track $index) {
|
|
30835
|
+
<div class="skeleton-table-cell" [style.flex]="getColumnFlex($index)">
|
|
30836
|
+
<val-skeleton
|
|
30837
|
+
[props]="{
|
|
30838
|
+
type: 'text',
|
|
30839
|
+
width: getColumnContentWidth($index),
|
|
30840
|
+
height: '14px',
|
|
30841
|
+
animated: config.animated !== false
|
|
30842
|
+
}"
|
|
30843
|
+
></val-skeleton>
|
|
30844
|
+
</div>
|
|
30845
|
+
}
|
|
30846
|
+
</div>
|
|
30847
|
+
}
|
|
30848
|
+
</div>
|
|
30849
|
+
`, styles: [".skeleton-table{width:100%;overflow:hidden;border-radius:8px;border:1px solid var(--ion-color-light-shade, #d7d8da)}.skeleton-table-header{display:flex;align-items:center;gap:16px;padding:14px 16px;background:var(--ion-color-light, #f4f5f8);border-bottom:1px solid var(--ion-color-light-shade, #d7d8da)}.skeleton-table-row{display:flex;align-items:center;gap:16px;padding:12px 16px;border-bottom:1px solid var(--ion-color-light-shade, #d7d8da);&:last-child{border-bottom:none}}.skeleton-table-cell{min-width:0}\n"] }]
|
|
30850
|
+
}], propDecorators: { config: [{
|
|
30851
|
+
type: Input
|
|
30852
|
+
}] } });
|
|
30853
|
+
|
|
30854
|
+
/**
|
|
30855
|
+
* Template de skeleton para paginas de detalle.
|
|
28592
30856
|
*
|
|
28593
|
-
*
|
|
28594
|
-
*
|
|
28595
|
-
* // Recargar lista
|
|
28596
|
-
* }
|
|
30857
|
+
* @example
|
|
30858
|
+
* <val-skeleton-detail></val-skeleton-detail>
|
|
28597
30859
|
*
|
|
28598
|
-
*
|
|
28599
|
-
*
|
|
28600
|
-
* console.log(`${result.sessionsRevoked} sesiones cerradas`);
|
|
28601
|
-
* }
|
|
28602
|
-
* }
|
|
28603
|
-
* ```
|
|
30860
|
+
* @example
|
|
30861
|
+
* <val-skeleton-detail [config]="{ sections: 3 }"></val-skeleton-detail>
|
|
28604
30862
|
*/
|
|
28605
|
-
class
|
|
28606
|
-
constructor(
|
|
28607
|
-
this.config =
|
|
28608
|
-
this.http = http;
|
|
28609
|
-
}
|
|
28610
|
-
get baseUrl() {
|
|
28611
|
-
return `${this.config.apiUrl}/v2/users/me/sessions`;
|
|
28612
|
-
}
|
|
28613
|
-
/**
|
|
28614
|
-
* Lista todas las sesiones activas del usuario.
|
|
28615
|
-
* La sesión actual está marcada con isCurrent=true.
|
|
28616
|
-
*/
|
|
28617
|
-
listSessions() {
|
|
28618
|
-
return this.http.get(this.baseUrl).pipe(map$1(response => response.sessions));
|
|
30863
|
+
class DetailSkeletonComponent {
|
|
30864
|
+
constructor() {
|
|
30865
|
+
this.config = { sections: 2 };
|
|
28619
30866
|
}
|
|
28620
|
-
|
|
28621
|
-
|
|
28622
|
-
* Fuerza el cierre de sesión en ese dispositivo/navegador.
|
|
28623
|
-
*/
|
|
28624
|
-
revokeSession(sessionId) {
|
|
28625
|
-
return this.http.delete(`${this.baseUrl}/${sessionId}`);
|
|
30867
|
+
get sections() {
|
|
30868
|
+
return Array(this.config.sections ?? 2).fill(0);
|
|
28626
30869
|
}
|
|
28627
|
-
|
|
28628
|
-
|
|
28629
|
-
* Útil para "cerrar sesión en todos los dispositivos".
|
|
28630
|
-
*
|
|
28631
|
-
* @returns Número de sesiones revocadas
|
|
28632
|
-
*/
|
|
28633
|
-
revokeAllSessions() {
|
|
28634
|
-
return this.http.delete(this.baseUrl);
|
|
30870
|
+
get metaItems() {
|
|
30871
|
+
return [1, 2, 3];
|
|
28635
30872
|
}
|
|
28636
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
28637
|
-
static { this.ɵ
|
|
30873
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DetailSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30874
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DetailSkeletonComponent, isStandalone: true, selector: "val-skeleton-detail", inputs: { config: "config" }, ngImport: i0, template: `
|
|
30875
|
+
<div class="skeleton-detail" [class]="config.cssClass">
|
|
30876
|
+
<!-- Hero/Header section -->
|
|
30877
|
+
<div class="skeleton-detail-hero">
|
|
30878
|
+
<val-skeleton
|
|
30879
|
+
[props]="{
|
|
30880
|
+
type: 'custom',
|
|
30881
|
+
width: '100%',
|
|
30882
|
+
height: '200px',
|
|
30883
|
+
borderRadius: '8px',
|
|
30884
|
+
animated: config.animated !== false
|
|
30885
|
+
}"
|
|
30886
|
+
></val-skeleton>
|
|
30887
|
+
</div>
|
|
30888
|
+
|
|
30889
|
+
<!-- Title section -->
|
|
30890
|
+
<div class="skeleton-detail-title">
|
|
30891
|
+
<val-skeleton
|
|
30892
|
+
[props]="{
|
|
30893
|
+
type: 'text',
|
|
30894
|
+
width: '70%',
|
|
30895
|
+
height: '28px',
|
|
30896
|
+
animated: config.animated !== false
|
|
30897
|
+
}"
|
|
30898
|
+
></val-skeleton>
|
|
30899
|
+
<val-skeleton
|
|
30900
|
+
[props]="{
|
|
30901
|
+
type: 'text',
|
|
30902
|
+
width: '40%',
|
|
30903
|
+
height: '16px',
|
|
30904
|
+
animated: config.animated !== false
|
|
30905
|
+
}"
|
|
30906
|
+
></val-skeleton>
|
|
30907
|
+
</div>
|
|
30908
|
+
|
|
30909
|
+
<!-- Metadata row -->
|
|
30910
|
+
<div class="skeleton-detail-meta">
|
|
30911
|
+
@for (meta of metaItems; track $index) {
|
|
30912
|
+
<val-skeleton
|
|
30913
|
+
[props]="{
|
|
30914
|
+
type: 'custom',
|
|
30915
|
+
width: '80px',
|
|
30916
|
+
height: '24px',
|
|
30917
|
+
borderRadius: '12px',
|
|
30918
|
+
animated: config.animated !== false
|
|
30919
|
+
}"
|
|
30920
|
+
></val-skeleton>
|
|
30921
|
+
}
|
|
30922
|
+
</div>
|
|
30923
|
+
|
|
30924
|
+
<!-- Content sections -->
|
|
30925
|
+
@for (section of sections; track $index) {
|
|
30926
|
+
<div class="skeleton-detail-section">
|
|
30927
|
+
<!-- Section title -->
|
|
30928
|
+
<val-skeleton
|
|
30929
|
+
[props]="{
|
|
30930
|
+
type: 'text',
|
|
30931
|
+
width: '30%',
|
|
30932
|
+
height: '20px',
|
|
30933
|
+
animated: config.animated !== false
|
|
30934
|
+
}"
|
|
30935
|
+
></val-skeleton>
|
|
30936
|
+
<!-- Section content -->
|
|
30937
|
+
<val-skeleton
|
|
30938
|
+
[props]="{
|
|
30939
|
+
type: 'paragraph',
|
|
30940
|
+
lines: 4,
|
|
30941
|
+
animated: config.animated !== false
|
|
30942
|
+
}"
|
|
30943
|
+
></val-skeleton>
|
|
30944
|
+
</div>
|
|
30945
|
+
}
|
|
30946
|
+
|
|
30947
|
+
<!-- Action buttons -->
|
|
30948
|
+
<div class="skeleton-detail-actions">
|
|
30949
|
+
<val-skeleton
|
|
30950
|
+
[props]="{
|
|
30951
|
+
type: 'custom',
|
|
30952
|
+
width: '120px',
|
|
30953
|
+
height: '44px',
|
|
30954
|
+
borderRadius: '8px',
|
|
30955
|
+
animated: config.animated !== false
|
|
30956
|
+
}"
|
|
30957
|
+
></val-skeleton>
|
|
30958
|
+
<val-skeleton
|
|
30959
|
+
[props]="{
|
|
30960
|
+
type: 'custom',
|
|
30961
|
+
width: '120px',
|
|
30962
|
+
height: '44px',
|
|
30963
|
+
borderRadius: '8px',
|
|
30964
|
+
animated: config.animated !== false
|
|
30965
|
+
}"
|
|
30966
|
+
></val-skeleton>
|
|
30967
|
+
</div>
|
|
30968
|
+
</div>
|
|
30969
|
+
`, isInline: true, styles: [".skeleton-detail{display:flex;flex-direction:column;gap:20px;width:100%}.skeleton-detail-hero{width:100%}.skeleton-detail-title{display:flex;flex-direction:column;gap:8px}.skeleton-detail-meta{display:flex;gap:12px;flex-wrap:wrap}.skeleton-detail-section{display:flex;flex-direction:column;gap:12px;padding-top:16px;border-top:1px solid var(--ion-color-light-shade, #d7d8da)}.skeleton-detail-actions{display:flex;gap:12px;padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
|
|
30970
|
+
}
|
|
30971
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DetailSkeletonComponent, decorators: [{
|
|
30972
|
+
type: Component,
|
|
30973
|
+
args: [{ selector: 'val-skeleton-detail', standalone: true, imports: [CommonModule, SkeletonComponent], template: `
|
|
30974
|
+
<div class="skeleton-detail" [class]="config.cssClass">
|
|
30975
|
+
<!-- Hero/Header section -->
|
|
30976
|
+
<div class="skeleton-detail-hero">
|
|
30977
|
+
<val-skeleton
|
|
30978
|
+
[props]="{
|
|
30979
|
+
type: 'custom',
|
|
30980
|
+
width: '100%',
|
|
30981
|
+
height: '200px',
|
|
30982
|
+
borderRadius: '8px',
|
|
30983
|
+
animated: config.animated !== false
|
|
30984
|
+
}"
|
|
30985
|
+
></val-skeleton>
|
|
30986
|
+
</div>
|
|
30987
|
+
|
|
30988
|
+
<!-- Title section -->
|
|
30989
|
+
<div class="skeleton-detail-title">
|
|
30990
|
+
<val-skeleton
|
|
30991
|
+
[props]="{
|
|
30992
|
+
type: 'text',
|
|
30993
|
+
width: '70%',
|
|
30994
|
+
height: '28px',
|
|
30995
|
+
animated: config.animated !== false
|
|
30996
|
+
}"
|
|
30997
|
+
></val-skeleton>
|
|
30998
|
+
<val-skeleton
|
|
30999
|
+
[props]="{
|
|
31000
|
+
type: 'text',
|
|
31001
|
+
width: '40%',
|
|
31002
|
+
height: '16px',
|
|
31003
|
+
animated: config.animated !== false
|
|
31004
|
+
}"
|
|
31005
|
+
></val-skeleton>
|
|
31006
|
+
</div>
|
|
31007
|
+
|
|
31008
|
+
<!-- Metadata row -->
|
|
31009
|
+
<div class="skeleton-detail-meta">
|
|
31010
|
+
@for (meta of metaItems; track $index) {
|
|
31011
|
+
<val-skeleton
|
|
31012
|
+
[props]="{
|
|
31013
|
+
type: 'custom',
|
|
31014
|
+
width: '80px',
|
|
31015
|
+
height: '24px',
|
|
31016
|
+
borderRadius: '12px',
|
|
31017
|
+
animated: config.animated !== false
|
|
31018
|
+
}"
|
|
31019
|
+
></val-skeleton>
|
|
31020
|
+
}
|
|
31021
|
+
</div>
|
|
31022
|
+
|
|
31023
|
+
<!-- Content sections -->
|
|
31024
|
+
@for (section of sections; track $index) {
|
|
31025
|
+
<div class="skeleton-detail-section">
|
|
31026
|
+
<!-- Section title -->
|
|
31027
|
+
<val-skeleton
|
|
31028
|
+
[props]="{
|
|
31029
|
+
type: 'text',
|
|
31030
|
+
width: '30%',
|
|
31031
|
+
height: '20px',
|
|
31032
|
+
animated: config.animated !== false
|
|
31033
|
+
}"
|
|
31034
|
+
></val-skeleton>
|
|
31035
|
+
<!-- Section content -->
|
|
31036
|
+
<val-skeleton
|
|
31037
|
+
[props]="{
|
|
31038
|
+
type: 'paragraph',
|
|
31039
|
+
lines: 4,
|
|
31040
|
+
animated: config.animated !== false
|
|
31041
|
+
}"
|
|
31042
|
+
></val-skeleton>
|
|
31043
|
+
</div>
|
|
31044
|
+
}
|
|
31045
|
+
|
|
31046
|
+
<!-- Action buttons -->
|
|
31047
|
+
<div class="skeleton-detail-actions">
|
|
31048
|
+
<val-skeleton
|
|
31049
|
+
[props]="{
|
|
31050
|
+
type: 'custom',
|
|
31051
|
+
width: '120px',
|
|
31052
|
+
height: '44px',
|
|
31053
|
+
borderRadius: '8px',
|
|
31054
|
+
animated: config.animated !== false
|
|
31055
|
+
}"
|
|
31056
|
+
></val-skeleton>
|
|
31057
|
+
<val-skeleton
|
|
31058
|
+
[props]="{
|
|
31059
|
+
type: 'custom',
|
|
31060
|
+
width: '120px',
|
|
31061
|
+
height: '44px',
|
|
31062
|
+
borderRadius: '8px',
|
|
31063
|
+
animated: config.animated !== false
|
|
31064
|
+
}"
|
|
31065
|
+
></val-skeleton>
|
|
31066
|
+
</div>
|
|
31067
|
+
</div>
|
|
31068
|
+
`, styles: [".skeleton-detail{display:flex;flex-direction:column;gap:20px;width:100%}.skeleton-detail-hero{width:100%}.skeleton-detail-title{display:flex;flex-direction:column;gap:8px}.skeleton-detail-meta{display:flex;gap:12px;flex-wrap:wrap}.skeleton-detail-section{display:flex;flex-direction:column;gap:12px;padding-top:16px;border-top:1px solid var(--ion-color-light-shade, #d7d8da)}.skeleton-detail-actions{display:flex;gap:12px;padding-top:16px}\n"] }]
|
|
31069
|
+
}], propDecorators: { config: [{
|
|
31070
|
+
type: Input
|
|
31071
|
+
}] } });
|
|
31072
|
+
|
|
31073
|
+
/**
|
|
31074
|
+
* Configura el sistema de skeletons para Valtech Components.
|
|
31075
|
+
*
|
|
31076
|
+
* @param config Configuracion de skeletons
|
|
31077
|
+
* @returns Providers para app.config.ts
|
|
31078
|
+
*
|
|
31079
|
+
* @example
|
|
31080
|
+
* // app.config.ts
|
|
31081
|
+
* import { provideValtechSkeleton } from 'valtech-components';
|
|
31082
|
+
*
|
|
31083
|
+
* export const appConfig: ApplicationConfig = {
|
|
31084
|
+
* providers: [
|
|
31085
|
+
* provideValtechSkeleton({
|
|
31086
|
+
* animated: true,
|
|
31087
|
+
* defaultDelay: 100,
|
|
31088
|
+
* defaultMinTime: 500,
|
|
31089
|
+
* templates: [
|
|
31090
|
+
* { name: 'custom-card', component: MyCustomCardSkeleton }
|
|
31091
|
+
* ]
|
|
31092
|
+
* }),
|
|
31093
|
+
* ]
|
|
31094
|
+
* };
|
|
31095
|
+
*
|
|
31096
|
+
* @example
|
|
31097
|
+
* // Uso minimo - usa configuracion por defecto
|
|
31098
|
+
* providers: [provideValtechSkeleton()]
|
|
31099
|
+
*/
|
|
31100
|
+
function provideValtechSkeleton(config = {}) {
|
|
31101
|
+
const mergedConfig = { ...DEFAULT_SKELETON_CONFIG, ...config };
|
|
31102
|
+
return makeEnvironmentProviders([
|
|
31103
|
+
{
|
|
31104
|
+
provide: APP_INITIALIZER,
|
|
31105
|
+
useFactory: (skeletonService) => {
|
|
31106
|
+
return () => {
|
|
31107
|
+
// Configurar servicio
|
|
31108
|
+
skeletonService.configure(mergedConfig);
|
|
31109
|
+
// Registrar templates built-in
|
|
31110
|
+
skeletonService.registerTemplate('list', ListSkeletonComponent, { count: 3 });
|
|
31111
|
+
skeletonService.registerTemplate('list-avatar', ListSkeletonComponent, {
|
|
31112
|
+
count: 3,
|
|
31113
|
+
variant: 'avatar',
|
|
31114
|
+
});
|
|
31115
|
+
skeletonService.registerTemplate('grid', GridSkeletonComponent, { count: 4 });
|
|
31116
|
+
skeletonService.registerTemplate('grid-cards', GridSkeletonComponent, {
|
|
31117
|
+
count: 6,
|
|
31118
|
+
variant: 'cards',
|
|
31119
|
+
});
|
|
31120
|
+
skeletonService.registerTemplate('form', FormSkeletonComponent, { count: 3 });
|
|
31121
|
+
skeletonService.registerTemplate('form-compact', FormSkeletonComponent, {
|
|
31122
|
+
count: 2,
|
|
31123
|
+
variant: 'compact',
|
|
31124
|
+
});
|
|
31125
|
+
skeletonService.registerTemplate('profile', ProfileSkeletonComponent, {});
|
|
31126
|
+
skeletonService.registerTemplate('profile-full', ProfileSkeletonComponent, {
|
|
31127
|
+
variant: 'full',
|
|
31128
|
+
});
|
|
31129
|
+
skeletonService.registerTemplate('table', TableSkeletonComponent, {
|
|
31130
|
+
columns: 4,
|
|
31131
|
+
rows: 5,
|
|
31132
|
+
});
|
|
31133
|
+
skeletonService.registerTemplate('detail', DetailSkeletonComponent, { sections: 2 });
|
|
31134
|
+
};
|
|
31135
|
+
},
|
|
31136
|
+
deps: [SkeletonService],
|
|
31137
|
+
multi: true,
|
|
31138
|
+
},
|
|
31139
|
+
]);
|
|
31140
|
+
}
|
|
31141
|
+
|
|
31142
|
+
// Types
|
|
31143
|
+
|
|
31144
|
+
/**
|
|
31145
|
+
* Estado inicial para controladores de paginacion.
|
|
31146
|
+
*/
|
|
31147
|
+
function createInitialPaginationState(pageSize, initialItems = []) {
|
|
31148
|
+
return {
|
|
31149
|
+
items: initialItems,
|
|
31150
|
+
page: 0,
|
|
31151
|
+
pageSize,
|
|
31152
|
+
hasMore: true,
|
|
31153
|
+
isLoading: false,
|
|
31154
|
+
error: undefined,
|
|
31155
|
+
};
|
|
28638
31156
|
}
|
|
28639
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SessionService, decorators: [{
|
|
28640
|
-
type: Injectable,
|
|
28641
|
-
args: [{ providedIn: 'root' }]
|
|
28642
|
-
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
28643
|
-
type: Inject,
|
|
28644
|
-
args: [VALTECH_AUTH_CONFIG]
|
|
28645
|
-
}] }, { type: i1$8.HttpClient }] });
|
|
28646
31157
|
|
|
28647
31158
|
/**
|
|
28648
|
-
*
|
|
28649
|
-
*
|
|
28650
|
-
* Este componente procesa la respuesta del servidor OAuth y envía
|
|
28651
|
-
* los tokens a la ventana padre via postMessage.
|
|
28652
|
-
*
|
|
28653
|
-
* Debe agregarse a las rutas de la aplicación:
|
|
28654
|
-
* ```typescript
|
|
28655
|
-
* // app.routes.ts
|
|
28656
|
-
* import { OAuthCallbackComponent } from 'valtech-components';
|
|
28657
|
-
*
|
|
28658
|
-
* export const routes: Routes = [
|
|
28659
|
-
* { path: 'auth/oauth/callback', component: OAuthCallbackComponent },
|
|
28660
|
-
* // ... otras rutas
|
|
28661
|
-
* ];
|
|
28662
|
-
* ```
|
|
28663
|
-
*
|
|
28664
|
-
* El backend redirige a esta ruta con los tokens en query params:
|
|
28665
|
-
* `/auth/oauth/callback?access_token=xxx&refresh_token=xxx&expires_in=900`
|
|
28666
|
-
*
|
|
28667
|
-
* O con error:
|
|
28668
|
-
* `/auth/oauth/callback?error=INVALID_CODE&error_description=...`
|
|
31159
|
+
* Implementacion del controlador de paginacion.
|
|
28669
31160
|
*/
|
|
28670
|
-
class
|
|
28671
|
-
constructor() {
|
|
28672
|
-
this.
|
|
28673
|
-
|
|
28674
|
-
|
|
28675
|
-
this.
|
|
28676
|
-
|
|
28677
|
-
|
|
28678
|
-
|
|
28679
|
-
|
|
28680
|
-
|
|
28681
|
-
|
|
28682
|
-
|
|
28683
|
-
|
|
28684
|
-
|
|
28685
|
-
code: error,
|
|
28686
|
-
message: params.get('error_description') || 'Error de autenticación',
|
|
28687
|
-
},
|
|
28688
|
-
});
|
|
28689
|
-
this.message = 'Error de autenticación';
|
|
28690
|
-
this.closeAfterDelay();
|
|
31161
|
+
class PaginationControllerImpl {
|
|
31162
|
+
constructor(config) {
|
|
31163
|
+
this.config = config;
|
|
31164
|
+
this._state = signal(createInitialPaginationState(this.config.pageSize ?? 20, this.config.initialItems));
|
|
31165
|
+
this.state = this._state.asReadonly();
|
|
31166
|
+
this.items = computed(() => this._state().items);
|
|
31167
|
+
this.isLoading = computed(() => this._state().isLoading);
|
|
31168
|
+
this.hasMore = computed(() => this._state().hasMore);
|
|
31169
|
+
this.error = computed(() => this._state().error ?? null);
|
|
31170
|
+
this.currentPage = computed(() => this._state().page);
|
|
31171
|
+
this.total = computed(() => this._state().total);
|
|
31172
|
+
}
|
|
31173
|
+
async loadNext() {
|
|
31174
|
+
const currentState = this._state();
|
|
31175
|
+
if (currentState.isLoading || !currentState.hasMore)
|
|
28691
31176
|
return;
|
|
31177
|
+
this._state.update((s) => ({ ...s, isLoading: true, error: undefined }));
|
|
31178
|
+
try {
|
|
31179
|
+
const params = {
|
|
31180
|
+
strategy: this.config.strategy,
|
|
31181
|
+
page: currentState.page,
|
|
31182
|
+
pageSize: currentState.pageSize,
|
|
31183
|
+
cursor: currentState.nextCursor,
|
|
31184
|
+
direction: 'forward',
|
|
31185
|
+
};
|
|
31186
|
+
const result = await this.executeLoad(params);
|
|
31187
|
+
this._state.update((s) => ({
|
|
31188
|
+
...s,
|
|
31189
|
+
items: [...s.items, ...result.items],
|
|
31190
|
+
page: s.page + 1,
|
|
31191
|
+
hasMore: result.hasMore,
|
|
31192
|
+
total: result.total ?? s.total,
|
|
31193
|
+
nextCursor: result.nextCursor,
|
|
31194
|
+
prevCursor: result.prevCursor ?? s.prevCursor,
|
|
31195
|
+
isLoading: false,
|
|
31196
|
+
}));
|
|
28692
31197
|
}
|
|
28693
|
-
|
|
28694
|
-
|
|
28695
|
-
|
|
28696
|
-
|
|
28697
|
-
|
|
28698
|
-
|
|
28699
|
-
this.sendToParent({
|
|
28700
|
-
type: 'oauth-callback',
|
|
28701
|
-
error: {
|
|
28702
|
-
code: 'MISSING_TOKENS',
|
|
28703
|
-
message: 'No se recibieron los tokens de autenticación',
|
|
28704
|
-
},
|
|
28705
|
-
});
|
|
28706
|
-
this.message = 'Error: tokens no recibidos';
|
|
28707
|
-
this.closeAfterDelay();
|
|
28708
|
-
return;
|
|
31198
|
+
catch (err) {
|
|
31199
|
+
this._state.update((s) => ({
|
|
31200
|
+
...s,
|
|
31201
|
+
isLoading: false,
|
|
31202
|
+
error: err,
|
|
31203
|
+
}));
|
|
28709
31204
|
}
|
|
28710
|
-
|
|
28711
|
-
|
|
28712
|
-
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
28717
|
-
|
|
28718
|
-
|
|
28719
|
-
|
|
28720
|
-
|
|
28721
|
-
|
|
31205
|
+
}
|
|
31206
|
+
async loadPrevious() {
|
|
31207
|
+
const currentState = this._state();
|
|
31208
|
+
if (currentState.isLoading || currentState.page <= 0)
|
|
31209
|
+
return;
|
|
31210
|
+
this._state.update((s) => ({ ...s, isLoading: true, error: undefined }));
|
|
31211
|
+
try {
|
|
31212
|
+
const params = {
|
|
31213
|
+
strategy: this.config.strategy,
|
|
31214
|
+
page: currentState.page - 1,
|
|
31215
|
+
pageSize: currentState.pageSize,
|
|
31216
|
+
cursor: currentState.prevCursor,
|
|
31217
|
+
direction: 'backward',
|
|
31218
|
+
};
|
|
31219
|
+
const result = await this.executeLoad(params);
|
|
31220
|
+
this._state.update((s) => ({
|
|
31221
|
+
...s,
|
|
31222
|
+
items: [...result.items, ...s.items],
|
|
31223
|
+
page: s.page - 1,
|
|
31224
|
+
prevCursor: result.prevCursor,
|
|
31225
|
+
isLoading: false,
|
|
31226
|
+
}));
|
|
28722
31227
|
}
|
|
28723
|
-
|
|
28724
|
-
|
|
28725
|
-
|
|
28726
|
-
|
|
28727
|
-
|
|
28728
|
-
|
|
28729
|
-
}
|
|
31228
|
+
catch (err) {
|
|
31229
|
+
this._state.update((s) => ({
|
|
31230
|
+
...s,
|
|
31231
|
+
isLoading: false,
|
|
31232
|
+
error: err,
|
|
31233
|
+
}));
|
|
28730
31234
|
}
|
|
28731
|
-
// Enviar tokens a la ventana padre
|
|
28732
|
-
const result = {
|
|
28733
|
-
accessToken,
|
|
28734
|
-
refreshToken,
|
|
28735
|
-
expiresIn: expiresIn ? parseInt(expiresIn, 10) : 900,
|
|
28736
|
-
firebaseToken: firebaseToken || undefined,
|
|
28737
|
-
roles,
|
|
28738
|
-
permissions,
|
|
28739
|
-
isNewUser: params.get('is_new_user') === 'true',
|
|
28740
|
-
linked: params.get('linked') === 'true',
|
|
28741
|
-
};
|
|
28742
|
-
this.sendToParent({
|
|
28743
|
-
type: 'oauth-callback',
|
|
28744
|
-
tokens: result,
|
|
28745
|
-
});
|
|
28746
|
-
this.message = 'Autenticación exitosa';
|
|
28747
|
-
this.closeAfterDelay();
|
|
28748
31235
|
}
|
|
28749
|
-
|
|
28750
|
-
|
|
28751
|
-
|
|
28752
|
-
|
|
31236
|
+
async refresh() {
|
|
31237
|
+
this._state.update((s) => ({
|
|
31238
|
+
...s,
|
|
31239
|
+
items: [],
|
|
31240
|
+
page: 0,
|
|
31241
|
+
hasMore: true,
|
|
31242
|
+
nextCursor: undefined,
|
|
31243
|
+
prevCursor: undefined,
|
|
31244
|
+
isLoading: true,
|
|
31245
|
+
error: undefined,
|
|
31246
|
+
}));
|
|
31247
|
+
try {
|
|
31248
|
+
const params = {
|
|
31249
|
+
strategy: this.config.strategy,
|
|
31250
|
+
page: 0,
|
|
31251
|
+
pageSize: this._state().pageSize,
|
|
31252
|
+
direction: 'forward',
|
|
31253
|
+
};
|
|
31254
|
+
const result = await this.executeLoad(params);
|
|
31255
|
+
this._state.update((s) => ({
|
|
31256
|
+
...s,
|
|
31257
|
+
items: result.items,
|
|
31258
|
+
page: 1,
|
|
31259
|
+
hasMore: result.hasMore,
|
|
31260
|
+
total: result.total,
|
|
31261
|
+
nextCursor: result.nextCursor,
|
|
31262
|
+
isLoading: false,
|
|
31263
|
+
}));
|
|
28753
31264
|
}
|
|
28754
|
-
|
|
28755
|
-
|
|
28756
|
-
|
|
31265
|
+
catch (err) {
|
|
31266
|
+
this._state.update((s) => ({
|
|
31267
|
+
...s,
|
|
31268
|
+
isLoading: false,
|
|
31269
|
+
error: err,
|
|
31270
|
+
}));
|
|
28757
31271
|
}
|
|
28758
31272
|
}
|
|
28759
|
-
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
28763
|
-
|
|
28764
|
-
|
|
28765
|
-
|
|
31273
|
+
reset() {
|
|
31274
|
+
this._state.set(createInitialPaginationState(this.config.pageSize ?? 20, this.config.initialItems));
|
|
31275
|
+
}
|
|
31276
|
+
updateItem(predicate, updates) {
|
|
31277
|
+
this._state.update((s) => ({
|
|
31278
|
+
...s,
|
|
31279
|
+
items: s.items.map((item) => (predicate(item) ? { ...item, ...updates } : item)),
|
|
31280
|
+
}));
|
|
31281
|
+
}
|
|
31282
|
+
removeItem(predicate) {
|
|
31283
|
+
this._state.update((s) => ({
|
|
31284
|
+
...s,
|
|
31285
|
+
items: s.items.filter((item) => !predicate(item)),
|
|
31286
|
+
}));
|
|
31287
|
+
}
|
|
31288
|
+
prependItems(items) {
|
|
31289
|
+
this._state.update((s) => ({
|
|
31290
|
+
...s,
|
|
31291
|
+
items: [...items, ...s.items],
|
|
31292
|
+
}));
|
|
31293
|
+
}
|
|
31294
|
+
appendItems(items) {
|
|
31295
|
+
this._state.update((s) => ({
|
|
31296
|
+
...s,
|
|
31297
|
+
items: [...s.items, ...items],
|
|
31298
|
+
}));
|
|
31299
|
+
}
|
|
31300
|
+
async executeLoad(params) {
|
|
31301
|
+
const result = this.config.loadFn(params);
|
|
31302
|
+
if (isObservable(result)) {
|
|
31303
|
+
return await firstValueFrom(result);
|
|
31304
|
+
}
|
|
31305
|
+
return await result;
|
|
28766
31306
|
}
|
|
28767
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OAuthCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
28768
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: OAuthCallbackComponent, isStandalone: true, selector: "val-oauth-callback", ngImport: i0, template: `
|
|
28769
|
-
<div class="oauth-callback">
|
|
28770
|
-
<div class="oauth-callback__spinner"></div>
|
|
28771
|
-
<p class="oauth-callback__text">{{ message }}</p>
|
|
28772
|
-
</div>
|
|
28773
|
-
`, isInline: true, styles: [".oauth-callback{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.oauth-callback__spinner{width:40px;height:40px;border:3px solid #f3f3f3;border-top:3px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:16px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.oauth-callback__text{color:#666;font-size:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
|
|
28774
31307
|
}
|
|
28775
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OAuthCallbackComponent, decorators: [{
|
|
28776
|
-
type: Component,
|
|
28777
|
-
args: [{ selector: 'val-oauth-callback', standalone: true, imports: [CommonModule], template: `
|
|
28778
|
-
<div class="oauth-callback">
|
|
28779
|
-
<div class="oauth-callback__spinner"></div>
|
|
28780
|
-
<p class="oauth-callback__text">{{ message }}</p>
|
|
28781
|
-
</div>
|
|
28782
|
-
`, styles: [".oauth-callback{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.oauth-callback__spinner{width:40px;height:40px;border:3px solid #f3f3f3;border-top:3px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:16px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.oauth-callback__text{color:#666;font-size:14px}\n"] }]
|
|
28783
|
-
}] });
|
|
28784
|
-
|
|
28785
31308
|
/**
|
|
28786
|
-
*
|
|
28787
|
-
*
|
|
28788
|
-
* Servicio de autenticación reutilizable para aplicaciones Angular.
|
|
28789
|
-
* Proporciona autenticación con AuthV2, MFA, sincronización entre pestañas,
|
|
28790
|
-
* refresh proactivo de tokens, y registro automático de dispositivos para
|
|
28791
|
-
* push notifications.
|
|
31309
|
+
* Servicio para crear controladores de paginacion reutilizables.
|
|
28792
31310
|
*
|
|
28793
31311
|
* @example
|
|
28794
|
-
*
|
|
28795
|
-
*
|
|
28796
|
-
* import { bootstrapApplication } from '@angular/platform-browser';
|
|
28797
|
-
* import { provideValtechAuth, provideValtechFirebase } from 'valtech-components';
|
|
28798
|
-
* import { environment } from './environments/environment';
|
|
31312
|
+
* // En un componente
|
|
31313
|
+
* pagination = inject(PaginationService);
|
|
28799
31314
|
*
|
|
28800
|
-
*
|
|
28801
|
-
*
|
|
28802
|
-
*
|
|
28803
|
-
*
|
|
28804
|
-
* apiUrl: environment.apiUrl,
|
|
28805
|
-
* enableFirebaseIntegration: true,
|
|
28806
|
-
* enableDeviceRegistration: true, // Auto-registra dispositivos para push
|
|
28807
|
-
* }),
|
|
28808
|
-
* ],
|
|
31315
|
+
* usersController = this.pagination.createController<User>({
|
|
31316
|
+
* strategy: 'offset',
|
|
31317
|
+
* pageSize: 20,
|
|
31318
|
+
* loadFn: (params) => this.userService.getUsers(params.page, params.pageSize)
|
|
28809
31319
|
* });
|
|
28810
31320
|
*
|
|
28811
|
-
* // En
|
|
28812
|
-
*
|
|
28813
|
-
*
|
|
28814
|
-
*
|
|
28815
|
-
* { path: 'login', canActivate: [guestGuard], loadComponent: () => import('./login.page') },
|
|
28816
|
-
* { path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('./dashboard.page') },
|
|
28817
|
-
* { path: 'admin', canActivate: [authGuard, permissionGuard('admin:*')], loadComponent: () => import('./admin.page') },
|
|
28818
|
-
* ];
|
|
28819
|
-
*
|
|
28820
|
-
* // En componentes
|
|
28821
|
-
* import { AuthService } from 'valtech-components';
|
|
28822
|
-
*
|
|
28823
|
-
* @Component({...})
|
|
28824
|
-
* export class LoginComponent {
|
|
28825
|
-
* private auth = inject(AuthService);
|
|
28826
|
-
*
|
|
28827
|
-
* async login() {
|
|
28828
|
-
* await firstValueFrom(this.auth.signin({ email, password }));
|
|
28829
|
-
* if (this.auth.mfaPending().required) {
|
|
28830
|
-
* // Mostrar UI de MFA
|
|
28831
|
-
* } else {
|
|
28832
|
-
* this.router.navigate(['/dashboard']);
|
|
28833
|
-
* }
|
|
28834
|
-
* }
|
|
28835
|
-
*
|
|
28836
|
-
* // Habilitar notificaciones push (solicita permisos + registra dispositivo)
|
|
28837
|
-
* async enableNotifications() {
|
|
28838
|
-
* const result = await this.auth.enableNotifications();
|
|
28839
|
-
* if (result.granted) {
|
|
28840
|
-
* console.log('Notificaciones habilitadas');
|
|
28841
|
-
* }
|
|
28842
|
-
* }
|
|
28843
|
-
*
|
|
28844
|
-
* // Verificar estado de permisos
|
|
28845
|
-
* get canReceiveNotifications(): boolean {
|
|
28846
|
-
* return this.auth.getNotificationPermissionState() === 'granted';
|
|
28847
|
-
* }
|
|
31321
|
+
* // En el template
|
|
31322
|
+
* @for (user of usersController.items(); track user.id) {
|
|
31323
|
+
* <app-user-card [user]="user"></app-user-card>
|
|
31324
|
+
* }
|
|
28848
31325
|
*
|
|
28849
|
-
*
|
|
28850
|
-
*
|
|
28851
|
-
* // @if (auth.hasPermission('templates:edit')) { ... }
|
|
31326
|
+
* @if (usersController.hasMore()) {
|
|
31327
|
+
* <ion-button (click)="usersController.loadNext()">Cargar mas</ion-button>
|
|
28852
31328
|
* }
|
|
28853
|
-
* ```
|
|
28854
31329
|
*/
|
|
28855
|
-
|
|
31330
|
+
class PaginationService {
|
|
31331
|
+
/**
|
|
31332
|
+
* Crea un controlador de paginacion para una fuente de datos.
|
|
31333
|
+
*
|
|
31334
|
+
* @param config Configuracion del controlador
|
|
31335
|
+
* @returns Controlador de paginacion
|
|
31336
|
+
*/
|
|
31337
|
+
createController(config) {
|
|
31338
|
+
return new PaginationControllerImpl(config);
|
|
31339
|
+
}
|
|
31340
|
+
/**
|
|
31341
|
+
* Crea un controlador simple para arrays estaticos con paginacion local.
|
|
31342
|
+
*
|
|
31343
|
+
* @param items Array completo de items
|
|
31344
|
+
* @param pageSize Tamano de pagina
|
|
31345
|
+
* @returns Controlador de paginacion
|
|
31346
|
+
*/
|
|
31347
|
+
createLocalController(items, pageSize = 20) {
|
|
31348
|
+
let currentIndex = 0;
|
|
31349
|
+
return this.createController({
|
|
31350
|
+
strategy: 'offset',
|
|
31351
|
+
pageSize,
|
|
31352
|
+
loadFn: async (params) => {
|
|
31353
|
+
const start = params.page * params.pageSize;
|
|
31354
|
+
const end = start + params.pageSize;
|
|
31355
|
+
const pageItems = items.slice(start, end);
|
|
31356
|
+
return {
|
|
31357
|
+
items: pageItems,
|
|
31358
|
+
hasMore: end < items.length,
|
|
31359
|
+
total: items.length,
|
|
31360
|
+
};
|
|
31361
|
+
},
|
|
31362
|
+
});
|
|
31363
|
+
}
|
|
31364
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
31365
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationService, providedIn: 'root' }); }
|
|
31366
|
+
}
|
|
31367
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationService, decorators: [{
|
|
31368
|
+
type: Injectable,
|
|
31369
|
+
args: [{ providedIn: 'root' }]
|
|
31370
|
+
}] });
|
|
31371
|
+
|
|
31372
|
+
// Types
|
|
28856
31373
|
|
|
28857
31374
|
/**
|
|
28858
31375
|
* Ads Loader Service
|
|
@@ -29584,5 +32101,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
29584
32101
|
* Generated bundle index. Do not edit.
|
|
29585
32102
|
*/
|
|
29586
32103
|
|
|
29587
|
-
export { AD_SIZE_MAP, ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, ArticleBuilder, ArticleComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BannerComponent, BaseDefault, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DeviceService, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FunHeaderComponent, GlowCardComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfoComponent, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, LocalStorageService, LocaleService, MODAL_SIZES, MOTION, MenuComponent, MessagingService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RightsFooterComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TabsComponent, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, VALTECH_ADS_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_DEFAULT_CONTENT, VALTECH_FIREBASE_CONFIG, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildPath, collections, createFirebaseConfig, createGlowCardProps, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFirebase, provideValtechI18n, provideValtechPresets, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
|
|
32104
|
+
export { AD_SIZE_MAP, ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, ArticleBuilder, ArticleComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BannerComponent, BaseDefault, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlowCardComponent, GridSkeletonComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfiniteListComponent, InfoComponent, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, MODAL_SIZES, MOTION, MenuComponent, MessagingService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RefresherComponent, RightsFooterComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TableSkeletonComponent, TabsComponent, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, VALTECH_ADS_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_DEFAULT_CONTENT, VALTECH_FIREBASE_CONFIG, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFirebase, provideValtechI18n, provideValtechPresets, provideValtechSkeleton, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
|
|
29588
32105
|
//# sourceMappingURL=valtech-components.mjs.map
|