valtech-components 2.0.417 → 2.0.419

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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, makeEnvironmentProviders, PLATFORM_ID, NgZone } from '@angular/core';
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, isPlatformBrowser } from '@angular/common';
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, Subject } from 'rxjs';
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, FirebaseService, FirestoreCollection, FirestoreService, 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, MessagingService, 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, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RightsFooterComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabsComponent, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextContent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, ToolbarActionType, ToolbarComponent, VALTECH_FIREBASE_CONFIG, ValtechConfigService, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, buildPath, content, createButtonProps, createContentHelper, createDisplayProps, createGlowCardProps, createNumberFromToField, createReactiveContentMetadata, createTextProps, createTitleProps, extractContentConfig, extractContentConfigWithInterpolation, extractPathParams, fromContent, fromContentWithInterpolation, fromMultipleContent, getCollectionPath, getDocumentId, globalContentData, goToTop, hasEmulators, interpolateContent, interpolateStaticContent, isAtEnd, isCollectionPath, isDocumentPath, isValidPath, joinPath, maxLength, provideValtechFirebase, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, shouldUseReactiveContent, shouldUseReactiveContentWithInterpolation };
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