valtech-components 2.0.417 → 2.0.418
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/public-api.mjs +4 -2
- package/fesm2022/valtech-components.mjs +4 -2607
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +0 -1
- package/esm2022/lib/services/firebase/config.mjs +0 -108
- package/esm2022/lib/services/firebase/firebase.service.mjs +0 -285
- package/esm2022/lib/services/firebase/firestore-collection.mjs +0 -266
- package/esm2022/lib/services/firebase/firestore.service.mjs +0 -508
- package/esm2022/lib/services/firebase/index.mjs +0 -46
- package/esm2022/lib/services/firebase/messaging.service.mjs +0 -503
- package/esm2022/lib/services/firebase/storage.service.mjs +0 -421
- package/esm2022/lib/services/firebase/types.mjs +0 -8
- package/esm2022/lib/services/firebase/utils/path-builder.mjs +0 -195
- package/esm2022/lib/services/firebase/utils/query-builder.mjs +0 -302
- package/lib/services/firebase/config.d.ts +0 -49
- package/lib/services/firebase/firebase.service.d.ts +0 -140
- package/lib/services/firebase/firestore-collection.d.ts +0 -195
- package/lib/services/firebase/firestore.service.d.ts +0 -303
- package/lib/services/firebase/index.d.ts +0 -38
- package/lib/services/firebase/messaging.service.d.ts +0 -254
- package/lib/services/firebase/storage.service.d.ts +0 -204
- package/lib/services/firebase/types.d.ts +0 -281
- package/lib/services/firebase/utils/path-builder.d.ts +0 -132
- package/lib/services/firebase/utils/query-builder.d.ts +0 -210
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { EventEmitter, Component, Input, Output, Injectable, inject, InjectionToken, Inject, ChangeDetectorRef, HostListener, Pipe, ChangeDetectionStrategy, ViewChild, ElementRef
|
|
2
|
+
import { EventEmitter, Component, Input, Output, Injectable, inject, InjectionToken, Inject, ChangeDetectorRef, HostListener, Pipe, ChangeDetectionStrategy, ViewChild, ElementRef } from '@angular/core';
|
|
3
3
|
import * as i2$1 from '@ionic/angular/standalone';
|
|
4
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, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonItem, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonPopover, IonList, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, AlertController, ModalController } from '@ionic/angular/standalone';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
|
-
import { CommonModule, NgStyle, Location, AsyncPipe, NgFor, NgClass
|
|
6
|
+
import { CommonModule, NgStyle, Location, AsyncPipe, NgFor, NgClass } from '@angular/common';
|
|
7
7
|
import { addIcons } from 'ionicons';
|
|
8
8
|
import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globe, list, grid, apps, menu, settings, home, search, person, helpCircle, informationCircle, documentText, notifications, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, checkmark, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline } from 'ionicons/icons';
|
|
9
|
-
import { BehaviorSubject, distinctUntilChanged, shareReplay, map, Subscription, of, combineLatest, filter
|
|
9
|
+
import { BehaviorSubject, distinctUntilChanged, shareReplay, map, Subscription, of, combineLatest, filter } from 'rxjs';
|
|
10
10
|
import * as i1$4 from '@angular/router';
|
|
11
11
|
import { Router, RouterLink, NavigationEnd, RouterOutlet } from '@angular/router';
|
|
12
12
|
import { Browser } from '@capacitor/browser';
|
|
@@ -28,11 +28,6 @@ import 'prismjs/components/prism-typescript';
|
|
|
28
28
|
import 'prismjs/components/prism-bash';
|
|
29
29
|
import Swiper from 'swiper';
|
|
30
30
|
import { Navigation, Pagination, EffectFade, EffectCube, EffectCoverflow, EffectFlip, Autoplay } from 'swiper/modules';
|
|
31
|
-
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
|
|
32
|
-
import { provideAuth, getAuth, connectAuthEmulator, Auth, authState, signInWithCustomToken, signOut } from '@angular/fire/auth';
|
|
33
|
-
import { provideFirestore, getFirestore, connectFirestoreEmulator, enableIndexedDbPersistence, Firestore, doc, getDoc, collection, query as query$1, getDocs, limit, docData, collectionData, serverTimestamp, addDoc, setDoc, updateDoc, deleteDoc, writeBatch, arrayUnion, arrayRemove, increment, where, orderBy, startAfter, startAt, endBefore, endAt, Timestamp } from '@angular/fire/firestore';
|
|
34
|
-
import { provideMessaging, getMessaging, Messaging, getToken, deleteToken, onMessage } from '@angular/fire/messaging';
|
|
35
|
-
import { provideStorage, getStorage, connectStorageEmulator, Storage, ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject, listAll } from '@angular/fire/storage';
|
|
36
31
|
|
|
37
32
|
/**
|
|
38
33
|
* val-avatar
|
|
@@ -22259,2604 +22254,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
22259
22254
|
}]
|
|
22260
22255
|
}] });
|
|
22261
22256
|
|
|
22262
|
-
/**
|
|
22263
|
-
* Firebase Types
|
|
22264
|
-
*
|
|
22265
|
-
* Tipos e interfaces para la integración de Firebase en valtech-components.
|
|
22266
|
-
* Todos los modelos de Firestore deben extender FirestoreDocument.
|
|
22267
|
-
*/
|
|
22268
|
-
|
|
22269
|
-
/**
|
|
22270
|
-
* Firebase Configuration
|
|
22271
|
-
*
|
|
22272
|
-
* Configuración e inicialización de Firebase para aplicaciones Angular.
|
|
22273
|
-
* Usa provideValtechFirebase() en el bootstrap de tu aplicación.
|
|
22274
|
-
*/
|
|
22275
|
-
/**
|
|
22276
|
-
* Token de inyección para la configuración de Firebase.
|
|
22277
|
-
* Usado internamente por los servicios de Firebase.
|
|
22278
|
-
*/
|
|
22279
|
-
const VALTECH_FIREBASE_CONFIG = new InjectionToken('ValtechFirebaseConfig');
|
|
22280
|
-
/**
|
|
22281
|
-
* Provee Firebase a la aplicación Angular.
|
|
22282
|
-
*
|
|
22283
|
-
* @param config - Configuración de Firebase
|
|
22284
|
-
* @returns EnvironmentProviders para usar en bootstrapApplication
|
|
22285
|
-
*
|
|
22286
|
-
* @example
|
|
22287
|
-
* ```typescript
|
|
22288
|
-
* // main.ts
|
|
22289
|
-
* import { bootstrapApplication } from '@angular/platform-browser';
|
|
22290
|
-
* import { provideValtechFirebase } from 'valtech-components';
|
|
22291
|
-
* import { environment } from './environments/environment';
|
|
22292
|
-
*
|
|
22293
|
-
* bootstrapApplication(AppComponent, {
|
|
22294
|
-
* providers: [
|
|
22295
|
-
* provideValtechFirebase({
|
|
22296
|
-
* firebase: environment.firebase,
|
|
22297
|
-
* persistence: true,
|
|
22298
|
-
* emulator: environment.useEmulators ? {
|
|
22299
|
-
* firestore: { host: 'localhost', port: 8080 },
|
|
22300
|
-
* auth: { host: 'localhost', port: 9099 },
|
|
22301
|
-
* storage: { host: 'localhost', port: 9199 },
|
|
22302
|
-
* } : undefined,
|
|
22303
|
-
* }),
|
|
22304
|
-
* ],
|
|
22305
|
-
* });
|
|
22306
|
-
* ```
|
|
22307
|
-
*/
|
|
22308
|
-
function provideValtechFirebase(config) {
|
|
22309
|
-
// Construir array de providers base
|
|
22310
|
-
const providers = [
|
|
22311
|
-
// Guardar configuración para uso en servicios
|
|
22312
|
-
{ provide: VALTECH_FIREBASE_CONFIG, useValue: config },
|
|
22313
|
-
// Inicializar Firebase App
|
|
22314
|
-
provideFirebaseApp(() => initializeApp(config.firebase)),
|
|
22315
|
-
// Firestore con soporte para emuladores y persistencia
|
|
22316
|
-
provideFirestore(() => {
|
|
22317
|
-
const firestore = getFirestore();
|
|
22318
|
-
// Conectar a emulador si está configurado
|
|
22319
|
-
if (config.emulator?.firestore) {
|
|
22320
|
-
connectFirestoreEmulator(firestore, config.emulator.firestore.host, config.emulator.firestore.port);
|
|
22321
|
-
}
|
|
22322
|
-
// Habilitar persistencia offline si está configurada
|
|
22323
|
-
if (config.persistence) {
|
|
22324
|
-
enableIndexedDbPersistence(firestore).catch((err) => {
|
|
22325
|
-
if (err.code === 'failed-precondition') {
|
|
22326
|
-
console.warn('[ValtechFirebase] Persistencia no disponible: múltiples pestañas abiertas');
|
|
22327
|
-
}
|
|
22328
|
-
else if (err.code === 'unimplemented') {
|
|
22329
|
-
console.warn('[ValtechFirebase] Persistencia no soportada en este navegador');
|
|
22330
|
-
}
|
|
22331
|
-
});
|
|
22332
|
-
}
|
|
22333
|
-
return firestore;
|
|
22334
|
-
}),
|
|
22335
|
-
// Auth con soporte para emulador
|
|
22336
|
-
provideAuth(() => {
|
|
22337
|
-
const auth = getAuth();
|
|
22338
|
-
// Conectar a emulador si está configurado
|
|
22339
|
-
if (config.emulator?.auth) {
|
|
22340
|
-
connectAuthEmulator(auth, `http://${config.emulator.auth.host}:${config.emulator.auth.port}`, { disableWarnings: true });
|
|
22341
|
-
}
|
|
22342
|
-
return auth;
|
|
22343
|
-
}),
|
|
22344
|
-
// Storage con soporte para emulador
|
|
22345
|
-
provideStorage(() => {
|
|
22346
|
-
const storage = getStorage();
|
|
22347
|
-
// Conectar a emulador si está configurado
|
|
22348
|
-
if (config.emulator?.storage) {
|
|
22349
|
-
connectStorageEmulator(storage, config.emulator.storage.host, config.emulator.storage.port);
|
|
22350
|
-
}
|
|
22351
|
-
return storage;
|
|
22352
|
-
}),
|
|
22353
|
-
];
|
|
22354
|
-
// Messaging (FCM) - solo si está explícitamente habilitado
|
|
22355
|
-
// Requiere Service Worker configurado, puede congelar la app si no está disponible
|
|
22356
|
-
if (config.enableMessaging) {
|
|
22357
|
-
providers.push(provideMessaging(() => getMessaging()));
|
|
22358
|
-
}
|
|
22359
|
-
return makeEnvironmentProviders(providers);
|
|
22360
|
-
}
|
|
22361
|
-
/**
|
|
22362
|
-
* Verifica si los emuladores están configurados.
|
|
22363
|
-
*
|
|
22364
|
-
* @param config - Configuración de Firebase
|
|
22365
|
-
* @returns true si hay al menos un emulador configurado
|
|
22366
|
-
*/
|
|
22367
|
-
function hasEmulators(config) {
|
|
22368
|
-
return !!(config.emulator?.firestore || config.emulator?.auth || config.emulator?.storage);
|
|
22369
|
-
}
|
|
22370
|
-
|
|
22371
|
-
/**
|
|
22372
|
-
* Firebase Service
|
|
22373
|
-
*
|
|
22374
|
-
* Servicio principal para la autenticación con Firebase usando Custom Tokens.
|
|
22375
|
-
* Permite que usuarios autenticados con tu backend (Cognito, etc.) accedan
|
|
22376
|
-
* a servicios de Firebase (Firestore, Storage, FCM) de manera segura.
|
|
22377
|
-
*/
|
|
22378
|
-
/**
|
|
22379
|
-
* Servicio de autenticación de Firebase.
|
|
22380
|
-
*
|
|
22381
|
-
* Este servicio NO maneja el login de usuarios directamente.
|
|
22382
|
-
* En su lugar, trabaja con Custom Tokens generados por tu backend.
|
|
22383
|
-
*
|
|
22384
|
-
* @example
|
|
22385
|
-
* ```typescript
|
|
22386
|
-
* // Después de autenticarte con tu backend (ej: Cognito)
|
|
22387
|
-
* @Component({...})
|
|
22388
|
-
* export class LoginComponent {
|
|
22389
|
-
* private authService = inject(AuthService); // Tu servicio de auth
|
|
22390
|
-
* private firebase = inject(FirebaseService); // Este servicio
|
|
22391
|
-
*
|
|
22392
|
-
* async login(email: string, password: string) {
|
|
22393
|
-
* // 1. Autenticar con tu backend
|
|
22394
|
-
* const response = await this.authService.login(email, password);
|
|
22395
|
-
*
|
|
22396
|
-
* // 2. El backend devuelve un Firebase Custom Token
|
|
22397
|
-
* if (response.firebaseToken) {
|
|
22398
|
-
* await this.firebase.signInWithCustomToken(response.firebaseToken);
|
|
22399
|
-
* }
|
|
22400
|
-
*
|
|
22401
|
-
* // Ahora el usuario puede acceder a Firestore, Storage, etc.
|
|
22402
|
-
* }
|
|
22403
|
-
*
|
|
22404
|
-
* async logout() {
|
|
22405
|
-
* await this.authService.logout();
|
|
22406
|
-
* await this.firebase.signOut();
|
|
22407
|
-
* }
|
|
22408
|
-
* }
|
|
22409
|
-
* ```
|
|
22410
|
-
*/
|
|
22411
|
-
class FirebaseService {
|
|
22412
|
-
constructor() {
|
|
22413
|
-
this.auth = inject(Auth);
|
|
22414
|
-
this.config = inject(VALTECH_FIREBASE_CONFIG);
|
|
22415
|
-
/** Estado interno de la sesión */
|
|
22416
|
-
this.sessionState = new BehaviorSubject({
|
|
22417
|
-
user: null,
|
|
22418
|
-
isAuthenticated: false,
|
|
22419
|
-
isLoading: true,
|
|
22420
|
-
error: null,
|
|
22421
|
-
});
|
|
22422
|
-
/** Estado actual de la sesión como Observable */
|
|
22423
|
-
this.state$ = this.sessionState.asObservable();
|
|
22424
|
-
/** Usuario actual de Firebase como Observable */
|
|
22425
|
-
this.user$ = authState(this.auth).pipe(map((user) => (user ? this.mapUser(user) : null)), distinctUntilChanged((a, b) => a?.uid === b?.uid));
|
|
22426
|
-
/** Indica si el usuario está autenticado en Firebase */
|
|
22427
|
-
this.isAuthenticated$ = this.user$.pipe(map((user) => !!user), distinctUntilChanged());
|
|
22428
|
-
// Escuchar cambios en el estado de autenticación
|
|
22429
|
-
authState(this.auth).subscribe({
|
|
22430
|
-
next: (user) => {
|
|
22431
|
-
this.sessionState.next({
|
|
22432
|
-
user: user ? this.mapUser(user) : null,
|
|
22433
|
-
isAuthenticated: !!user,
|
|
22434
|
-
isLoading: false,
|
|
22435
|
-
error: null,
|
|
22436
|
-
});
|
|
22437
|
-
},
|
|
22438
|
-
error: (error) => {
|
|
22439
|
-
this.sessionState.next({
|
|
22440
|
-
user: null,
|
|
22441
|
-
isAuthenticated: false,
|
|
22442
|
-
isLoading: false,
|
|
22443
|
-
error,
|
|
22444
|
-
});
|
|
22445
|
-
},
|
|
22446
|
-
});
|
|
22447
|
-
}
|
|
22448
|
-
// ===========================================================================
|
|
22449
|
-
// AUTENTICACIÓN
|
|
22450
|
-
// ===========================================================================
|
|
22451
|
-
/**
|
|
22452
|
-
* Autentica al usuario con un Custom Token generado por el backend.
|
|
22453
|
-
*
|
|
22454
|
-
* @param token - Firebase Custom Token generado por tu backend
|
|
22455
|
-
* @returns UserCredential con la información del usuario
|
|
22456
|
-
* @throws Error si el token es inválido o expiró
|
|
22457
|
-
*
|
|
22458
|
-
* @example
|
|
22459
|
-
* ```typescript
|
|
22460
|
-
* // Después de login exitoso con tu backend
|
|
22461
|
-
* const { firebaseToken } = await backendAuth.login(email, password);
|
|
22462
|
-
* await firebaseService.signInWithCustomToken(firebaseToken);
|
|
22463
|
-
* ```
|
|
22464
|
-
*/
|
|
22465
|
-
async signInWithCustomToken(token) {
|
|
22466
|
-
try {
|
|
22467
|
-
const credential = await signInWithCustomToken(this.auth, token);
|
|
22468
|
-
return credential;
|
|
22469
|
-
}
|
|
22470
|
-
catch (error) {
|
|
22471
|
-
const message = this.getErrorMessage(error);
|
|
22472
|
-
throw new Error(message);
|
|
22473
|
-
}
|
|
22474
|
-
}
|
|
22475
|
-
/**
|
|
22476
|
-
* Cierra la sesión de Firebase.
|
|
22477
|
-
* Llamar junto con el logout de tu sistema de autenticación principal.
|
|
22478
|
-
*
|
|
22479
|
-
* @example
|
|
22480
|
-
* ```typescript
|
|
22481
|
-
* async logout() {
|
|
22482
|
-
* await this.backendAuth.logout(); // Tu auth
|
|
22483
|
-
* await this.firebase.signOut(); // Firebase
|
|
22484
|
-
* }
|
|
22485
|
-
* ```
|
|
22486
|
-
*/
|
|
22487
|
-
async signOut() {
|
|
22488
|
-
try {
|
|
22489
|
-
await signOut(this.auth);
|
|
22490
|
-
}
|
|
22491
|
-
catch (error) {
|
|
22492
|
-
const message = this.getErrorMessage(error);
|
|
22493
|
-
throw new Error(message);
|
|
22494
|
-
}
|
|
22495
|
-
}
|
|
22496
|
-
// ===========================================================================
|
|
22497
|
-
// GETTERS SÍNCRONOS
|
|
22498
|
-
// ===========================================================================
|
|
22499
|
-
/**
|
|
22500
|
-
* Obtiene el usuario actual de Firebase (síncrono).
|
|
22501
|
-
* Retorna null si no hay usuario autenticado.
|
|
22502
|
-
*/
|
|
22503
|
-
get currentUser() {
|
|
22504
|
-
const user = this.auth.currentUser;
|
|
22505
|
-
return user ? this.mapUser(user) : null;
|
|
22506
|
-
}
|
|
22507
|
-
/**
|
|
22508
|
-
* Obtiene el UID del usuario actual.
|
|
22509
|
-
* Retorna null si no hay usuario autenticado.
|
|
22510
|
-
*/
|
|
22511
|
-
get uid() {
|
|
22512
|
-
return this.auth.currentUser?.uid ?? null;
|
|
22513
|
-
}
|
|
22514
|
-
/**
|
|
22515
|
-
* Indica si hay un usuario autenticado actualmente.
|
|
22516
|
-
*/
|
|
22517
|
-
get isAuthenticated() {
|
|
22518
|
-
return !!this.auth.currentUser;
|
|
22519
|
-
}
|
|
22520
|
-
// ===========================================================================
|
|
22521
|
-
// TOKENS
|
|
22522
|
-
// ===========================================================================
|
|
22523
|
-
/**
|
|
22524
|
-
* Obtiene el ID Token de Firebase para el usuario actual.
|
|
22525
|
-
* Útil para validar el usuario en tu backend.
|
|
22526
|
-
*
|
|
22527
|
-
* @param forceRefresh - Si true, fuerza la renovación del token
|
|
22528
|
-
* @returns ID Token o null si no hay usuario
|
|
22529
|
-
*/
|
|
22530
|
-
async getIdToken(forceRefresh = false) {
|
|
22531
|
-
const user = this.auth.currentUser;
|
|
22532
|
-
if (!user)
|
|
22533
|
-
return null;
|
|
22534
|
-
try {
|
|
22535
|
-
return await user.getIdToken(forceRefresh);
|
|
22536
|
-
}
|
|
22537
|
-
catch {
|
|
22538
|
-
return null;
|
|
22539
|
-
}
|
|
22540
|
-
}
|
|
22541
|
-
/**
|
|
22542
|
-
* Obtiene los claims personalizados del token del usuario.
|
|
22543
|
-
* Los claims son establecidos por tu backend al crear el Custom Token.
|
|
22544
|
-
*
|
|
22545
|
-
* @returns Objeto con los claims o vacío si no hay usuario
|
|
22546
|
-
*/
|
|
22547
|
-
async getClaims() {
|
|
22548
|
-
const user = this.auth.currentUser;
|
|
22549
|
-
if (!user)
|
|
22550
|
-
return {};
|
|
22551
|
-
try {
|
|
22552
|
-
const result = await user.getIdTokenResult();
|
|
22553
|
-
return result.claims;
|
|
22554
|
-
}
|
|
22555
|
-
catch {
|
|
22556
|
-
return {};
|
|
22557
|
-
}
|
|
22558
|
-
}
|
|
22559
|
-
/**
|
|
22560
|
-
* Verifica si el usuario tiene un rol específico.
|
|
22561
|
-
* El rol debe estar definido en los claims del Custom Token.
|
|
22562
|
-
*
|
|
22563
|
-
* @param role - Nombre del rol a verificar
|
|
22564
|
-
* @returns true si el usuario tiene el rol
|
|
22565
|
-
*/
|
|
22566
|
-
async hasRole(role) {
|
|
22567
|
-
const claims = await this.getClaims();
|
|
22568
|
-
return claims['role'] === role || (Array.isArray(claims['roles']) && claims['roles'].includes(role));
|
|
22569
|
-
}
|
|
22570
|
-
// ===========================================================================
|
|
22571
|
-
// UTILIDADES
|
|
22572
|
-
// ===========================================================================
|
|
22573
|
-
/**
|
|
22574
|
-
* Espera a que el estado de autenticación esté determinado.
|
|
22575
|
-
* Útil en guards o al inicializar la app.
|
|
22576
|
-
*
|
|
22577
|
-
* @returns Usuario actual o null
|
|
22578
|
-
*/
|
|
22579
|
-
waitForAuth() {
|
|
22580
|
-
return new Promise((resolve) => {
|
|
22581
|
-
const subscription = this.state$.subscribe((state) => {
|
|
22582
|
-
if (!state.isLoading) {
|
|
22583
|
-
subscription.unsubscribe();
|
|
22584
|
-
resolve(state.user);
|
|
22585
|
-
}
|
|
22586
|
-
});
|
|
22587
|
-
});
|
|
22588
|
-
}
|
|
22589
|
-
/**
|
|
22590
|
-
* Obtiene la configuración actual de Firebase.
|
|
22591
|
-
*/
|
|
22592
|
-
getConfig() {
|
|
22593
|
-
return this.config;
|
|
22594
|
-
}
|
|
22595
|
-
/**
|
|
22596
|
-
* Indica si los emuladores están habilitados.
|
|
22597
|
-
*/
|
|
22598
|
-
isUsingEmulators() {
|
|
22599
|
-
return !!(this.config.emulator?.firestore || this.config.emulator?.auth || this.config.emulator?.storage);
|
|
22600
|
-
}
|
|
22601
|
-
// ===========================================================================
|
|
22602
|
-
// MÉTODOS PRIVADOS
|
|
22603
|
-
// ===========================================================================
|
|
22604
|
-
/**
|
|
22605
|
-
* Mapea un User de Firebase a nuestra interface FirebaseUser
|
|
22606
|
-
*/
|
|
22607
|
-
mapUser(user) {
|
|
22608
|
-
return {
|
|
22609
|
-
uid: user.uid,
|
|
22610
|
-
email: user.email,
|
|
22611
|
-
displayName: user.displayName,
|
|
22612
|
-
photoURL: user.photoURL,
|
|
22613
|
-
emailVerified: user.emailVerified,
|
|
22614
|
-
isAnonymous: user.isAnonymous,
|
|
22615
|
-
providerId: user.providerId,
|
|
22616
|
-
};
|
|
22617
|
-
}
|
|
22618
|
-
/**
|
|
22619
|
-
* Convierte errores de Firebase a mensajes en español
|
|
22620
|
-
*/
|
|
22621
|
-
getErrorMessage(error) {
|
|
22622
|
-
if (error instanceof Error) {
|
|
22623
|
-
const code = error.code;
|
|
22624
|
-
switch (code) {
|
|
22625
|
-
case 'auth/invalid-custom-token':
|
|
22626
|
-
return 'Token de autenticación inválido';
|
|
22627
|
-
case 'auth/custom-token-mismatch':
|
|
22628
|
-
return 'El token no corresponde a este proyecto';
|
|
22629
|
-
case 'auth/network-request-failed':
|
|
22630
|
-
return 'Error de conexión. Verifica tu conexión a internet';
|
|
22631
|
-
case 'auth/too-many-requests':
|
|
22632
|
-
return 'Demasiados intentos. Intenta de nuevo más tarde';
|
|
22633
|
-
case 'auth/user-disabled':
|
|
22634
|
-
return 'Esta cuenta ha sido deshabilitada';
|
|
22635
|
-
case 'auth/user-not-found':
|
|
22636
|
-
return 'Usuario no encontrado';
|
|
22637
|
-
default:
|
|
22638
|
-
return error.message || 'Error de autenticación desconocido';
|
|
22639
|
-
}
|
|
22640
|
-
}
|
|
22641
|
-
return 'Error de autenticación desconocido';
|
|
22642
|
-
}
|
|
22643
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirebaseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
22644
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirebaseService, providedIn: 'root' }); }
|
|
22645
|
-
}
|
|
22646
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirebaseService, decorators: [{
|
|
22647
|
-
type: Injectable,
|
|
22648
|
-
args: [{ providedIn: 'root' }]
|
|
22649
|
-
}], ctorParameters: () => [] });
|
|
22650
|
-
|
|
22651
|
-
/**
|
|
22652
|
-
* Path Builder
|
|
22653
|
-
*
|
|
22654
|
-
* Utilidades para construir rutas de Firestore con templates.
|
|
22655
|
-
* Soporta rutas multi-nivel y anidadas.
|
|
22656
|
-
*/
|
|
22657
|
-
/**
|
|
22658
|
-
* Construye una ruta de Firestore reemplazando placeholders.
|
|
22659
|
-
*
|
|
22660
|
-
* @param template - Template con placeholders en formato {param}
|
|
22661
|
-
* @param params - Objeto con los valores a reemplazar
|
|
22662
|
-
* @returns Ruta construida
|
|
22663
|
-
* @throws Error si faltan parámetros requeridos
|
|
22664
|
-
*
|
|
22665
|
-
* @example
|
|
22666
|
-
* ```typescript
|
|
22667
|
-
* // Ruta simple
|
|
22668
|
-
* buildPath('users/{userId}', { userId: 'abc123' });
|
|
22669
|
-
* // => 'users/abc123'
|
|
22670
|
-
*
|
|
22671
|
-
* // Ruta anidada
|
|
22672
|
-
* buildPath('users/{userId}/documents/{docId}', {
|
|
22673
|
-
* userId: 'abc123',
|
|
22674
|
-
* docId: 'doc456'
|
|
22675
|
-
* });
|
|
22676
|
-
* // => 'users/abc123/documents/doc456'
|
|
22677
|
-
*
|
|
22678
|
-
* // Múltiples niveles
|
|
22679
|
-
* buildPath('orgs/{orgId}/teams/{teamId}/members/{memberId}', {
|
|
22680
|
-
* orgId: 'org1',
|
|
22681
|
-
* teamId: 'team2',
|
|
22682
|
-
* memberId: 'member3'
|
|
22683
|
-
* });
|
|
22684
|
-
* // => 'orgs/org1/teams/team2/members/member3'
|
|
22685
|
-
* ```
|
|
22686
|
-
*/
|
|
22687
|
-
function buildPath(template, params) {
|
|
22688
|
-
let result = template;
|
|
22689
|
-
// Encontrar todos los placeholders
|
|
22690
|
-
const placeholders = template.match(/\{([^}]+)\}/g);
|
|
22691
|
-
if (!placeholders) {
|
|
22692
|
-
return template;
|
|
22693
|
-
}
|
|
22694
|
-
for (const placeholder of placeholders) {
|
|
22695
|
-
const key = placeholder.slice(1, -1); // Remover { y }
|
|
22696
|
-
const value = params[key];
|
|
22697
|
-
if (value === undefined || value === null) {
|
|
22698
|
-
throw new Error(`Parámetro requerido '${key}' no proporcionado para la ruta: ${template}`);
|
|
22699
|
-
}
|
|
22700
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
22701
|
-
throw new Error(`El parámetro '${key}' debe ser un string no vacío`);
|
|
22702
|
-
}
|
|
22703
|
-
// Validar que no contenga caracteres inválidos para Firestore
|
|
22704
|
-
if (value.includes('/')) {
|
|
22705
|
-
throw new Error(`El parámetro '${key}' no puede contener '/'`);
|
|
22706
|
-
}
|
|
22707
|
-
result = result.replace(placeholder, value);
|
|
22708
|
-
}
|
|
22709
|
-
return result;
|
|
22710
|
-
}
|
|
22711
|
-
/**
|
|
22712
|
-
* Extrae los nombres de los parámetros de un template de ruta.
|
|
22713
|
-
*
|
|
22714
|
-
* @param template - Template de ruta
|
|
22715
|
-
* @returns Array con los nombres de los parámetros
|
|
22716
|
-
*
|
|
22717
|
-
* @example
|
|
22718
|
-
* ```typescript
|
|
22719
|
-
* extractParams('users/{userId}/documents/{docId}');
|
|
22720
|
-
* // => ['userId', 'docId']
|
|
22721
|
-
* ```
|
|
22722
|
-
*/
|
|
22723
|
-
function extractPathParams(template) {
|
|
22724
|
-
const matches = template.match(/\{([^}]+)\}/g);
|
|
22725
|
-
if (!matches)
|
|
22726
|
-
return [];
|
|
22727
|
-
return matches.map((m) => m.slice(1, -1));
|
|
22728
|
-
}
|
|
22729
|
-
/**
|
|
22730
|
-
* Valida que una ruta de Firestore sea válida.
|
|
22731
|
-
*
|
|
22732
|
-
* @param path - Ruta a validar
|
|
22733
|
-
* @returns true si la ruta es válida
|
|
22734
|
-
*
|
|
22735
|
-
* @example
|
|
22736
|
-
* ```typescript
|
|
22737
|
-
* isValidPath('users/abc123'); // true
|
|
22738
|
-
* isValidPath('users/abc123/documents'); // true
|
|
22739
|
-
* isValidPath('users//documents'); // false (segmento vacío)
|
|
22740
|
-
* isValidPath(''); // false (vacío)
|
|
22741
|
-
* ```
|
|
22742
|
-
*/
|
|
22743
|
-
function isValidPath(path) {
|
|
22744
|
-
if (!path || path.trim() === '')
|
|
22745
|
-
return false;
|
|
22746
|
-
const segments = path.split('/');
|
|
22747
|
-
// No puede tener segmentos vacíos
|
|
22748
|
-
if (segments.some((s) => s.trim() === ''))
|
|
22749
|
-
return false;
|
|
22750
|
-
// No puede empezar o terminar con /
|
|
22751
|
-
if (path.startsWith('/') || path.endsWith('/'))
|
|
22752
|
-
return false;
|
|
22753
|
-
return true;
|
|
22754
|
-
}
|
|
22755
|
-
/**
|
|
22756
|
-
* Obtiene la ruta de la colección padre de un documento.
|
|
22757
|
-
*
|
|
22758
|
-
* @param documentPath - Ruta completa del documento
|
|
22759
|
-
* @returns Ruta de la colección padre
|
|
22760
|
-
*
|
|
22761
|
-
* @example
|
|
22762
|
-
* ```typescript
|
|
22763
|
-
* getCollectionPath('users/abc123');
|
|
22764
|
-
* // => 'users'
|
|
22765
|
-
*
|
|
22766
|
-
* getCollectionPath('users/abc123/documents/doc456');
|
|
22767
|
-
* // => 'users/abc123/documents'
|
|
22768
|
-
* ```
|
|
22769
|
-
*/
|
|
22770
|
-
function getCollectionPath(documentPath) {
|
|
22771
|
-
const segments = documentPath.split('/');
|
|
22772
|
-
if (segments.length < 2) {
|
|
22773
|
-
throw new Error(`Ruta de documento inválida: ${documentPath}`);
|
|
22774
|
-
}
|
|
22775
|
-
return segments.slice(0, -1).join('/');
|
|
22776
|
-
}
|
|
22777
|
-
/**
|
|
22778
|
-
* Obtiene el ID del documento de una ruta.
|
|
22779
|
-
*
|
|
22780
|
-
* @param documentPath - Ruta completa del documento
|
|
22781
|
-
* @returns ID del documento
|
|
22782
|
-
*
|
|
22783
|
-
* @example
|
|
22784
|
-
* ```typescript
|
|
22785
|
-
* getDocumentId('users/abc123');
|
|
22786
|
-
* // => 'abc123'
|
|
22787
|
-
*
|
|
22788
|
-
* getDocumentId('users/abc123/documents/doc456');
|
|
22789
|
-
* // => 'doc456'
|
|
22790
|
-
* ```
|
|
22791
|
-
*/
|
|
22792
|
-
function getDocumentId(documentPath) {
|
|
22793
|
-
const segments = documentPath.split('/');
|
|
22794
|
-
if (segments.length < 2 || segments.length % 2 !== 0) {
|
|
22795
|
-
throw new Error(`Ruta de documento inválida: ${documentPath}`);
|
|
22796
|
-
}
|
|
22797
|
-
return segments[segments.length - 1];
|
|
22798
|
-
}
|
|
22799
|
-
/**
|
|
22800
|
-
* Verifica si una ruta apunta a un documento (número par de segmentos).
|
|
22801
|
-
*
|
|
22802
|
-
* @param path - Ruta a verificar
|
|
22803
|
-
* @returns true si es una ruta de documento
|
|
22804
|
-
*
|
|
22805
|
-
* @example
|
|
22806
|
-
* ```typescript
|
|
22807
|
-
* isDocumentPath('users/abc123'); // true
|
|
22808
|
-
* isDocumentPath('users'); // false (colección)
|
|
22809
|
-
* isDocumentPath('users/abc123/documents'); // false (colección)
|
|
22810
|
-
* ```
|
|
22811
|
-
*/
|
|
22812
|
-
function isDocumentPath(path) {
|
|
22813
|
-
const segments = path.split('/').filter((s) => s.trim() !== '');
|
|
22814
|
-
return segments.length > 0 && segments.length % 2 === 0;
|
|
22815
|
-
}
|
|
22816
|
-
/**
|
|
22817
|
-
* Verifica si una ruta apunta a una colección (número impar de segmentos).
|
|
22818
|
-
*
|
|
22819
|
-
* @param path - Ruta a verificar
|
|
22820
|
-
* @returns true si es una ruta de colección
|
|
22821
|
-
*/
|
|
22822
|
-
function isCollectionPath(path) {
|
|
22823
|
-
const segments = path.split('/').filter((s) => s.trim() !== '');
|
|
22824
|
-
return segments.length > 0 && segments.length % 2 !== 0;
|
|
22825
|
-
}
|
|
22826
|
-
/**
|
|
22827
|
-
* Combina una ruta base con segmentos adicionales.
|
|
22828
|
-
*
|
|
22829
|
-
* @param basePath - Ruta base
|
|
22830
|
-
* @param segments - Segmentos adicionales
|
|
22831
|
-
* @returns Ruta combinada
|
|
22832
|
-
*
|
|
22833
|
-
* @example
|
|
22834
|
-
* ```typescript
|
|
22835
|
-
* joinPath('users', 'abc123', 'documents');
|
|
22836
|
-
* // => 'users/abc123/documents'
|
|
22837
|
-
* ```
|
|
22838
|
-
*/
|
|
22839
|
-
function joinPath(...segments) {
|
|
22840
|
-
return segments
|
|
22841
|
-
.filter((s) => s && s.trim() !== '')
|
|
22842
|
-
.map((s) => s.replace(/^\/+|\/+$/g, '')) // Remover / al inicio y final
|
|
22843
|
-
.join('/');
|
|
22844
|
-
}
|
|
22845
|
-
|
|
22846
|
-
/**
|
|
22847
|
-
* Firestore Service
|
|
22848
|
-
*
|
|
22849
|
-
* Servicio genérico para operaciones CRUD en Firestore.
|
|
22850
|
-
* Soporta lecturas one-time, subscripciones real-time, paginación y queries complejas.
|
|
22851
|
-
*/
|
|
22852
|
-
/**
|
|
22853
|
-
* Servicio para operaciones CRUD en Firestore.
|
|
22854
|
-
*
|
|
22855
|
-
* @example
|
|
22856
|
-
* ```typescript
|
|
22857
|
-
* interface User extends FirestoreDocument {
|
|
22858
|
-
* name: string;
|
|
22859
|
-
* email: string;
|
|
22860
|
-
* role: 'admin' | 'user';
|
|
22861
|
-
* }
|
|
22862
|
-
*
|
|
22863
|
-
* @Component({...})
|
|
22864
|
-
* export class UsersComponent {
|
|
22865
|
-
* private firestore = inject(FirestoreService);
|
|
22866
|
-
*
|
|
22867
|
-
* // Lectura one-time
|
|
22868
|
-
* async loadUser(id: string) {
|
|
22869
|
-
* const user = await this.firestore.getDoc<User>('users', id);
|
|
22870
|
-
* }
|
|
22871
|
-
*
|
|
22872
|
-
* // Subscripción real-time
|
|
22873
|
-
* users$ = this.firestore.collectionChanges<User>('users', {
|
|
22874
|
-
* where: [{ field: 'role', operator: '==', value: 'admin' }],
|
|
22875
|
-
* orderBy: [{ field: 'name', direction: 'asc' }]
|
|
22876
|
-
* });
|
|
22877
|
-
*
|
|
22878
|
-
* // Crear documento
|
|
22879
|
-
* async createUser(data: Omit<User, 'id'>) {
|
|
22880
|
-
* const user = await this.firestore.addDoc<User>('users', data);
|
|
22881
|
-
* }
|
|
22882
|
-
* }
|
|
22883
|
-
* ```
|
|
22884
|
-
*/
|
|
22885
|
-
class FirestoreService {
|
|
22886
|
-
constructor() {
|
|
22887
|
-
this.firestore = inject(Firestore);
|
|
22888
|
-
}
|
|
22889
|
-
// ===========================================================================
|
|
22890
|
-
// LECTURAS ONE-TIME (Promise)
|
|
22891
|
-
// ===========================================================================
|
|
22892
|
-
/**
|
|
22893
|
-
* Obtiene un documento por ID (lectura única).
|
|
22894
|
-
*
|
|
22895
|
-
* @param collectionPath - Ruta de la colección
|
|
22896
|
-
* @param docId - ID del documento
|
|
22897
|
-
* @returns Documento o null si no existe
|
|
22898
|
-
*
|
|
22899
|
-
* @example
|
|
22900
|
-
* ```typescript
|
|
22901
|
-
* const user = await firestoreService.getDoc<User>('users', 'abc123');
|
|
22902
|
-
* if (user) {
|
|
22903
|
-
* console.log(user.name);
|
|
22904
|
-
* }
|
|
22905
|
-
* ```
|
|
22906
|
-
*/
|
|
22907
|
-
async getDoc(collectionPath, docId) {
|
|
22908
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
22909
|
-
const snapshot = await getDoc(docRef);
|
|
22910
|
-
if (!snapshot.exists()) {
|
|
22911
|
-
return null;
|
|
22912
|
-
}
|
|
22913
|
-
return this.mapDocument(snapshot);
|
|
22914
|
-
}
|
|
22915
|
-
/**
|
|
22916
|
-
* Obtiene múltiples documentos con opciones de query.
|
|
22917
|
-
*
|
|
22918
|
-
* @param collectionPath - Ruta de la colección
|
|
22919
|
-
* @param options - Opciones de query (where, orderBy, limit)
|
|
22920
|
-
* @returns Array de documentos
|
|
22921
|
-
*
|
|
22922
|
-
* @example
|
|
22923
|
-
* ```typescript
|
|
22924
|
-
* // Todos los usuarios activos ordenados por nombre
|
|
22925
|
-
* const users = await firestoreService.getDocs<User>('users', {
|
|
22926
|
-
* where: [{ field: 'active', operator: '==', value: true }],
|
|
22927
|
-
* orderBy: [{ field: 'name', direction: 'asc' }],
|
|
22928
|
-
* limit: 50
|
|
22929
|
-
* });
|
|
22930
|
-
* ```
|
|
22931
|
-
*/
|
|
22932
|
-
async getDocs(collectionPath, options) {
|
|
22933
|
-
const collectionRef = collection(this.firestore, collectionPath);
|
|
22934
|
-
const constraints = this.buildQueryConstraints(options);
|
|
22935
|
-
const q = query$1(collectionRef, ...constraints);
|
|
22936
|
-
const snapshot = await getDocs(q);
|
|
22937
|
-
return snapshot.docs.map((doc) => this.mapDocument(doc));
|
|
22938
|
-
}
|
|
22939
|
-
/**
|
|
22940
|
-
* Obtiene documentos con paginación basada en cursores.
|
|
22941
|
-
*
|
|
22942
|
-
* @param collectionPath - Ruta de la colección
|
|
22943
|
-
* @param options - Opciones de query (debe incluir limit)
|
|
22944
|
-
* @returns Resultado paginado con cursor para la siguiente página
|
|
22945
|
-
*
|
|
22946
|
-
* @example
|
|
22947
|
-
* ```typescript
|
|
22948
|
-
* // Primera página
|
|
22949
|
-
* const page1 = await firestoreService.getPaginated<User>('users', {
|
|
22950
|
-
* orderBy: [{ field: 'createdAt', direction: 'desc' }],
|
|
22951
|
-
* limit: 10
|
|
22952
|
-
* });
|
|
22953
|
-
*
|
|
22954
|
-
* // Siguiente página
|
|
22955
|
-
* if (page1.hasMore) {
|
|
22956
|
-
* const page2 = await firestoreService.getPaginated<User>('users', {
|
|
22957
|
-
* orderBy: [{ field: 'createdAt', direction: 'desc' }],
|
|
22958
|
-
* limit: 10,
|
|
22959
|
-
* startAfter: page1.lastDoc
|
|
22960
|
-
* });
|
|
22961
|
-
* }
|
|
22962
|
-
* ```
|
|
22963
|
-
*/
|
|
22964
|
-
async getPaginated(collectionPath, options) {
|
|
22965
|
-
const collectionRef = collection(this.firestore, collectionPath);
|
|
22966
|
-
const constraints = this.buildQueryConstraints(options);
|
|
22967
|
-
// Pedir uno más para saber si hay más páginas
|
|
22968
|
-
const q = query$1(collectionRef, ...constraints, limit(options.limit + 1));
|
|
22969
|
-
const snapshot = await getDocs(q);
|
|
22970
|
-
const docs = snapshot.docs;
|
|
22971
|
-
const hasMore = docs.length > options.limit;
|
|
22972
|
-
// Si hay más, remover el documento extra
|
|
22973
|
-
const resultDocs = hasMore ? docs.slice(0, -1) : docs;
|
|
22974
|
-
const lastDoc = resultDocs.length > 0 ? resultDocs[resultDocs.length - 1] : null;
|
|
22975
|
-
return {
|
|
22976
|
-
data: resultDocs.map((doc) => this.mapDocument(doc)),
|
|
22977
|
-
hasMore,
|
|
22978
|
-
lastDoc,
|
|
22979
|
-
};
|
|
22980
|
-
}
|
|
22981
|
-
/**
|
|
22982
|
-
* Verifica si un documento existe.
|
|
22983
|
-
*
|
|
22984
|
-
* @param collectionPath - Ruta de la colección
|
|
22985
|
-
* @param docId - ID del documento
|
|
22986
|
-
* @returns true si el documento existe
|
|
22987
|
-
*/
|
|
22988
|
-
async exists(collectionPath, docId) {
|
|
22989
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
22990
|
-
const snapshot = await getDoc(docRef);
|
|
22991
|
-
return snapshot.exists();
|
|
22992
|
-
}
|
|
22993
|
-
// ===========================================================================
|
|
22994
|
-
// SUBSCRIPCIONES REAL-TIME (Observable)
|
|
22995
|
-
// ===========================================================================
|
|
22996
|
-
/**
|
|
22997
|
-
* Suscribe a cambios de un documento (real-time).
|
|
22998
|
-
*
|
|
22999
|
-
* @param collectionPath - Ruta de la colección
|
|
23000
|
-
* @param docId - ID del documento
|
|
23001
|
-
* @returns Observable que emite cuando el documento cambia
|
|
23002
|
-
*
|
|
23003
|
-
* @example
|
|
23004
|
-
* ```typescript
|
|
23005
|
-
* // En el componente
|
|
23006
|
-
* user$ = this.firestoreService.docChanges<User>('users', this.userId);
|
|
23007
|
-
*
|
|
23008
|
-
* // En el template
|
|
23009
|
-
* @if (user$ | async; as user) {
|
|
23010
|
-
* <p>{{ user.name }}</p>
|
|
23011
|
-
* }
|
|
23012
|
-
* ```
|
|
23013
|
-
*/
|
|
23014
|
-
docChanges(collectionPath, docId) {
|
|
23015
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23016
|
-
return docData(docRef, { idField: 'id' }).pipe(map((data) => {
|
|
23017
|
-
if (!data)
|
|
23018
|
-
return null;
|
|
23019
|
-
return this.convertTimestamps(data);
|
|
23020
|
-
}));
|
|
23021
|
-
}
|
|
23022
|
-
/**
|
|
23023
|
-
* Suscribe a cambios de una colección (real-time).
|
|
23024
|
-
*
|
|
23025
|
-
* @param collectionPath - Ruta de la colección
|
|
23026
|
-
* @param options - Opciones de query
|
|
23027
|
-
* @returns Observable que emite cuando la colección cambia
|
|
23028
|
-
*
|
|
23029
|
-
* @example
|
|
23030
|
-
* ```typescript
|
|
23031
|
-
* // Usuarios activos en tiempo real
|
|
23032
|
-
* activeUsers$ = this.firestoreService.collectionChanges<User>('users', {
|
|
23033
|
-
* where: [{ field: 'status', operator: '==', value: 'online' }]
|
|
23034
|
-
* });
|
|
23035
|
-
* ```
|
|
23036
|
-
*/
|
|
23037
|
-
collectionChanges(collectionPath, options) {
|
|
23038
|
-
const collectionRef = collection(this.firestore, collectionPath);
|
|
23039
|
-
const constraints = this.buildQueryConstraints(options);
|
|
23040
|
-
const q = query$1(collectionRef, ...constraints);
|
|
23041
|
-
return collectionData(q, { idField: 'id' }).pipe(map((docs) => docs.map((doc) => this.convertTimestamps(doc))));
|
|
23042
|
-
}
|
|
23043
|
-
// ===========================================================================
|
|
23044
|
-
// ESCRITURA
|
|
23045
|
-
// ===========================================================================
|
|
23046
|
-
/**
|
|
23047
|
-
* Agrega un documento con ID auto-generado.
|
|
23048
|
-
*
|
|
23049
|
-
* @param collectionPath - Ruta de la colección
|
|
23050
|
-
* @param data - Datos del documento (sin id, createdAt, updatedAt)
|
|
23051
|
-
* @returns Documento creado con su ID
|
|
23052
|
-
*
|
|
23053
|
-
* @example
|
|
23054
|
-
* ```typescript
|
|
23055
|
-
* const newUser = await firestoreService.addDoc<User>('users', {
|
|
23056
|
-
* name: 'John Doe',
|
|
23057
|
-
* email: 'john@example.com',
|
|
23058
|
-
* role: 'user'
|
|
23059
|
-
* });
|
|
23060
|
-
* console.log('Created user with ID:', newUser.id);
|
|
23061
|
-
* ```
|
|
23062
|
-
*/
|
|
23063
|
-
async addDoc(collectionPath, data) {
|
|
23064
|
-
const collectionRef = collection(this.firestore, collectionPath);
|
|
23065
|
-
const timestamp = serverTimestamp();
|
|
23066
|
-
const docData = {
|
|
23067
|
-
...data,
|
|
23068
|
-
createdAt: timestamp,
|
|
23069
|
-
updatedAt: timestamp,
|
|
23070
|
-
};
|
|
23071
|
-
const docRef = await addDoc(collectionRef, docData);
|
|
23072
|
-
// Obtener el documento creado para retornarlo con timestamps resueltos
|
|
23073
|
-
const snapshot = await getDoc(docRef);
|
|
23074
|
-
return this.mapDocument(snapshot);
|
|
23075
|
-
}
|
|
23076
|
-
/**
|
|
23077
|
-
* Crea o sobrescribe un documento con ID específico.
|
|
23078
|
-
*
|
|
23079
|
-
* @param collectionPath - Ruta de la colección
|
|
23080
|
-
* @param docId - ID del documento
|
|
23081
|
-
* @param data - Datos del documento
|
|
23082
|
-
* @param options - Opciones (merge: true para merge en lugar de sobrescribir)
|
|
23083
|
-
*
|
|
23084
|
-
* @example
|
|
23085
|
-
* ```typescript
|
|
23086
|
-
* // Sobrescribir completamente
|
|
23087
|
-
* await firestoreService.setDoc<User>('users', 'user123', userData);
|
|
23088
|
-
*
|
|
23089
|
-
* // Merge con datos existentes
|
|
23090
|
-
* await firestoreService.setDoc<User>('users', 'user123', { name: 'New Name' }, { merge: true });
|
|
23091
|
-
* ```
|
|
23092
|
-
*/
|
|
23093
|
-
async setDoc(collectionPath, docId, data, options) {
|
|
23094
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23095
|
-
const timestamp = serverTimestamp();
|
|
23096
|
-
const docData = {
|
|
23097
|
-
...data,
|
|
23098
|
-
updatedAt: timestamp,
|
|
23099
|
-
...(options?.merge ? {} : { createdAt: timestamp }),
|
|
23100
|
-
};
|
|
23101
|
-
await setDoc(docRef, docData, { merge: options?.merge ?? false });
|
|
23102
|
-
}
|
|
23103
|
-
/**
|
|
23104
|
-
* Actualiza campos específicos de un documento.
|
|
23105
|
-
*
|
|
23106
|
-
* @param collectionPath - Ruta de la colección
|
|
23107
|
-
* @param docId - ID del documento
|
|
23108
|
-
* @param data - Campos a actualizar
|
|
23109
|
-
*
|
|
23110
|
-
* @example
|
|
23111
|
-
* ```typescript
|
|
23112
|
-
* await firestoreService.updateDoc<User>('users', 'user123', {
|
|
23113
|
-
* name: 'Updated Name',
|
|
23114
|
-
* lastLogin: new Date()
|
|
23115
|
-
* });
|
|
23116
|
-
* ```
|
|
23117
|
-
*/
|
|
23118
|
-
async updateDoc(collectionPath, docId, data) {
|
|
23119
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23120
|
-
await updateDoc(docRef, {
|
|
23121
|
-
...data,
|
|
23122
|
-
updatedAt: serverTimestamp(),
|
|
23123
|
-
});
|
|
23124
|
-
}
|
|
23125
|
-
/**
|
|
23126
|
-
* Elimina un documento.
|
|
23127
|
-
*
|
|
23128
|
-
* @param collectionPath - Ruta de la colección
|
|
23129
|
-
* @param docId - ID del documento
|
|
23130
|
-
*
|
|
23131
|
-
* @example
|
|
23132
|
-
* ```typescript
|
|
23133
|
-
* await firestoreService.deleteDoc('users', 'user123');
|
|
23134
|
-
* ```
|
|
23135
|
-
*/
|
|
23136
|
-
async deleteDoc(collectionPath, docId) {
|
|
23137
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23138
|
-
await deleteDoc(docRef);
|
|
23139
|
-
}
|
|
23140
|
-
// ===========================================================================
|
|
23141
|
-
// OPERACIONES EN LOTE
|
|
23142
|
-
// ===========================================================================
|
|
23143
|
-
/**
|
|
23144
|
-
* Ejecuta múltiples operaciones de escritura de forma atómica.
|
|
23145
|
-
*
|
|
23146
|
-
* @param operations - Función que recibe el batch y agrega operaciones
|
|
23147
|
-
*
|
|
23148
|
-
* @example
|
|
23149
|
-
* ```typescript
|
|
23150
|
-
* await firestoreService.batch((batch) => {
|
|
23151
|
-
* batch.set('users/user1', { name: 'User 1' });
|
|
23152
|
-
* batch.update('users/user2', { status: 'inactive' });
|
|
23153
|
-
* batch.delete('users/user3');
|
|
23154
|
-
* });
|
|
23155
|
-
* ```
|
|
23156
|
-
*/
|
|
23157
|
-
async batch(operations) {
|
|
23158
|
-
const batch = writeBatch(this.firestore);
|
|
23159
|
-
const batchApi = {
|
|
23160
|
-
set: (path, data) => {
|
|
23161
|
-
const [collectionPath, docId] = this.splitPath(path);
|
|
23162
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23163
|
-
batch.set(docRef, {
|
|
23164
|
-
...data,
|
|
23165
|
-
createdAt: serverTimestamp(),
|
|
23166
|
-
updatedAt: serverTimestamp(),
|
|
23167
|
-
});
|
|
23168
|
-
},
|
|
23169
|
-
update: (path, data) => {
|
|
23170
|
-
const [collectionPath, docId] = this.splitPath(path);
|
|
23171
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23172
|
-
batch.update(docRef, {
|
|
23173
|
-
...data,
|
|
23174
|
-
updatedAt: serverTimestamp(),
|
|
23175
|
-
});
|
|
23176
|
-
},
|
|
23177
|
-
delete: (path) => {
|
|
23178
|
-
const [collectionPath, docId] = this.splitPath(path);
|
|
23179
|
-
const docRef = doc(this.firestore, collectionPath, docId);
|
|
23180
|
-
batch.delete(docRef);
|
|
23181
|
-
},
|
|
23182
|
-
};
|
|
23183
|
-
operations(batchApi);
|
|
23184
|
-
await batch.commit();
|
|
23185
|
-
}
|
|
23186
|
-
// ===========================================================================
|
|
23187
|
-
// UTILIDADES
|
|
23188
|
-
// ===========================================================================
|
|
23189
|
-
/**
|
|
23190
|
-
* Construye una ruta a partir de un template.
|
|
23191
|
-
*
|
|
23192
|
-
* @param template - Template con placeholders {param}
|
|
23193
|
-
* @param params - Valores para los placeholders
|
|
23194
|
-
* @returns Ruta construida
|
|
23195
|
-
*
|
|
23196
|
-
* @example
|
|
23197
|
-
* ```typescript
|
|
23198
|
-
* const path = firestoreService.buildPath('users/{userId}/documents/{docId}', {
|
|
23199
|
-
* userId: 'user123',
|
|
23200
|
-
* docId: 'doc456'
|
|
23201
|
-
* });
|
|
23202
|
-
* // => 'users/user123/documents/doc456'
|
|
23203
|
-
* ```
|
|
23204
|
-
*/
|
|
23205
|
-
buildPath(template, params) {
|
|
23206
|
-
return buildPath(template, params);
|
|
23207
|
-
}
|
|
23208
|
-
/**
|
|
23209
|
-
* Genera un ID único para un documento (sin crearlo).
|
|
23210
|
-
*
|
|
23211
|
-
* @param collectionPath - Ruta de la colección
|
|
23212
|
-
* @returns ID único generado por Firestore
|
|
23213
|
-
*/
|
|
23214
|
-
generateId(collectionPath) {
|
|
23215
|
-
const collectionRef = collection(this.firestore, collectionPath);
|
|
23216
|
-
return doc(collectionRef).id;
|
|
23217
|
-
}
|
|
23218
|
-
/**
|
|
23219
|
-
* Retorna un valor de timestamp del servidor.
|
|
23220
|
-
* Usar en campos de fecha para que Firestore asigne el timestamp.
|
|
23221
|
-
*/
|
|
23222
|
-
serverTimestamp() {
|
|
23223
|
-
return serverTimestamp();
|
|
23224
|
-
}
|
|
23225
|
-
/**
|
|
23226
|
-
* Retorna un valor para agregar elementos a un array.
|
|
23227
|
-
*
|
|
23228
|
-
* @example
|
|
23229
|
-
* ```typescript
|
|
23230
|
-
* await firestoreService.updateDoc('users', 'user123', {
|
|
23231
|
-
* tags: firestoreService.arrayUnion('new-tag')
|
|
23232
|
-
* });
|
|
23233
|
-
* ```
|
|
23234
|
-
*/
|
|
23235
|
-
arrayUnion(...elements) {
|
|
23236
|
-
return arrayUnion(...elements);
|
|
23237
|
-
}
|
|
23238
|
-
/**
|
|
23239
|
-
* Retorna un valor para remover elementos de un array.
|
|
23240
|
-
*/
|
|
23241
|
-
arrayRemove(...elements) {
|
|
23242
|
-
return arrayRemove(...elements);
|
|
23243
|
-
}
|
|
23244
|
-
/**
|
|
23245
|
-
* Retorna un valor para incrementar un campo numérico.
|
|
23246
|
-
*
|
|
23247
|
-
* @example
|
|
23248
|
-
* ```typescript
|
|
23249
|
-
* await firestoreService.updateDoc('users', 'user123', {
|
|
23250
|
-
* loginCount: firestoreService.increment(1)
|
|
23251
|
-
* });
|
|
23252
|
-
* ```
|
|
23253
|
-
*/
|
|
23254
|
-
increment(n) {
|
|
23255
|
-
return increment(n);
|
|
23256
|
-
}
|
|
23257
|
-
// ===========================================================================
|
|
23258
|
-
// MÉTODOS PRIVADOS
|
|
23259
|
-
// ===========================================================================
|
|
23260
|
-
/**
|
|
23261
|
-
* Construye los QueryConstraints a partir de QueryOptions
|
|
23262
|
-
*/
|
|
23263
|
-
buildQueryConstraints(options) {
|
|
23264
|
-
const constraints = [];
|
|
23265
|
-
if (!options)
|
|
23266
|
-
return constraints;
|
|
23267
|
-
// Where clauses
|
|
23268
|
-
if (options.where) {
|
|
23269
|
-
for (const clause of options.where) {
|
|
23270
|
-
constraints.push(where(clause.field, clause.operator, clause.value));
|
|
23271
|
-
}
|
|
23272
|
-
}
|
|
23273
|
-
// OrderBy clauses
|
|
23274
|
-
if (options.orderBy) {
|
|
23275
|
-
for (const clause of options.orderBy) {
|
|
23276
|
-
constraints.push(orderBy(clause.field, clause.direction));
|
|
23277
|
-
}
|
|
23278
|
-
}
|
|
23279
|
-
// Cursors para paginación
|
|
23280
|
-
if (options.startAfter) {
|
|
23281
|
-
constraints.push(startAfter(options.startAfter));
|
|
23282
|
-
}
|
|
23283
|
-
if (options.startAt) {
|
|
23284
|
-
constraints.push(startAt(options.startAt));
|
|
23285
|
-
}
|
|
23286
|
-
if (options.endBefore) {
|
|
23287
|
-
constraints.push(endBefore(options.endBefore));
|
|
23288
|
-
}
|
|
23289
|
-
if (options.endAt) {
|
|
23290
|
-
constraints.push(endAt(options.endAt));
|
|
23291
|
-
}
|
|
23292
|
-
// Limit (se agrega al final)
|
|
23293
|
-
if (options.limit) {
|
|
23294
|
-
constraints.push(limit(options.limit));
|
|
23295
|
-
}
|
|
23296
|
-
return constraints;
|
|
23297
|
-
}
|
|
23298
|
-
/**
|
|
23299
|
-
* Mapea un DocumentSnapshot a nuestro tipo
|
|
23300
|
-
*/
|
|
23301
|
-
mapDocument(snapshot) {
|
|
23302
|
-
const data = snapshot.data();
|
|
23303
|
-
if (!data) {
|
|
23304
|
-
throw new Error('Documento no tiene datos');
|
|
23305
|
-
}
|
|
23306
|
-
return {
|
|
23307
|
-
id: snapshot.id,
|
|
23308
|
-
...this.convertTimestamps(data),
|
|
23309
|
-
};
|
|
23310
|
-
}
|
|
23311
|
-
/**
|
|
23312
|
-
* Convierte Timestamps de Firestore a Date de JavaScript
|
|
23313
|
-
*/
|
|
23314
|
-
convertTimestamps(data) {
|
|
23315
|
-
const result = {};
|
|
23316
|
-
for (const [key, value] of Object.entries(data)) {
|
|
23317
|
-
if (value instanceof Timestamp) {
|
|
23318
|
-
result[key] = value.toDate();
|
|
23319
|
-
}
|
|
23320
|
-
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
23321
|
-
result[key] = this.convertTimestamps(value);
|
|
23322
|
-
}
|
|
23323
|
-
else {
|
|
23324
|
-
result[key] = value;
|
|
23325
|
-
}
|
|
23326
|
-
}
|
|
23327
|
-
return result;
|
|
23328
|
-
}
|
|
23329
|
-
/**
|
|
23330
|
-
* Divide una ruta de documento en colección e ID
|
|
23331
|
-
*/
|
|
23332
|
-
splitPath(path) {
|
|
23333
|
-
const segments = path.split('/');
|
|
23334
|
-
if (segments.length < 2 || segments.length % 2 !== 0) {
|
|
23335
|
-
throw new Error(`Ruta de documento inválida: ${path}`);
|
|
23336
|
-
}
|
|
23337
|
-
const docId = segments.pop();
|
|
23338
|
-
const collectionPath = segments.join('/');
|
|
23339
|
-
return [collectionPath, docId];
|
|
23340
|
-
}
|
|
23341
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
23342
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreService, providedIn: 'root' }); }
|
|
23343
|
-
}
|
|
23344
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FirestoreService, decorators: [{
|
|
23345
|
-
type: Injectable,
|
|
23346
|
-
args: [{ providedIn: 'root' }]
|
|
23347
|
-
}] });
|
|
23348
|
-
|
|
23349
|
-
/**
|
|
23350
|
-
* Firestore Collection
|
|
23351
|
-
*
|
|
23352
|
-
* Clase base abstracta para crear servicios de colección tipados.
|
|
23353
|
-
* Extiende esta clase para tener un servicio dedicado por entidad.
|
|
23354
|
-
*/
|
|
23355
|
-
/**
|
|
23356
|
-
* Clase base para servicios de colección tipados.
|
|
23357
|
-
*
|
|
23358
|
-
* Extiende esta clase para crear un servicio dedicado para cada entidad,
|
|
23359
|
-
* con métodos personalizados y tipado fuerte.
|
|
23360
|
-
*
|
|
23361
|
-
* @example
|
|
23362
|
-
* ```typescript
|
|
23363
|
-
* // Definir el modelo
|
|
23364
|
-
* interface User extends FirestoreDocument {
|
|
23365
|
-
* name: string;
|
|
23366
|
-
* email: string;
|
|
23367
|
-
* role: 'admin' | 'user';
|
|
23368
|
-
* active: boolean;
|
|
23369
|
-
* }
|
|
23370
|
-
*
|
|
23371
|
-
* // Crear el servicio
|
|
23372
|
-
* @Injectable({ providedIn: 'root' })
|
|
23373
|
-
* export class UsersCollection extends FirestoreCollection<User> {
|
|
23374
|
-
* constructor() {
|
|
23375
|
-
* super('users');
|
|
23376
|
-
* }
|
|
23377
|
-
*
|
|
23378
|
-
* // Métodos personalizados
|
|
23379
|
-
* async getActiveUsers(): Promise<User[]> {
|
|
23380
|
-
* return this.query({
|
|
23381
|
-
* where: [{ field: 'active', operator: '==', value: true }]
|
|
23382
|
-
* });
|
|
23383
|
-
* }
|
|
23384
|
-
*
|
|
23385
|
-
* async getAdmins(): Promise<User[]> {
|
|
23386
|
-
* return this.query({
|
|
23387
|
-
* where: [{ field: 'role', operator: '==', value: 'admin' }]
|
|
23388
|
-
* });
|
|
23389
|
-
* }
|
|
23390
|
-
*
|
|
23391
|
-
* watchOnlineUsers(): Observable<User[]> {
|
|
23392
|
-
* return this.watchQuery({
|
|
23393
|
-
* where: [{ field: 'status', operator: '==', value: 'online' }]
|
|
23394
|
-
* });
|
|
23395
|
-
* }
|
|
23396
|
-
* }
|
|
23397
|
-
*
|
|
23398
|
-
* // Usar en componentes
|
|
23399
|
-
* @Component({...})
|
|
23400
|
-
* export class UsersComponent {
|
|
23401
|
-
* private users = inject(UsersCollection);
|
|
23402
|
-
*
|
|
23403
|
-
* admins$ = this.users.getAdmins();
|
|
23404
|
-
* onlineUsers$ = this.users.watchOnlineUsers();
|
|
23405
|
-
*
|
|
23406
|
-
* async createUser(data: Omit<User, 'id'>) {
|
|
23407
|
-
* const user = await this.users.create(data);
|
|
23408
|
-
* }
|
|
23409
|
-
* }
|
|
23410
|
-
* ```
|
|
23411
|
-
*/
|
|
23412
|
-
class FirestoreCollection {
|
|
23413
|
-
/**
|
|
23414
|
-
* @param collectionPath - Ruta de la colección en Firestore
|
|
23415
|
-
* @param options - Opciones de configuración
|
|
23416
|
-
*/
|
|
23417
|
-
constructor(collectionPath, options = {}) {
|
|
23418
|
-
this.firestore = inject(FirestoreService);
|
|
23419
|
-
this.collectionPath = collectionPath;
|
|
23420
|
-
this.options = {
|
|
23421
|
-
softDelete: false,
|
|
23422
|
-
timestamps: true,
|
|
23423
|
-
...options,
|
|
23424
|
-
};
|
|
23425
|
-
}
|
|
23426
|
-
// ===========================================================================
|
|
23427
|
-
// LECTURAS ONE-TIME
|
|
23428
|
-
// ===========================================================================
|
|
23429
|
-
/**
|
|
23430
|
-
* Obtiene un documento por ID.
|
|
23431
|
-
*/
|
|
23432
|
-
async getById(id) {
|
|
23433
|
-
return this.firestore.getDoc(this.collectionPath, id);
|
|
23434
|
-
}
|
|
23435
|
-
/**
|
|
23436
|
-
* Obtiene todos los documentos de la colección.
|
|
23437
|
-
*/
|
|
23438
|
-
async getAll(options) {
|
|
23439
|
-
const queryOptions = this.applyDefaultFilters(options);
|
|
23440
|
-
return this.firestore.getDocs(this.collectionPath, queryOptions);
|
|
23441
|
-
}
|
|
23442
|
-
/**
|
|
23443
|
-
* Ejecuta una query personalizada.
|
|
23444
|
-
*/
|
|
23445
|
-
async query(options) {
|
|
23446
|
-
const queryOptions = this.applyDefaultFilters(options);
|
|
23447
|
-
return this.firestore.getDocs(this.collectionPath, queryOptions);
|
|
23448
|
-
}
|
|
23449
|
-
/**
|
|
23450
|
-
* Obtiene documentos con paginación.
|
|
23451
|
-
*/
|
|
23452
|
-
async paginate(options) {
|
|
23453
|
-
const queryOptions = this.applyDefaultFilters(options);
|
|
23454
|
-
return this.firestore.getPaginated(this.collectionPath, queryOptions);
|
|
23455
|
-
}
|
|
23456
|
-
/**
|
|
23457
|
-
* Obtiene el primer documento que coincida con la query.
|
|
23458
|
-
*/
|
|
23459
|
-
async getFirst(options) {
|
|
23460
|
-
const queryOptions = this.applyDefaultFilters({
|
|
23461
|
-
...options,
|
|
23462
|
-
limit: 1,
|
|
23463
|
-
});
|
|
23464
|
-
const results = await this.firestore.getDocs(this.collectionPath, queryOptions);
|
|
23465
|
-
return results[0] ?? null;
|
|
23466
|
-
}
|
|
23467
|
-
/**
|
|
23468
|
-
* Cuenta los documentos que coinciden con la query.
|
|
23469
|
-
* Nota: Esto carga todos los documentos, usar con cuidado en colecciones grandes.
|
|
23470
|
-
*/
|
|
23471
|
-
async count(options) {
|
|
23472
|
-
const queryOptions = this.applyDefaultFilters(options);
|
|
23473
|
-
const results = await this.firestore.getDocs(this.collectionPath, queryOptions);
|
|
23474
|
-
return results.length;
|
|
23475
|
-
}
|
|
23476
|
-
/**
|
|
23477
|
-
* Verifica si un documento existe.
|
|
23478
|
-
*/
|
|
23479
|
-
async exists(id) {
|
|
23480
|
-
return this.firestore.exists(this.collectionPath, id);
|
|
23481
|
-
}
|
|
23482
|
-
// ===========================================================================
|
|
23483
|
-
// SUBSCRIPCIONES REAL-TIME
|
|
23484
|
-
// ===========================================================================
|
|
23485
|
-
/**
|
|
23486
|
-
* Suscribe a cambios de un documento.
|
|
23487
|
-
*/
|
|
23488
|
-
watch(id) {
|
|
23489
|
-
return this.firestore.docChanges(this.collectionPath, id);
|
|
23490
|
-
}
|
|
23491
|
-
/**
|
|
23492
|
-
* Suscribe a cambios de la colección.
|
|
23493
|
-
*/
|
|
23494
|
-
watchAll(options) {
|
|
23495
|
-
const queryOptions = this.applyDefaultFilters(options);
|
|
23496
|
-
return this.firestore.collectionChanges(this.collectionPath, queryOptions);
|
|
23497
|
-
}
|
|
23498
|
-
/**
|
|
23499
|
-
* Suscribe a una query personalizada.
|
|
23500
|
-
*/
|
|
23501
|
-
watchQuery(options) {
|
|
23502
|
-
const queryOptions = this.applyDefaultFilters(options);
|
|
23503
|
-
return this.firestore.collectionChanges(this.collectionPath, queryOptions);
|
|
23504
|
-
}
|
|
23505
|
-
// ===========================================================================
|
|
23506
|
-
// ESCRITURA
|
|
23507
|
-
// ===========================================================================
|
|
23508
|
-
/**
|
|
23509
|
-
* Crea un nuevo documento con ID auto-generado.
|
|
23510
|
-
*/
|
|
23511
|
-
async create(data) {
|
|
23512
|
-
return this.firestore.addDoc(this.collectionPath, data);
|
|
23513
|
-
}
|
|
23514
|
-
/**
|
|
23515
|
-
* Crea un documento con ID específico.
|
|
23516
|
-
*/
|
|
23517
|
-
async createWithId(id, data) {
|
|
23518
|
-
return this.firestore.setDoc(this.collectionPath, id, data);
|
|
23519
|
-
}
|
|
23520
|
-
/**
|
|
23521
|
-
* Actualiza campos de un documento.
|
|
23522
|
-
*/
|
|
23523
|
-
async update(id, data) {
|
|
23524
|
-
return this.firestore.updateDoc(this.collectionPath, id, data);
|
|
23525
|
-
}
|
|
23526
|
-
/**
|
|
23527
|
-
* Elimina un documento.
|
|
23528
|
-
* Si softDelete está habilitado, marca como eliminado en lugar de borrar.
|
|
23529
|
-
*/
|
|
23530
|
-
async delete(id) {
|
|
23531
|
-
if (this.options.softDelete) {
|
|
23532
|
-
return this.firestore.updateDoc(this.collectionPath, id, {
|
|
23533
|
-
deletedAt: new Date(),
|
|
23534
|
-
});
|
|
23535
|
-
}
|
|
23536
|
-
return this.firestore.deleteDoc(this.collectionPath, id);
|
|
23537
|
-
}
|
|
23538
|
-
/**
|
|
23539
|
-
* Restaura un documento soft-deleted.
|
|
23540
|
-
*/
|
|
23541
|
-
async restore(id) {
|
|
23542
|
-
if (!this.options.softDelete) {
|
|
23543
|
-
throw new Error('Soft delete no está habilitado para esta colección');
|
|
23544
|
-
}
|
|
23545
|
-
return this.firestore.updateDoc(this.collectionPath, id, {
|
|
23546
|
-
deletedAt: null,
|
|
23547
|
-
});
|
|
23548
|
-
}
|
|
23549
|
-
// ===========================================================================
|
|
23550
|
-
// SUB-COLECCIONES
|
|
23551
|
-
// ===========================================================================
|
|
23552
|
-
/**
|
|
23553
|
-
* Obtiene una referencia a una sub-colección.
|
|
23554
|
-
*
|
|
23555
|
-
* @example
|
|
23556
|
-
* ```typescript
|
|
23557
|
-
* // En UsersCollection
|
|
23558
|
-
* getUserDocuments(userId: string) {
|
|
23559
|
-
* return this.subcollection<Document>(userId, 'documents');
|
|
23560
|
-
* }
|
|
23561
|
-
*
|
|
23562
|
-
* // Uso
|
|
23563
|
-
* const docs = await users.getUserDocuments('user123').getAll();
|
|
23564
|
-
* ```
|
|
23565
|
-
*/
|
|
23566
|
-
subcollection(parentId, subcollectionName) {
|
|
23567
|
-
const subPath = `${this.collectionPath}/${parentId}/${subcollectionName}`;
|
|
23568
|
-
return {
|
|
23569
|
-
getById: (id) => this.firestore.getDoc(subPath, id),
|
|
23570
|
-
getAll: (options) => this.firestore.getDocs(subPath, options),
|
|
23571
|
-
watch: (id) => this.firestore.docChanges(subPath, id),
|
|
23572
|
-
watchAll: (options) => this.firestore.collectionChanges(subPath, options),
|
|
23573
|
-
create: (data) => this.firestore.addDoc(subPath, data),
|
|
23574
|
-
update: (id, data) => this.firestore.updateDoc(subPath, id, data),
|
|
23575
|
-
delete: (id) => this.firestore.deleteDoc(subPath, id),
|
|
23576
|
-
};
|
|
23577
|
-
}
|
|
23578
|
-
// ===========================================================================
|
|
23579
|
-
// MÉTODOS PROTEGIDOS (para override en subclases)
|
|
23580
|
-
// ===========================================================================
|
|
23581
|
-
/**
|
|
23582
|
-
* Aplica filtros por defecto a las queries.
|
|
23583
|
-
* Override este método para agregar filtros globales (ej: excluir soft-deleted).
|
|
23584
|
-
*/
|
|
23585
|
-
applyDefaultFilters(options) {
|
|
23586
|
-
if (!this.options.softDelete) {
|
|
23587
|
-
return options ?? {};
|
|
23588
|
-
}
|
|
23589
|
-
// Excluir documentos soft-deleted por defecto
|
|
23590
|
-
const whereClause = { field: 'deletedAt', operator: '==', value: null };
|
|
23591
|
-
return {
|
|
23592
|
-
...options,
|
|
23593
|
-
where: [...(options?.where ?? []), whereClause],
|
|
23594
|
-
};
|
|
23595
|
-
}
|
|
23596
|
-
// ===========================================================================
|
|
23597
|
-
// UTILIDADES
|
|
23598
|
-
// ===========================================================================
|
|
23599
|
-
/**
|
|
23600
|
-
* Genera un nuevo ID sin crear el documento.
|
|
23601
|
-
*/
|
|
23602
|
-
generateId() {
|
|
23603
|
-
return this.firestore.generateId(this.collectionPath);
|
|
23604
|
-
}
|
|
23605
|
-
/**
|
|
23606
|
-
* Obtiene la ruta de la colección.
|
|
23607
|
-
*/
|
|
23608
|
-
getPath() {
|
|
23609
|
-
return this.collectionPath;
|
|
23610
|
-
}
|
|
23611
|
-
}
|
|
23612
|
-
|
|
23613
|
-
/**
|
|
23614
|
-
* Query Builder
|
|
23615
|
-
*
|
|
23616
|
-
* Builder fluido para construir queries de Firestore de manera legible.
|
|
23617
|
-
* Alternativa más expresiva a pasar objetos QueryOptions directamente.
|
|
23618
|
-
*/
|
|
23619
|
-
/**
|
|
23620
|
-
* Builder fluido para queries de Firestore.
|
|
23621
|
-
*
|
|
23622
|
-
* @example
|
|
23623
|
-
* ```typescript
|
|
23624
|
-
* // Construir query con builder
|
|
23625
|
-
* const options = new QueryBuilder()
|
|
23626
|
-
* .where('status', '==', 'active')
|
|
23627
|
-
* .where('age', '>=', 18)
|
|
23628
|
-
* .orderBy('createdAt', 'desc')
|
|
23629
|
-
* .limit(10)
|
|
23630
|
-
* .build();
|
|
23631
|
-
*
|
|
23632
|
-
* // Usar con FirestoreService
|
|
23633
|
-
* const users = await firestoreService.getDocs<User>('users', options);
|
|
23634
|
-
*
|
|
23635
|
-
* // O con método estático
|
|
23636
|
-
* const options2 = QueryBuilder.create()
|
|
23637
|
-
* .where('category', '==', 'electronics')
|
|
23638
|
-
* .orderBy('price', 'asc')
|
|
23639
|
-
* .build();
|
|
23640
|
-
* ```
|
|
23641
|
-
*/
|
|
23642
|
-
class QueryBuilder {
|
|
23643
|
-
constructor() {
|
|
23644
|
-
this.whereConditions = [];
|
|
23645
|
-
this.orderByConditions = [];
|
|
23646
|
-
}
|
|
23647
|
-
/**
|
|
23648
|
-
* Crea una nueva instancia del builder (método estático alternativo).
|
|
23649
|
-
*/
|
|
23650
|
-
static create() {
|
|
23651
|
-
return new QueryBuilder();
|
|
23652
|
-
}
|
|
23653
|
-
/**
|
|
23654
|
-
* Agrega una condición where.
|
|
23655
|
-
*
|
|
23656
|
-
* @param field - Campo a filtrar
|
|
23657
|
-
* @param operator - Operador de comparación
|
|
23658
|
-
* @param value - Valor a comparar
|
|
23659
|
-
*
|
|
23660
|
-
* @example
|
|
23661
|
-
* ```typescript
|
|
23662
|
-
* builder.where('status', '==', 'active')
|
|
23663
|
-
* builder.where('price', '>=', 100)
|
|
23664
|
-
* builder.where('tags', 'array-contains', 'featured')
|
|
23665
|
-
* builder.where('category', 'in', ['electronics', 'books'])
|
|
23666
|
-
* ```
|
|
23667
|
-
*/
|
|
23668
|
-
where(field, operator, value) {
|
|
23669
|
-
this.whereConditions.push({ field, operator, value });
|
|
23670
|
-
return this;
|
|
23671
|
-
}
|
|
23672
|
-
/**
|
|
23673
|
-
* Shortcut para where con operador '=='.
|
|
23674
|
-
*
|
|
23675
|
-
* @example
|
|
23676
|
-
* ```typescript
|
|
23677
|
-
* builder.whereEquals('status', 'active')
|
|
23678
|
-
* // equivalente a: builder.where('status', '==', 'active')
|
|
23679
|
-
* ```
|
|
23680
|
-
*/
|
|
23681
|
-
whereEquals(field, value) {
|
|
23682
|
-
return this.where(field, '==', value);
|
|
23683
|
-
}
|
|
23684
|
-
/**
|
|
23685
|
-
* Shortcut para where con operador '!='.
|
|
23686
|
-
*/
|
|
23687
|
-
whereNotEquals(field, value) {
|
|
23688
|
-
return this.where(field, '!=', value);
|
|
23689
|
-
}
|
|
23690
|
-
/**
|
|
23691
|
-
* Shortcut para where con operador '>'.
|
|
23692
|
-
*/
|
|
23693
|
-
whereGreaterThan(field, value) {
|
|
23694
|
-
return this.where(field, '>', value);
|
|
23695
|
-
}
|
|
23696
|
-
/**
|
|
23697
|
-
* Shortcut para where con operador '>='.
|
|
23698
|
-
*/
|
|
23699
|
-
whereGreaterOrEqual(field, value) {
|
|
23700
|
-
return this.where(field, '>=', value);
|
|
23701
|
-
}
|
|
23702
|
-
/**
|
|
23703
|
-
* Shortcut para where con operador '<'.
|
|
23704
|
-
*/
|
|
23705
|
-
whereLessThan(field, value) {
|
|
23706
|
-
return this.where(field, '<', value);
|
|
23707
|
-
}
|
|
23708
|
-
/**
|
|
23709
|
-
* Shortcut para where con operador '<='.
|
|
23710
|
-
*/
|
|
23711
|
-
whereLessOrEqual(field, value) {
|
|
23712
|
-
return this.where(field, '<=', value);
|
|
23713
|
-
}
|
|
23714
|
-
/**
|
|
23715
|
-
* Shortcut para where con operador 'array-contains'.
|
|
23716
|
-
*
|
|
23717
|
-
* @example
|
|
23718
|
-
* ```typescript
|
|
23719
|
-
* builder.whereArrayContains('tags', 'featured')
|
|
23720
|
-
* ```
|
|
23721
|
-
*/
|
|
23722
|
-
whereArrayContains(field, value) {
|
|
23723
|
-
return this.where(field, 'array-contains', value);
|
|
23724
|
-
}
|
|
23725
|
-
/**
|
|
23726
|
-
* Shortcut para where con operador 'array-contains-any'.
|
|
23727
|
-
*
|
|
23728
|
-
* @example
|
|
23729
|
-
* ```typescript
|
|
23730
|
-
* builder.whereArrayContainsAny('tags', ['featured', 'new'])
|
|
23731
|
-
* ```
|
|
23732
|
-
*/
|
|
23733
|
-
whereArrayContainsAny(field, values) {
|
|
23734
|
-
return this.where(field, 'array-contains-any', values);
|
|
23735
|
-
}
|
|
23736
|
-
/**
|
|
23737
|
-
* Shortcut para where con operador 'in'.
|
|
23738
|
-
*
|
|
23739
|
-
* @example
|
|
23740
|
-
* ```typescript
|
|
23741
|
-
* builder.whereIn('status', ['active', 'pending'])
|
|
23742
|
-
* ```
|
|
23743
|
-
*/
|
|
23744
|
-
whereIn(field, values) {
|
|
23745
|
-
return this.where(field, 'in', values);
|
|
23746
|
-
}
|
|
23747
|
-
/**
|
|
23748
|
-
* Shortcut para where con operador 'not-in'.
|
|
23749
|
-
*/
|
|
23750
|
-
whereNotIn(field, values) {
|
|
23751
|
-
return this.where(field, 'not-in', values);
|
|
23752
|
-
}
|
|
23753
|
-
/**
|
|
23754
|
-
* Agrega ordenamiento por un campo.
|
|
23755
|
-
*
|
|
23756
|
-
* @param field - Campo por el cual ordenar
|
|
23757
|
-
* @param direction - Dirección: 'asc' o 'desc' (default: 'asc')
|
|
23758
|
-
*
|
|
23759
|
-
* @example
|
|
23760
|
-
* ```typescript
|
|
23761
|
-
* builder.orderBy('createdAt', 'desc')
|
|
23762
|
-
* builder.orderBy('name') // asc por defecto
|
|
23763
|
-
* ```
|
|
23764
|
-
*/
|
|
23765
|
-
orderBy(field, direction = 'asc') {
|
|
23766
|
-
this.orderByConditions.push({ field, direction });
|
|
23767
|
-
return this;
|
|
23768
|
-
}
|
|
23769
|
-
/**
|
|
23770
|
-
* Shortcut para orderBy descendente.
|
|
23771
|
-
*/
|
|
23772
|
-
orderByDesc(field) {
|
|
23773
|
-
return this.orderBy(field, 'desc');
|
|
23774
|
-
}
|
|
23775
|
-
/**
|
|
23776
|
-
* Shortcut para orderBy ascendente.
|
|
23777
|
-
*/
|
|
23778
|
-
orderByAsc(field) {
|
|
23779
|
-
return this.orderBy(field, 'asc');
|
|
23780
|
-
}
|
|
23781
|
-
/**
|
|
23782
|
-
* Limita el número de resultados.
|
|
23783
|
-
*
|
|
23784
|
-
* @param count - Número máximo de documentos
|
|
23785
|
-
*
|
|
23786
|
-
* @example
|
|
23787
|
-
* ```typescript
|
|
23788
|
-
* builder.limit(10)
|
|
23789
|
-
* ```
|
|
23790
|
-
*/
|
|
23791
|
-
limit(count) {
|
|
23792
|
-
if (count <= 0) {
|
|
23793
|
-
throw new Error('El límite debe ser mayor a 0');
|
|
23794
|
-
}
|
|
23795
|
-
this.limitValue = count;
|
|
23796
|
-
return this;
|
|
23797
|
-
}
|
|
23798
|
-
/**
|
|
23799
|
-
* Cursor para paginación: empezar después de un documento.
|
|
23800
|
-
*
|
|
23801
|
-
* @param cursor - Documento o snapshot desde donde continuar
|
|
23802
|
-
*
|
|
23803
|
-
* @example
|
|
23804
|
-
* ```typescript
|
|
23805
|
-
* // Primera página
|
|
23806
|
-
* const page1 = await service.getPaginated('users', builder.limit(10).build());
|
|
23807
|
-
*
|
|
23808
|
-
* // Siguiente página
|
|
23809
|
-
* const page2 = await service.getPaginated('users',
|
|
23810
|
-
* builder.startAfter(page1.lastDoc).limit(10).build()
|
|
23811
|
-
* );
|
|
23812
|
-
* ```
|
|
23813
|
-
*/
|
|
23814
|
-
startAfter(cursor) {
|
|
23815
|
-
this.startAfterValue = cursor;
|
|
23816
|
-
return this;
|
|
23817
|
-
}
|
|
23818
|
-
/**
|
|
23819
|
-
* Cursor para paginación: empezar en un documento.
|
|
23820
|
-
*/
|
|
23821
|
-
startAt(cursor) {
|
|
23822
|
-
this.startAtValue = cursor;
|
|
23823
|
-
return this;
|
|
23824
|
-
}
|
|
23825
|
-
/**
|
|
23826
|
-
* Cursor para paginación: terminar antes de un documento.
|
|
23827
|
-
*/
|
|
23828
|
-
endBefore(cursor) {
|
|
23829
|
-
this.endBeforeValue = cursor;
|
|
23830
|
-
return this;
|
|
23831
|
-
}
|
|
23832
|
-
/**
|
|
23833
|
-
* Cursor para paginación: terminar en un documento.
|
|
23834
|
-
*/
|
|
23835
|
-
endAt(cursor) {
|
|
23836
|
-
this.endAtValue = cursor;
|
|
23837
|
-
return this;
|
|
23838
|
-
}
|
|
23839
|
-
/**
|
|
23840
|
-
* Construye el objeto QueryOptions.
|
|
23841
|
-
*
|
|
23842
|
-
* @returns QueryOptions para usar con FirestoreService
|
|
23843
|
-
*/
|
|
23844
|
-
build() {
|
|
23845
|
-
const options = {};
|
|
23846
|
-
if (this.whereConditions.length > 0) {
|
|
23847
|
-
options.where = [...this.whereConditions];
|
|
23848
|
-
}
|
|
23849
|
-
if (this.orderByConditions.length > 0) {
|
|
23850
|
-
options.orderBy = [...this.orderByConditions];
|
|
23851
|
-
}
|
|
23852
|
-
if (this.limitValue !== undefined) {
|
|
23853
|
-
options.limit = this.limitValue;
|
|
23854
|
-
}
|
|
23855
|
-
if (this.startAfterValue !== undefined) {
|
|
23856
|
-
options.startAfter = this.startAfterValue;
|
|
23857
|
-
}
|
|
23858
|
-
if (this.startAtValue !== undefined) {
|
|
23859
|
-
options.startAt = this.startAtValue;
|
|
23860
|
-
}
|
|
23861
|
-
if (this.endBeforeValue !== undefined) {
|
|
23862
|
-
options.endBefore = this.endBeforeValue;
|
|
23863
|
-
}
|
|
23864
|
-
if (this.endAtValue !== undefined) {
|
|
23865
|
-
options.endAt = this.endAtValue;
|
|
23866
|
-
}
|
|
23867
|
-
return options;
|
|
23868
|
-
}
|
|
23869
|
-
/**
|
|
23870
|
-
* Resetea el builder para reutilización.
|
|
23871
|
-
*/
|
|
23872
|
-
reset() {
|
|
23873
|
-
this.whereConditions = [];
|
|
23874
|
-
this.orderByConditions = [];
|
|
23875
|
-
this.limitValue = undefined;
|
|
23876
|
-
this.startAfterValue = undefined;
|
|
23877
|
-
this.startAtValue = undefined;
|
|
23878
|
-
this.endBeforeValue = undefined;
|
|
23879
|
-
this.endAtValue = undefined;
|
|
23880
|
-
return this;
|
|
23881
|
-
}
|
|
23882
|
-
/**
|
|
23883
|
-
* Clona el builder actual.
|
|
23884
|
-
*/
|
|
23885
|
-
clone() {
|
|
23886
|
-
const cloned = new QueryBuilder();
|
|
23887
|
-
cloned.whereConditions = [...this.whereConditions];
|
|
23888
|
-
cloned.orderByConditions = [...this.orderByConditions];
|
|
23889
|
-
cloned.limitValue = this.limitValue;
|
|
23890
|
-
cloned.startAfterValue = this.startAfterValue;
|
|
23891
|
-
cloned.startAtValue = this.startAtValue;
|
|
23892
|
-
cloned.endBeforeValue = this.endBeforeValue;
|
|
23893
|
-
cloned.endAtValue = this.endAtValue;
|
|
23894
|
-
return cloned;
|
|
23895
|
-
}
|
|
23896
|
-
}
|
|
23897
|
-
/**
|
|
23898
|
-
* Función helper para crear un QueryBuilder.
|
|
23899
|
-
*
|
|
23900
|
-
* @example
|
|
23901
|
-
* ```typescript
|
|
23902
|
-
* import { query } from 'valtech-components';
|
|
23903
|
-
*
|
|
23904
|
-
* const options = query()
|
|
23905
|
-
* .where('status', '==', 'active')
|
|
23906
|
-
* .orderBy('createdAt', 'desc')
|
|
23907
|
-
* .limit(10)
|
|
23908
|
-
* .build();
|
|
23909
|
-
* ```
|
|
23910
|
-
*/
|
|
23911
|
-
function query() {
|
|
23912
|
-
return new QueryBuilder();
|
|
23913
|
-
}
|
|
23914
|
-
|
|
23915
|
-
/**
|
|
23916
|
-
* Storage Service
|
|
23917
|
-
*
|
|
23918
|
-
* Servicio para operaciones de Firebase Storage.
|
|
23919
|
-
* Soporta upload con tracking de progreso, download y gestión de archivos.
|
|
23920
|
-
*/
|
|
23921
|
-
/**
|
|
23922
|
-
* Servicio para Firebase Storage.
|
|
23923
|
-
*
|
|
23924
|
-
* @example
|
|
23925
|
-
* ```typescript
|
|
23926
|
-
* @Component({...})
|
|
23927
|
-
* export class FileUploadComponent {
|
|
23928
|
-
* private storage = inject(StorageService);
|
|
23929
|
-
*
|
|
23930
|
-
* uploadProgress = signal<number>(0);
|
|
23931
|
-
* downloadUrl = signal<string | null>(null);
|
|
23932
|
-
*
|
|
23933
|
-
* async onFileSelected(event: Event) {
|
|
23934
|
-
* const file = (event.target as HTMLInputElement).files?.[0];
|
|
23935
|
-
* if (!file) return;
|
|
23936
|
-
*
|
|
23937
|
-
* // Upload con progreso
|
|
23938
|
-
* this.storage.upload(`uploads/${file.name}`, file).subscribe({
|
|
23939
|
-
* next: (progress) => this.uploadProgress.set(progress.percentage),
|
|
23940
|
-
* complete: async () => {
|
|
23941
|
-
* const url = await this.storage.getDownloadUrl(`uploads/${file.name}`);
|
|
23942
|
-
* this.downloadUrl.set(url);
|
|
23943
|
-
* }
|
|
23944
|
-
* });
|
|
23945
|
-
* }
|
|
23946
|
-
* }
|
|
23947
|
-
* ```
|
|
23948
|
-
*/
|
|
23949
|
-
class StorageService {
|
|
23950
|
-
constructor() {
|
|
23951
|
-
this.storage = inject(Storage);
|
|
23952
|
-
}
|
|
23953
|
-
// ===========================================================================
|
|
23954
|
-
// UPLOAD
|
|
23955
|
-
// ===========================================================================
|
|
23956
|
-
/**
|
|
23957
|
-
* Sube un archivo con tracking de progreso.
|
|
23958
|
-
*
|
|
23959
|
-
* @param path - Ruta en Storage donde guardar el archivo
|
|
23960
|
-
* @param file - Archivo a subir (File o Blob)
|
|
23961
|
-
* @param metadata - Metadata opcional (contentType, customMetadata)
|
|
23962
|
-
* @returns Observable que emite el progreso y completa cuando termina
|
|
23963
|
-
*
|
|
23964
|
-
* @example
|
|
23965
|
-
* ```typescript
|
|
23966
|
-
* // Upload básico
|
|
23967
|
-
* storage.upload('images/photo.jpg', file).subscribe({
|
|
23968
|
-
* next: (progress) => console.log(`${progress.percentage}%`),
|
|
23969
|
-
* complete: () => console.log('Upload completado')
|
|
23970
|
-
* });
|
|
23971
|
-
*
|
|
23972
|
-
* // Con metadata
|
|
23973
|
-
* storage.upload('docs/report.pdf', file, {
|
|
23974
|
-
* contentType: 'application/pdf',
|
|
23975
|
-
* customMetadata: { uploadedBy: 'user123' }
|
|
23976
|
-
* }).subscribe(...);
|
|
23977
|
-
* ```
|
|
23978
|
-
*/
|
|
23979
|
-
upload(path, file, metadata) {
|
|
23980
|
-
const storageRef = ref(this.storage, path);
|
|
23981
|
-
const uploadMetadata = {
|
|
23982
|
-
contentType: metadata?.contentType || (file instanceof File ? file.type : undefined),
|
|
23983
|
-
customMetadata: metadata?.customMetadata,
|
|
23984
|
-
cacheControl: metadata?.cacheControl,
|
|
23985
|
-
};
|
|
23986
|
-
const task = uploadBytesResumable(storageRef, file, uploadMetadata);
|
|
23987
|
-
const progress$ = new BehaviorSubject({
|
|
23988
|
-
bytesTransferred: 0,
|
|
23989
|
-
totalBytes: file.size,
|
|
23990
|
-
percentage: 0,
|
|
23991
|
-
state: 'running',
|
|
23992
|
-
});
|
|
23993
|
-
task.on('state_changed', (snapshot) => {
|
|
23994
|
-
progress$.next({
|
|
23995
|
-
bytesTransferred: snapshot.bytesTransferred,
|
|
23996
|
-
totalBytes: snapshot.totalBytes,
|
|
23997
|
-
percentage: Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100),
|
|
23998
|
-
state: this.mapTaskState(snapshot.state),
|
|
23999
|
-
});
|
|
24000
|
-
}, (error) => {
|
|
24001
|
-
progress$.next({
|
|
24002
|
-
bytesTransferred: 0,
|
|
24003
|
-
totalBytes: file.size,
|
|
24004
|
-
percentage: 0,
|
|
24005
|
-
state: 'error',
|
|
24006
|
-
});
|
|
24007
|
-
progress$.error(this.getErrorMessage(error));
|
|
24008
|
-
}, () => {
|
|
24009
|
-
progress$.next({
|
|
24010
|
-
bytesTransferred: file.size,
|
|
24011
|
-
totalBytes: file.size,
|
|
24012
|
-
percentage: 100,
|
|
24013
|
-
state: 'success',
|
|
24014
|
-
});
|
|
24015
|
-
progress$.complete();
|
|
24016
|
-
});
|
|
24017
|
-
return progress$.asObservable();
|
|
24018
|
-
}
|
|
24019
|
-
/**
|
|
24020
|
-
* Sube un archivo y retorna la URL de descarga al completar.
|
|
24021
|
-
*
|
|
24022
|
-
* @param path - Ruta en Storage
|
|
24023
|
-
* @param file - Archivo a subir
|
|
24024
|
-
* @param metadata - Metadata opcional
|
|
24025
|
-
* @returns Resultado del upload con URL de descarga
|
|
24026
|
-
*
|
|
24027
|
-
* @example
|
|
24028
|
-
* ```typescript
|
|
24029
|
-
* const result = await storage.uploadAndGetUrl('avatars/user123.jpg', file);
|
|
24030
|
-
* console.log('URL:', result.downloadUrl);
|
|
24031
|
-
* ```
|
|
24032
|
-
*/
|
|
24033
|
-
async uploadAndGetUrl(path, file, metadata) {
|
|
24034
|
-
return new Promise((resolve, reject) => {
|
|
24035
|
-
this.upload(path, file, metadata).subscribe({
|
|
24036
|
-
complete: async () => {
|
|
24037
|
-
try {
|
|
24038
|
-
const storageRef = ref(this.storage, path);
|
|
24039
|
-
const downloadUrl = await getDownloadURL(storageRef);
|
|
24040
|
-
const storedMetadata = await getMetadata(storageRef);
|
|
24041
|
-
resolve({
|
|
24042
|
-
downloadUrl,
|
|
24043
|
-
fullPath: storedMetadata.fullPath,
|
|
24044
|
-
name: storedMetadata.name,
|
|
24045
|
-
size: storedMetadata.size,
|
|
24046
|
-
contentType: storedMetadata.contentType || 'application/octet-stream',
|
|
24047
|
-
metadata: storedMetadata.customMetadata || {},
|
|
24048
|
-
});
|
|
24049
|
-
}
|
|
24050
|
-
catch (error) {
|
|
24051
|
-
reject(this.getErrorMessage(error));
|
|
24052
|
-
}
|
|
24053
|
-
},
|
|
24054
|
-
error: (error) => reject(error),
|
|
24055
|
-
});
|
|
24056
|
-
});
|
|
24057
|
-
}
|
|
24058
|
-
/**
|
|
24059
|
-
* Sube un archivo desde una Data URL (base64).
|
|
24060
|
-
*
|
|
24061
|
-
* @param path - Ruta en Storage
|
|
24062
|
-
* @param dataUrl - Data URL (ej: 'data:image/png;base64,...')
|
|
24063
|
-
* @param metadata - Metadata opcional
|
|
24064
|
-
* @returns Resultado del upload
|
|
24065
|
-
*
|
|
24066
|
-
* @example
|
|
24067
|
-
* ```typescript
|
|
24068
|
-
* // Desde canvas
|
|
24069
|
-
* const dataUrl = canvas.toDataURL('image/png');
|
|
24070
|
-
* const result = await storage.uploadFromDataUrl('images/drawing.png', dataUrl);
|
|
24071
|
-
* ```
|
|
24072
|
-
*/
|
|
24073
|
-
async uploadFromDataUrl(path, dataUrl, metadata) {
|
|
24074
|
-
// Extraer content type y datos base64
|
|
24075
|
-
const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
24076
|
-
if (!matches) {
|
|
24077
|
-
throw new Error('Data URL inválida');
|
|
24078
|
-
}
|
|
24079
|
-
const contentType = matches[1];
|
|
24080
|
-
const base64Data = matches[2];
|
|
24081
|
-
// Convertir base64 a Blob
|
|
24082
|
-
const byteCharacters = atob(base64Data);
|
|
24083
|
-
const byteNumbers = new Array(byteCharacters.length);
|
|
24084
|
-
for (let i = 0; i < byteCharacters.length; i++) {
|
|
24085
|
-
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
24086
|
-
}
|
|
24087
|
-
const byteArray = new Uint8Array(byteNumbers);
|
|
24088
|
-
const blob = new Blob([byteArray], { type: contentType });
|
|
24089
|
-
return this.uploadAndGetUrl(path, blob, {
|
|
24090
|
-
contentType,
|
|
24091
|
-
...metadata,
|
|
24092
|
-
});
|
|
24093
|
-
}
|
|
24094
|
-
// ===========================================================================
|
|
24095
|
-
// DOWNLOAD
|
|
24096
|
-
// ===========================================================================
|
|
24097
|
-
/**
|
|
24098
|
-
* Obtiene la URL de descarga de un archivo.
|
|
24099
|
-
*
|
|
24100
|
-
* @param path - Ruta del archivo en Storage
|
|
24101
|
-
* @returns URL de descarga
|
|
24102
|
-
*
|
|
24103
|
-
* @example
|
|
24104
|
-
* ```typescript
|
|
24105
|
-
* const url = await storage.getDownloadUrl('images/photo.jpg');
|
|
24106
|
-
* // Usar en <img [src]="url">
|
|
24107
|
-
* ```
|
|
24108
|
-
*/
|
|
24109
|
-
async getDownloadUrl(path) {
|
|
24110
|
-
try {
|
|
24111
|
-
const storageRef = ref(this.storage, path);
|
|
24112
|
-
return await getDownloadURL(storageRef);
|
|
24113
|
-
}
|
|
24114
|
-
catch (error) {
|
|
24115
|
-
throw new Error(this.getErrorMessage(error));
|
|
24116
|
-
}
|
|
24117
|
-
}
|
|
24118
|
-
/**
|
|
24119
|
-
* Obtiene la metadata de un archivo.
|
|
24120
|
-
*
|
|
24121
|
-
* @param path - Ruta del archivo
|
|
24122
|
-
* @returns Metadata del archivo
|
|
24123
|
-
*/
|
|
24124
|
-
async getMetadata(path) {
|
|
24125
|
-
try {
|
|
24126
|
-
const storageRef = ref(this.storage, path);
|
|
24127
|
-
const metadata = await getMetadata(storageRef);
|
|
24128
|
-
return {
|
|
24129
|
-
contentType: metadata.contentType,
|
|
24130
|
-
customMetadata: metadata.customMetadata,
|
|
24131
|
-
cacheControl: metadata.cacheControl,
|
|
24132
|
-
size: metadata.size,
|
|
24133
|
-
name: metadata.name,
|
|
24134
|
-
};
|
|
24135
|
-
}
|
|
24136
|
-
catch (error) {
|
|
24137
|
-
throw new Error(this.getErrorMessage(error));
|
|
24138
|
-
}
|
|
24139
|
-
}
|
|
24140
|
-
// ===========================================================================
|
|
24141
|
-
// DELETE
|
|
24142
|
-
// ===========================================================================
|
|
24143
|
-
/**
|
|
24144
|
-
* Elimina un archivo.
|
|
24145
|
-
*
|
|
24146
|
-
* @param path - Ruta del archivo a eliminar
|
|
24147
|
-
*
|
|
24148
|
-
* @example
|
|
24149
|
-
* ```typescript
|
|
24150
|
-
* await storage.delete('images/old-photo.jpg');
|
|
24151
|
-
* ```
|
|
24152
|
-
*/
|
|
24153
|
-
async delete(path) {
|
|
24154
|
-
try {
|
|
24155
|
-
const storageRef = ref(this.storage, path);
|
|
24156
|
-
await deleteObject(storageRef);
|
|
24157
|
-
}
|
|
24158
|
-
catch (error) {
|
|
24159
|
-
throw new Error(this.getErrorMessage(error));
|
|
24160
|
-
}
|
|
24161
|
-
}
|
|
24162
|
-
/**
|
|
24163
|
-
* Elimina múltiples archivos.
|
|
24164
|
-
*
|
|
24165
|
-
* @param paths - Array de rutas a eliminar
|
|
24166
|
-
*
|
|
24167
|
-
* @example
|
|
24168
|
-
* ```typescript
|
|
24169
|
-
* await storage.deleteMultiple([
|
|
24170
|
-
* 'images/photo1.jpg',
|
|
24171
|
-
* 'images/photo2.jpg'
|
|
24172
|
-
* ]);
|
|
24173
|
-
* ```
|
|
24174
|
-
*/
|
|
24175
|
-
async deleteMultiple(paths) {
|
|
24176
|
-
await Promise.all(paths.map((path) => this.delete(path)));
|
|
24177
|
-
}
|
|
24178
|
-
// ===========================================================================
|
|
24179
|
-
// LIST
|
|
24180
|
-
// ===========================================================================
|
|
24181
|
-
/**
|
|
24182
|
-
* Lista archivos en un directorio.
|
|
24183
|
-
*
|
|
24184
|
-
* @param path - Ruta del directorio
|
|
24185
|
-
* @returns Lista de rutas de archivos
|
|
24186
|
-
*
|
|
24187
|
-
* @example
|
|
24188
|
-
* ```typescript
|
|
24189
|
-
* const result = await storage.list('images/');
|
|
24190
|
-
* console.log(result.items); // ['images/photo1.jpg', 'images/photo2.jpg']
|
|
24191
|
-
* ```
|
|
24192
|
-
*/
|
|
24193
|
-
async list(path) {
|
|
24194
|
-
try {
|
|
24195
|
-
const storageRef = ref(this.storage, path);
|
|
24196
|
-
const result = await listAll(storageRef);
|
|
24197
|
-
return {
|
|
24198
|
-
items: result.items.map((item) => item.fullPath),
|
|
24199
|
-
nextPageToken: undefined, // listAll no soporta paginación
|
|
24200
|
-
};
|
|
24201
|
-
}
|
|
24202
|
-
catch (error) {
|
|
24203
|
-
throw new Error(this.getErrorMessage(error));
|
|
24204
|
-
}
|
|
24205
|
-
}
|
|
24206
|
-
// ===========================================================================
|
|
24207
|
-
// UTILIDADES
|
|
24208
|
-
// ===========================================================================
|
|
24209
|
-
/**
|
|
24210
|
-
* Genera un nombre de archivo único con timestamp.
|
|
24211
|
-
*
|
|
24212
|
-
* @param originalName - Nombre original del archivo
|
|
24213
|
-
* @param prefix - Prefijo opcional
|
|
24214
|
-
* @returns Nombre único
|
|
24215
|
-
*
|
|
24216
|
-
* @example
|
|
24217
|
-
* ```typescript
|
|
24218
|
-
* const uniqueName = storage.generateFileName('photo.jpg', 'user123');
|
|
24219
|
-
* // => 'user123_1703091234567_photo.jpg'
|
|
24220
|
-
* ```
|
|
24221
|
-
*/
|
|
24222
|
-
generateFileName(originalName, prefix) {
|
|
24223
|
-
const timestamp = Date.now();
|
|
24224
|
-
const sanitizedName = originalName.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
24225
|
-
if (prefix) {
|
|
24226
|
-
return `${prefix}_${timestamp}_${sanitizedName}`;
|
|
24227
|
-
}
|
|
24228
|
-
return `${timestamp}_${sanitizedName}`;
|
|
24229
|
-
}
|
|
24230
|
-
/**
|
|
24231
|
-
* Genera una ruta única para un archivo.
|
|
24232
|
-
*
|
|
24233
|
-
* @param directory - Directorio base
|
|
24234
|
-
* @param originalName - Nombre original
|
|
24235
|
-
* @param prefix - Prefijo opcional
|
|
24236
|
-
* @returns Ruta completa única
|
|
24237
|
-
*
|
|
24238
|
-
* @example
|
|
24239
|
-
* ```typescript
|
|
24240
|
-
* const path = storage.generatePath('uploads', 'photo.jpg', 'user123');
|
|
24241
|
-
* // => 'uploads/user123_1703091234567_photo.jpg'
|
|
24242
|
-
* ```
|
|
24243
|
-
*/
|
|
24244
|
-
generatePath(directory, originalName, prefix) {
|
|
24245
|
-
const fileName = this.generateFileName(originalName, prefix);
|
|
24246
|
-
const cleanDir = directory.replace(/\/+$/, ''); // Remover / final
|
|
24247
|
-
return `${cleanDir}/${fileName}`;
|
|
24248
|
-
}
|
|
24249
|
-
/**
|
|
24250
|
-
* Obtiene la extensión de un archivo.
|
|
24251
|
-
*
|
|
24252
|
-
* @param filename - Nombre del archivo
|
|
24253
|
-
* @returns Extensión (sin el punto)
|
|
24254
|
-
*/
|
|
24255
|
-
getExtension(filename) {
|
|
24256
|
-
const parts = filename.split('.');
|
|
24257
|
-
return parts.length > 1 ? parts.pop().toLowerCase() : '';
|
|
24258
|
-
}
|
|
24259
|
-
/**
|
|
24260
|
-
* Verifica si un archivo es una imagen basándose en su extensión.
|
|
24261
|
-
*/
|
|
24262
|
-
isImage(filename) {
|
|
24263
|
-
const ext = this.getExtension(filename);
|
|
24264
|
-
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
|
24265
|
-
}
|
|
24266
|
-
/**
|
|
24267
|
-
* Verifica si un archivo es un documento.
|
|
24268
|
-
*/
|
|
24269
|
-
isDocument(filename) {
|
|
24270
|
-
const ext = this.getExtension(filename);
|
|
24271
|
-
return ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(ext);
|
|
24272
|
-
}
|
|
24273
|
-
// ===========================================================================
|
|
24274
|
-
// MÉTODOS PRIVADOS
|
|
24275
|
-
// ===========================================================================
|
|
24276
|
-
/**
|
|
24277
|
-
* Mapea el estado de la tarea de upload
|
|
24278
|
-
*/
|
|
24279
|
-
mapTaskState(state) {
|
|
24280
|
-
switch (state) {
|
|
24281
|
-
case 'running':
|
|
24282
|
-
return 'running';
|
|
24283
|
-
case 'paused':
|
|
24284
|
-
return 'paused';
|
|
24285
|
-
case 'success':
|
|
24286
|
-
return 'success';
|
|
24287
|
-
case 'canceled':
|
|
24288
|
-
return 'canceled';
|
|
24289
|
-
case 'error':
|
|
24290
|
-
return 'error';
|
|
24291
|
-
default:
|
|
24292
|
-
return 'running';
|
|
24293
|
-
}
|
|
24294
|
-
}
|
|
24295
|
-
/**
|
|
24296
|
-
* Convierte errores de Storage a mensajes en español
|
|
24297
|
-
*/
|
|
24298
|
-
getErrorMessage(error) {
|
|
24299
|
-
if (error instanceof Error) {
|
|
24300
|
-
const code = error.code;
|
|
24301
|
-
switch (code) {
|
|
24302
|
-
case 'storage/object-not-found':
|
|
24303
|
-
return 'El archivo no existe';
|
|
24304
|
-
case 'storage/unauthorized':
|
|
24305
|
-
return 'No tienes permiso para acceder a este archivo';
|
|
24306
|
-
case 'storage/canceled':
|
|
24307
|
-
return 'La operación fue cancelada';
|
|
24308
|
-
case 'storage/quota-exceeded':
|
|
24309
|
-
return 'Se ha excedido la cuota de almacenamiento';
|
|
24310
|
-
case 'storage/invalid-checksum':
|
|
24311
|
-
return 'El archivo está corrupto';
|
|
24312
|
-
case 'storage/retry-limit-exceeded':
|
|
24313
|
-
return 'Error de conexión. Intenta de nuevo';
|
|
24314
|
-
case 'storage/invalid-url':
|
|
24315
|
-
return 'URL de archivo inválida';
|
|
24316
|
-
case 'storage/invalid-argument':
|
|
24317
|
-
return 'Argumento inválido';
|
|
24318
|
-
default:
|
|
24319
|
-
return error.message || 'Error de almacenamiento desconocido';
|
|
24320
|
-
}
|
|
24321
|
-
}
|
|
24322
|
-
return 'Error de almacenamiento desconocido';
|
|
24323
|
-
}
|
|
24324
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
24325
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StorageService, providedIn: 'root' }); }
|
|
24326
|
-
}
|
|
24327
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: StorageService, decorators: [{
|
|
24328
|
-
type: Injectable,
|
|
24329
|
-
args: [{ providedIn: 'root' }]
|
|
24330
|
-
}] });
|
|
24331
|
-
|
|
24332
|
-
/**
|
|
24333
|
-
* Messaging Service (FCM)
|
|
24334
|
-
*
|
|
24335
|
-
* Servicio para Firebase Cloud Messaging (Push Notifications).
|
|
24336
|
-
* Permite solicitar permisos, obtener tokens, escuchar mensajes y manejar
|
|
24337
|
-
* navegación (deep linking) cuando el usuario toca una notificación.
|
|
24338
|
-
*/
|
|
24339
|
-
/**
|
|
24340
|
-
* Servicio para Firebase Cloud Messaging (FCM).
|
|
24341
|
-
*
|
|
24342
|
-
* Permite recibir notificaciones push en la aplicación web.
|
|
24343
|
-
* Requiere VAPID key configurada en ValtechFirebaseConfig.
|
|
24344
|
-
*
|
|
24345
|
-
* @example
|
|
24346
|
-
* ```typescript
|
|
24347
|
-
* @Component({...})
|
|
24348
|
-
* export class NotificationComponent {
|
|
24349
|
-
* private messaging = inject(MessagingService);
|
|
24350
|
-
*
|
|
24351
|
-
* token = signal<string | null>(null);
|
|
24352
|
-
*
|
|
24353
|
-
* async enableNotifications() {
|
|
24354
|
-
* // Solicitar permiso y obtener token
|
|
24355
|
-
* const token = await this.messaging.requestPermission();
|
|
24356
|
-
*
|
|
24357
|
-
* if (token) {
|
|
24358
|
-
* this.token.set(token);
|
|
24359
|
-
* // Enviar token a tu backend para almacenarlo
|
|
24360
|
-
* await this.backend.registerDeviceToken(token);
|
|
24361
|
-
* }
|
|
24362
|
-
* }
|
|
24363
|
-
*
|
|
24364
|
-
* // Escuchar mensajes en foreground
|
|
24365
|
-
* messages$ = this.messaging.onMessage();
|
|
24366
|
-
* }
|
|
24367
|
-
* ```
|
|
24368
|
-
*/
|
|
24369
|
-
class MessagingService {
|
|
24370
|
-
constructor() {
|
|
24371
|
-
this.messaging = inject(Messaging, { optional: true });
|
|
24372
|
-
this.config = inject(VALTECH_FIREBASE_CONFIG);
|
|
24373
|
-
this.platformId = inject(PLATFORM_ID);
|
|
24374
|
-
this.ngZone = inject(NgZone);
|
|
24375
|
-
this.messageSubject = new Subject();
|
|
24376
|
-
this.notificationClickSubject = new Subject();
|
|
24377
|
-
this.stateSubject = new BehaviorSubject({
|
|
24378
|
-
token: null,
|
|
24379
|
-
permission: 'default',
|
|
24380
|
-
isSupported: false,
|
|
24381
|
-
});
|
|
24382
|
-
this.initializeMessaging();
|
|
24383
|
-
}
|
|
24384
|
-
// ===========================================================================
|
|
24385
|
-
// INICIALIZACIÓN
|
|
24386
|
-
// ===========================================================================
|
|
24387
|
-
/**
|
|
24388
|
-
* Inicializa el servicio de messaging
|
|
24389
|
-
*/
|
|
24390
|
-
async initializeMessaging() {
|
|
24391
|
-
if (!isPlatformBrowser(this.platformId)) {
|
|
24392
|
-
return;
|
|
24393
|
-
}
|
|
24394
|
-
const supported = await this.checkSupport();
|
|
24395
|
-
const permission = this.getPermissionState();
|
|
24396
|
-
this.stateSubject.next({
|
|
24397
|
-
...this.stateSubject.value,
|
|
24398
|
-
isSupported: supported,
|
|
24399
|
-
permission,
|
|
24400
|
-
});
|
|
24401
|
-
// Si ya tiene permiso, configurar listeners
|
|
24402
|
-
if (supported && permission === 'granted') {
|
|
24403
|
-
this.setupMessageListener();
|
|
24404
|
-
}
|
|
24405
|
-
// Escuchar mensajes del Service Worker (clicks en notificaciones background)
|
|
24406
|
-
this.setupServiceWorkerListener();
|
|
24407
|
-
}
|
|
24408
|
-
/**
|
|
24409
|
-
* Configura listener para mensajes del Service Worker.
|
|
24410
|
-
* Recibe eventos cuando el usuario hace click en una notificación background.
|
|
24411
|
-
*/
|
|
24412
|
-
setupServiceWorkerListener() {
|
|
24413
|
-
if (!isPlatformBrowser(this.platformId) || !('serviceWorker' in navigator)) {
|
|
24414
|
-
return;
|
|
24415
|
-
}
|
|
24416
|
-
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
24417
|
-
// Verificar que es un mensaje de notificación click
|
|
24418
|
-
if (event.data?.type === 'NOTIFICATION_CLICK') {
|
|
24419
|
-
this.ngZone.run(() => {
|
|
24420
|
-
const notification = event.data.notification;
|
|
24421
|
-
const action = this.extractActionFromData(notification.data);
|
|
24422
|
-
this.notificationClickSubject.next({
|
|
24423
|
-
notification,
|
|
24424
|
-
action,
|
|
24425
|
-
timestamp: new Date(),
|
|
24426
|
-
});
|
|
24427
|
-
});
|
|
24428
|
-
}
|
|
24429
|
-
});
|
|
24430
|
-
}
|
|
24431
|
-
/**
|
|
24432
|
-
* Verifica si FCM está soportado en el navegador actual
|
|
24433
|
-
*/
|
|
24434
|
-
async checkSupport() {
|
|
24435
|
-
if (!isPlatformBrowser(this.platformId)) {
|
|
24436
|
-
return false;
|
|
24437
|
-
}
|
|
24438
|
-
// Verificar APIs necesarias
|
|
24439
|
-
if (!('Notification' in window)) {
|
|
24440
|
-
return false;
|
|
24441
|
-
}
|
|
24442
|
-
if (!('serviceWorker' in navigator)) {
|
|
24443
|
-
return false;
|
|
24444
|
-
}
|
|
24445
|
-
// Verificar que messaging esté disponible
|
|
24446
|
-
if (!this.messaging) {
|
|
24447
|
-
return false;
|
|
24448
|
-
}
|
|
24449
|
-
return true;
|
|
24450
|
-
}
|
|
24451
|
-
// ===========================================================================
|
|
24452
|
-
// PERMISOS Y TOKEN
|
|
24453
|
-
// ===========================================================================
|
|
24454
|
-
/**
|
|
24455
|
-
* Solicita permiso de notificaciones y obtiene el token FCM.
|
|
24456
|
-
*
|
|
24457
|
-
* @returns Token FCM si se otorgó permiso, null si se denegó
|
|
24458
|
-
*
|
|
24459
|
-
* @example
|
|
24460
|
-
* ```typescript
|
|
24461
|
-
* const token = await messaging.requestPermission();
|
|
24462
|
-
* if (token) {
|
|
24463
|
-
* console.log('Token FCM:', token);
|
|
24464
|
-
* // Enviar a backend
|
|
24465
|
-
* } else {
|
|
24466
|
-
* console.log('Permiso denegado o no soportado');
|
|
24467
|
-
* }
|
|
24468
|
-
* ```
|
|
24469
|
-
*/
|
|
24470
|
-
async requestPermission() {
|
|
24471
|
-
if (!await this.isSupported()) {
|
|
24472
|
-
console.warn('FCM no está soportado en este navegador');
|
|
24473
|
-
return null;
|
|
24474
|
-
}
|
|
24475
|
-
try {
|
|
24476
|
-
// Solicitar permiso de notificaciones
|
|
24477
|
-
const permission = await Notification.requestPermission();
|
|
24478
|
-
this.stateSubject.next({
|
|
24479
|
-
...this.stateSubject.value,
|
|
24480
|
-
permission: permission,
|
|
24481
|
-
});
|
|
24482
|
-
if (permission !== 'granted') {
|
|
24483
|
-
console.warn('Permiso de notificaciones denegado');
|
|
24484
|
-
return null;
|
|
24485
|
-
}
|
|
24486
|
-
// Obtener token FCM
|
|
24487
|
-
const token = await this.getToken();
|
|
24488
|
-
if (token) {
|
|
24489
|
-
// Configurar listener de mensajes
|
|
24490
|
-
this.setupMessageListener();
|
|
24491
|
-
}
|
|
24492
|
-
return token;
|
|
24493
|
-
}
|
|
24494
|
-
catch (error) {
|
|
24495
|
-
console.error('Error solicitando permiso de notificaciones:', error);
|
|
24496
|
-
return null;
|
|
24497
|
-
}
|
|
24498
|
-
}
|
|
24499
|
-
/**
|
|
24500
|
-
* Obtiene el token FCM actual (sin solicitar permiso).
|
|
24501
|
-
*
|
|
24502
|
-
* @returns Token FCM si está disponible, null si no
|
|
24503
|
-
*
|
|
24504
|
-
* @example
|
|
24505
|
-
* ```typescript
|
|
24506
|
-
* const token = await messaging.getToken();
|
|
24507
|
-
* ```
|
|
24508
|
-
*/
|
|
24509
|
-
async getToken() {
|
|
24510
|
-
if (!this.messaging) {
|
|
24511
|
-
return null;
|
|
24512
|
-
}
|
|
24513
|
-
const vapidKey = this.config.messagingVapidKey;
|
|
24514
|
-
if (!vapidKey) {
|
|
24515
|
-
console.warn('VAPID key no configurada. FCM no funcionará.');
|
|
24516
|
-
return null;
|
|
24517
|
-
}
|
|
24518
|
-
try {
|
|
24519
|
-
const token = await getToken(this.messaging, { vapidKey });
|
|
24520
|
-
this.stateSubject.next({
|
|
24521
|
-
...this.stateSubject.value,
|
|
24522
|
-
token,
|
|
24523
|
-
});
|
|
24524
|
-
return token;
|
|
24525
|
-
}
|
|
24526
|
-
catch (error) {
|
|
24527
|
-
console.error('Error obteniendo token FCM:', error);
|
|
24528
|
-
return null;
|
|
24529
|
-
}
|
|
24530
|
-
}
|
|
24531
|
-
/**
|
|
24532
|
-
* Elimina el token FCM actual (unsubscribe de notificaciones).
|
|
24533
|
-
*
|
|
24534
|
-
* @example
|
|
24535
|
-
* ```typescript
|
|
24536
|
-
* await messaging.deleteToken();
|
|
24537
|
-
* console.log('Token eliminado, no recibirá más notificaciones');
|
|
24538
|
-
* ```
|
|
24539
|
-
*/
|
|
24540
|
-
async deleteToken() {
|
|
24541
|
-
if (!this.messaging) {
|
|
24542
|
-
return;
|
|
24543
|
-
}
|
|
24544
|
-
try {
|
|
24545
|
-
await deleteToken(this.messaging);
|
|
24546
|
-
this.stateSubject.next({
|
|
24547
|
-
...this.stateSubject.value,
|
|
24548
|
-
token: null,
|
|
24549
|
-
});
|
|
24550
|
-
// Limpiar listener de mensajes
|
|
24551
|
-
if (this.unsubscribeOnMessage) {
|
|
24552
|
-
this.unsubscribeOnMessage();
|
|
24553
|
-
this.unsubscribeOnMessage = undefined;
|
|
24554
|
-
}
|
|
24555
|
-
}
|
|
24556
|
-
catch (error) {
|
|
24557
|
-
console.error('Error eliminando token FCM:', error);
|
|
24558
|
-
throw new Error('No se pudo eliminar el token de notificaciones');
|
|
24559
|
-
}
|
|
24560
|
-
}
|
|
24561
|
-
// ===========================================================================
|
|
24562
|
-
// MENSAJES
|
|
24563
|
-
// ===========================================================================
|
|
24564
|
-
/**
|
|
24565
|
-
* Observable de mensajes recibidos en foreground.
|
|
24566
|
-
*
|
|
24567
|
-
* IMPORTANTE: Los mensajes en background son manejados por el Service Worker.
|
|
24568
|
-
*
|
|
24569
|
-
* @returns Observable que emite cuando llega un mensaje en foreground
|
|
24570
|
-
*
|
|
24571
|
-
* @example
|
|
24572
|
-
* ```typescript
|
|
24573
|
-
* messaging.onMessage().subscribe(payload => {
|
|
24574
|
-
* console.log('Mensaje recibido:', payload);
|
|
24575
|
-
* // Mostrar notificación custom o actualizar UI
|
|
24576
|
-
* });
|
|
24577
|
-
* ```
|
|
24578
|
-
*/
|
|
24579
|
-
onMessage() {
|
|
24580
|
-
return this.messageSubject.asObservable();
|
|
24581
|
-
}
|
|
24582
|
-
/**
|
|
24583
|
-
* Configura el listener de mensajes en foreground
|
|
24584
|
-
*/
|
|
24585
|
-
setupMessageListener() {
|
|
24586
|
-
if (!this.messaging || this.unsubscribeOnMessage) {
|
|
24587
|
-
return;
|
|
24588
|
-
}
|
|
24589
|
-
this.unsubscribeOnMessage = onMessage(this.messaging, (payload) => {
|
|
24590
|
-
const notification = {
|
|
24591
|
-
title: payload.notification?.title,
|
|
24592
|
-
body: payload.notification?.body,
|
|
24593
|
-
image: payload.notification?.image,
|
|
24594
|
-
data: payload.data,
|
|
24595
|
-
messageId: payload.messageId,
|
|
24596
|
-
};
|
|
24597
|
-
this.messageSubject.next(notification);
|
|
24598
|
-
});
|
|
24599
|
-
}
|
|
24600
|
-
// ===========================================================================
|
|
24601
|
-
// ESTADO Y UTILIDADES
|
|
24602
|
-
// ===========================================================================
|
|
24603
|
-
/**
|
|
24604
|
-
* Obtiene el estado actual del permiso de notificaciones.
|
|
24605
|
-
*
|
|
24606
|
-
* @returns 'granted' | 'denied' | 'default'
|
|
24607
|
-
*
|
|
24608
|
-
* @example
|
|
24609
|
-
* ```typescript
|
|
24610
|
-
* const permission = messaging.getPermissionState();
|
|
24611
|
-
* if (permission === 'granted') {
|
|
24612
|
-
* // Ya tiene permiso
|
|
24613
|
-
* } else if (permission === 'default') {
|
|
24614
|
-
* // Puede solicitar permiso
|
|
24615
|
-
* } else {
|
|
24616
|
-
* // Denegado, debe habilitar manualmente
|
|
24617
|
-
* }
|
|
24618
|
-
* ```
|
|
24619
|
-
*/
|
|
24620
|
-
getPermissionState() {
|
|
24621
|
-
if (!isPlatformBrowser(this.platformId)) {
|
|
24622
|
-
return 'default';
|
|
24623
|
-
}
|
|
24624
|
-
if (!('Notification' in window)) {
|
|
24625
|
-
return 'denied';
|
|
24626
|
-
}
|
|
24627
|
-
return Notification.permission;
|
|
24628
|
-
}
|
|
24629
|
-
/**
|
|
24630
|
-
* Verifica si FCM está soportado en el navegador actual.
|
|
24631
|
-
*
|
|
24632
|
-
* @returns true si FCM está soportado
|
|
24633
|
-
*
|
|
24634
|
-
* @example
|
|
24635
|
-
* ```typescript
|
|
24636
|
-
* if (await messaging.isSupported()) {
|
|
24637
|
-
* // Puede usar notificaciones push
|
|
24638
|
-
* } else {
|
|
24639
|
-
* // Navegador no soporta o no tiene Service Worker
|
|
24640
|
-
* }
|
|
24641
|
-
* ```
|
|
24642
|
-
*/
|
|
24643
|
-
async isSupported() {
|
|
24644
|
-
return this.checkSupport();
|
|
24645
|
-
}
|
|
24646
|
-
/**
|
|
24647
|
-
* Obtiene el token actual sin hacer request.
|
|
24648
|
-
*
|
|
24649
|
-
* @returns Token almacenado o null
|
|
24650
|
-
*/
|
|
24651
|
-
get currentToken() {
|
|
24652
|
-
return this.stateSubject.value.token;
|
|
24653
|
-
}
|
|
24654
|
-
/**
|
|
24655
|
-
* Observable del estado completo del servicio de messaging.
|
|
24656
|
-
*/
|
|
24657
|
-
get state$() {
|
|
24658
|
-
return this.stateSubject.asObservable();
|
|
24659
|
-
}
|
|
24660
|
-
/**
|
|
24661
|
-
* Verifica si el usuario ya otorgó permiso de notificaciones.
|
|
24662
|
-
*/
|
|
24663
|
-
get hasPermission() {
|
|
24664
|
-
return this.stateSubject.value.permission === 'granted';
|
|
24665
|
-
}
|
|
24666
|
-
// ===========================================================================
|
|
24667
|
-
// DEEP LINKING / NAVEGACIÓN
|
|
24668
|
-
// ===========================================================================
|
|
24669
|
-
/**
|
|
24670
|
-
* Observable de clicks en notificaciones.
|
|
24671
|
-
*
|
|
24672
|
-
* Emite cuando el usuario hace click en una notificación (foreground o background).
|
|
24673
|
-
* Usa este observable para navegar a la página correspondiente.
|
|
24674
|
-
*
|
|
24675
|
-
* @returns Observable que emite NotificationClickEvent
|
|
24676
|
-
*
|
|
24677
|
-
* @example
|
|
24678
|
-
* ```typescript
|
|
24679
|
-
* @Component({...})
|
|
24680
|
-
* export class AppComponent {
|
|
24681
|
-
* private messaging = inject(MessagingService);
|
|
24682
|
-
* private router = inject(Router);
|
|
24683
|
-
*
|
|
24684
|
-
* constructor() {
|
|
24685
|
-
* this.messaging.onNotificationClick().subscribe(event => {
|
|
24686
|
-
* if (event.action.route) {
|
|
24687
|
-
* this.router.navigate([event.action.route], {
|
|
24688
|
-
* queryParams: event.action.queryParams
|
|
24689
|
-
* });
|
|
24690
|
-
* }
|
|
24691
|
-
* });
|
|
24692
|
-
* }
|
|
24693
|
-
* }
|
|
24694
|
-
* ```
|
|
24695
|
-
*/
|
|
24696
|
-
onNotificationClick() {
|
|
24697
|
-
return this.notificationClickSubject.asObservable();
|
|
24698
|
-
}
|
|
24699
|
-
/**
|
|
24700
|
-
* Extrae la acción de navegación de los datos de una notificación.
|
|
24701
|
-
*
|
|
24702
|
-
* Busca campos específicos en el payload de datos:
|
|
24703
|
-
* - `route`: Ruta interna de la app (ej: '/orders/123')
|
|
24704
|
-
* - `url`: URL externa (ej: 'https://example.com')
|
|
24705
|
-
* - `action_type`: Tipo de acción personalizada
|
|
24706
|
-
* - Campos con prefijo `action_`: Datos adicionales
|
|
24707
|
-
*
|
|
24708
|
-
* @param data - Datos del payload de la notificación
|
|
24709
|
-
* @returns Acción de navegación extraída
|
|
24710
|
-
*
|
|
24711
|
-
* @example
|
|
24712
|
-
* ```typescript
|
|
24713
|
-
* // Payload desde el backend:
|
|
24714
|
-
* // { route: '/orders/123', action_type: 'view_order', action_orderId: '123' }
|
|
24715
|
-
*
|
|
24716
|
-
* const action = messaging.extractActionFromData(notification.data);
|
|
24717
|
-
* // { route: '/orders/123', actionType: 'view_order', actionData: { orderId: '123' } }
|
|
24718
|
-
* ```
|
|
24719
|
-
*/
|
|
24720
|
-
extractActionFromData(data) {
|
|
24721
|
-
if (!data) {
|
|
24722
|
-
return {};
|
|
24723
|
-
}
|
|
24724
|
-
const action = {};
|
|
24725
|
-
// Ruta interna
|
|
24726
|
-
if (data['route']) {
|
|
24727
|
-
action.route = data['route'];
|
|
24728
|
-
}
|
|
24729
|
-
// URL externa
|
|
24730
|
-
if (data['url']) {
|
|
24731
|
-
action.url = data['url'];
|
|
24732
|
-
}
|
|
24733
|
-
// Tipo de acción
|
|
24734
|
-
if (data['action_type']) {
|
|
24735
|
-
action.actionType = data['action_type'];
|
|
24736
|
-
}
|
|
24737
|
-
// Query params (puede venir como JSON string)
|
|
24738
|
-
if (data['query_params']) {
|
|
24739
|
-
try {
|
|
24740
|
-
action.queryParams = JSON.parse(data['query_params']);
|
|
24741
|
-
}
|
|
24742
|
-
catch {
|
|
24743
|
-
// Si no es JSON válido, intentar parsear como key=value
|
|
24744
|
-
action.queryParams = this.parseQueryString(data['query_params']);
|
|
24745
|
-
}
|
|
24746
|
-
}
|
|
24747
|
-
// Datos adicionales con prefijo action_
|
|
24748
|
-
const actionData = {};
|
|
24749
|
-
for (const [key, value] of Object.entries(data)) {
|
|
24750
|
-
if (key.startsWith('action_') && key !== 'action_type') {
|
|
24751
|
-
const cleanKey = key.replace('action_', '');
|
|
24752
|
-
// Intentar parsear JSON si es posible
|
|
24753
|
-
try {
|
|
24754
|
-
actionData[cleanKey] = JSON.parse(value);
|
|
24755
|
-
}
|
|
24756
|
-
catch {
|
|
24757
|
-
actionData[cleanKey] = value;
|
|
24758
|
-
}
|
|
24759
|
-
}
|
|
24760
|
-
}
|
|
24761
|
-
if (Object.keys(actionData).length > 0) {
|
|
24762
|
-
action.actionData = actionData;
|
|
24763
|
-
}
|
|
24764
|
-
return action;
|
|
24765
|
-
}
|
|
24766
|
-
/**
|
|
24767
|
-
* Emite manualmente un evento de click en notificación.
|
|
24768
|
-
*
|
|
24769
|
-
* Útil para manejar clicks en notificaciones foreground donde
|
|
24770
|
-
* la app decide mostrar un banner custom.
|
|
24771
|
-
*
|
|
24772
|
-
* @param notification - Payload de la notificación
|
|
24773
|
-
*
|
|
24774
|
-
* @example
|
|
24775
|
-
* ```typescript
|
|
24776
|
-
* messaging.onMessage().subscribe(notification => {
|
|
24777
|
-
* // Mostrar banner custom
|
|
24778
|
-
* this.showBanner(notification, () => {
|
|
24779
|
-
* // Usuario hizo click en el banner
|
|
24780
|
-
* messaging.handleNotificationClick(notification);
|
|
24781
|
-
* });
|
|
24782
|
-
* });
|
|
24783
|
-
* ```
|
|
24784
|
-
*/
|
|
24785
|
-
handleNotificationClick(notification) {
|
|
24786
|
-
const action = this.extractActionFromData(notification.data);
|
|
24787
|
-
this.notificationClickSubject.next({
|
|
24788
|
-
notification,
|
|
24789
|
-
action,
|
|
24790
|
-
timestamp: new Date(),
|
|
24791
|
-
});
|
|
24792
|
-
}
|
|
24793
|
-
/**
|
|
24794
|
-
* Verifica si una notificación tiene acción de navegación.
|
|
24795
|
-
*
|
|
24796
|
-
* @param data - Datos del payload
|
|
24797
|
-
* @returns true si tiene route o url
|
|
24798
|
-
*/
|
|
24799
|
-
hasNavigationAction(data) {
|
|
24800
|
-
if (!data)
|
|
24801
|
-
return false;
|
|
24802
|
-
return !!(data['route'] || data['url']);
|
|
24803
|
-
}
|
|
24804
|
-
/**
|
|
24805
|
-
* Parsea un query string en un objeto.
|
|
24806
|
-
*/
|
|
24807
|
-
parseQueryString(queryString) {
|
|
24808
|
-
const params = {};
|
|
24809
|
-
if (!queryString)
|
|
24810
|
-
return params;
|
|
24811
|
-
// Remover ? inicial si existe
|
|
24812
|
-
const cleanQuery = queryString.startsWith('?') ? queryString.slice(1) : queryString;
|
|
24813
|
-
for (const pair of cleanQuery.split('&')) {
|
|
24814
|
-
const [key, value] = pair.split('=');
|
|
24815
|
-
if (key) {
|
|
24816
|
-
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
24817
|
-
}
|
|
24818
|
-
}
|
|
24819
|
-
return params;
|
|
24820
|
-
}
|
|
24821
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessagingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
24822
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessagingService, providedIn: 'root' }); }
|
|
24823
|
-
}
|
|
24824
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessagingService, decorators: [{
|
|
24825
|
-
type: Injectable,
|
|
24826
|
-
args: [{ providedIn: 'root' }]
|
|
24827
|
-
}], ctorParameters: () => [] });
|
|
24828
|
-
|
|
24829
|
-
/**
|
|
24830
|
-
* Firebase Services
|
|
24831
|
-
*
|
|
24832
|
-
* Servicios reutilizables para integración con Firebase.
|
|
24833
|
-
*
|
|
24834
|
-
* @example
|
|
24835
|
-
* ```typescript
|
|
24836
|
-
* // En main.ts
|
|
24837
|
-
* import { provideValtechFirebase } from 'valtech-components';
|
|
24838
|
-
*
|
|
24839
|
-
* bootstrapApplication(AppComponent, {
|
|
24840
|
-
* providers: [
|
|
24841
|
-
* provideValtechFirebase({
|
|
24842
|
-
* firebase: environment.firebase,
|
|
24843
|
-
* persistence: true,
|
|
24844
|
-
* }),
|
|
24845
|
-
* ],
|
|
24846
|
-
* });
|
|
24847
|
-
*
|
|
24848
|
-
* // En componentes
|
|
24849
|
-
* import { FirebaseService, FirestoreService } from 'valtech-components';
|
|
24850
|
-
*
|
|
24851
|
-
* @Component({...})
|
|
24852
|
-
* export class MyComponent {
|
|
24853
|
-
* private firebase = inject(FirebaseService);
|
|
24854
|
-
* private firestore = inject(FirestoreService);
|
|
24855
|
-
* }
|
|
24856
|
-
* ```
|
|
24857
|
-
*/
|
|
24858
|
-
// Tipos
|
|
24859
|
-
|
|
24860
22257
|
/**
|
|
24861
22258
|
* Create a reactive content observable from a content key.
|
|
24862
22259
|
* This is the primary utility for the `fromContent` pattern with unified support
|
|
@@ -25050,5 +22447,5 @@ function createContentHelper(langService, className) {
|
|
|
25050
22447
|
* Generated bundle index. Do not edit.
|
|
25051
22448
|
*/
|
|
25052
22449
|
|
|
25053
|
-
export { ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AlertBoxComponent, ArticleBuilder, ArticleComponent, 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_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, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent,
|
|
22450
|
+
export { ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AlertBoxComponent, ArticleBuilder, ArticleComponent, 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_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, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FunHeaderComponent, GlobalContent, GlowCardComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfoComponent, InputType, ItemListComponent, LANGUAGES, LangService, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, LocalStorageService, MODAL_SIZES, MOTION, MenuComponent, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, 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, 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, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, SwipeCarouselComponent, TabsComponent, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextContent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, ToolbarActionType, ToolbarComponent, ValtechConfigService, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, content, createButtonProps, createContentHelper, createDisplayProps, createGlowCardProps, createNumberFromToField, createReactiveContentMetadata, createTextProps, createTitleProps, extractContentConfig, extractContentConfigWithInterpolation, fromContent, fromContentWithInterpolation, fromMultipleContent, globalContentData, goToTop, interpolateContent, interpolateStaticContent, isAtEnd, maxLength, replaceSpecialChars, resolveColor, resolveInputDefaultValue, shouldUseReactiveContent, shouldUseReactiveContentWithInterpolation };
|
|
25054
22451
|
//# sourceMappingURL=valtech-components.mjs.map
|