ng-easycommerce-v18 0.3.22-beta.1 → 0.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/esm2022/lib/classes/filters/attributes-filter.mjs +74 -4
- package/esm2022/lib/classes/filters/category-filter.mjs +105 -26
- package/esm2022/lib/classes/filters/filter-factory.mjs +7 -3
- package/esm2022/lib/classes/filters/filter.mjs +27 -2
- package/esm2022/lib/classes/filters/price_range-filter.mjs +3 -3
- package/esm2022/lib/constants/core.constants.service.mjs +12 -1
- package/esm2022/lib/ec-components/blocks-ec/block-products-ec/block-products-ec.component.mjs +5 -3
- package/esm2022/lib/ec-components/collection-ec/collection-ec.component.mjs +41 -17
- package/esm2022/lib/ec-components/filters-ec/filters-ec.component.mjs +42 -9
- package/esm2022/lib/ec-components/header-ec/header-ec.component.mjs +42 -27
- package/esm2022/lib/ec-components/price-range-filter/price-range-filter.component.mjs +13 -2
- package/esm2022/lib/ec-components/related-products-ec/related-products-ec.component.mjs +6 -4
- package/esm2022/lib/ec-components/widgets-ec/magnizoom-ec/magnizoom-ec.component.mjs +4 -2
- package/esm2022/lib/ec-services/analytics/facebook-pixel.service.mjs +4 -2
- package/esm2022/lib/ec-services/analytics/google-analytics.service.mjs +4 -2
- package/esm2022/lib/ec-services/filters.service.mjs +124 -18
- package/esm2022/lib/ec-services/options.service.mjs +27 -3
- package/esm2022/lib/ec-services/pagination.service.mjs +70 -22
- package/esm2022/lib/ec-services/products.service.mjs +5 -3
- package/fesm2022/ng-easycommerce-v18.mjs +586 -126
- package/fesm2022/ng-easycommerce-v18.mjs.map +1 -1
- package/lib/classes/filters/attributes-filter.d.ts +24 -0
- package/lib/classes/filters/category-filter.d.ts +30 -3
- package/lib/constants/core.constants.service.d.ts +7 -0
- package/lib/ec-components/collection-ec/collection-ec.component.d.ts +5 -4
- package/lib/ec-components/filters-ec/filters-ec.component.d.ts +13 -0
- package/lib/ec-components/price-range-filter/price-range-filter.component.d.ts +2 -0
- package/lib/ec-services/filters.service.d.ts +18 -1
- package/lib/ec-services/options.service.d.ts +4 -0
- package/lib/ec-services/pagination.service.d.ts +21 -5
- package/lib/ec-services/products.service.d.ts +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, PLATFORM_ID, RendererFactory2, afterNextRender, signal, EnvironmentInjector, runInInjectionContext, Component, ChangeDetectorRef, HostListener, CUSTOM_ELEMENTS_SCHEMA, Input, Pipe, Injector, EventEmitter, Output, forwardRef, afterRender, ViewChild, Inject, computed, Renderer2, ChangeDetectionStrategy, Directive } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { DOCUMENT, isPlatformBrowser, AsyncPipe, CommonModule, TitleCasePipe, JsonPipe, UpperCasePipe, Location } from '@angular/common';
|
|
5
|
-
import { take, BehaviorSubject, shareReplay, map, catchError, of, filter, ReplaySubject, firstValueFrom, concatMap, throwError, switchMap, combineLatest } from 'rxjs';
|
|
5
|
+
import { take, BehaviorSubject, shareReplay, map, catchError, of, filter, ReplaySubject, firstValueFrom, concatMap, throwError, tap, distinctUntilChanged, switchMap, combineLatest, Subject, takeUntil } from 'rxjs';
|
|
6
6
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
7
7
|
import * as i1$1 from '@ngx-translate/core';
|
|
8
8
|
import { TranslateService, TranslateModule } from '@ngx-translate/core';
|
|
@@ -243,6 +243,11 @@ class CoreConstantsService {
|
|
|
243
243
|
* Guarda la variable window del web browser.
|
|
244
244
|
*/
|
|
245
245
|
window;
|
|
246
|
+
/**
|
|
247
|
+
* Tipo de ruta actual (ej: 'categories', 'attributes', etc).
|
|
248
|
+
* Lo usamos para que PaginationService sepa qué filtro esperar.
|
|
249
|
+
*/
|
|
250
|
+
currentRouteType = null;
|
|
246
251
|
constructor() {
|
|
247
252
|
if (isPlatformBrowser(this.platformId)) {
|
|
248
253
|
this.window = window;
|
|
@@ -520,6 +525,12 @@ class CoreConstantsService {
|
|
|
520
525
|
return this.apiConstants.CHANNEL;
|
|
521
526
|
}
|
|
522
527
|
form_sender = false;
|
|
528
|
+
setCurrentRouteType(type) {
|
|
529
|
+
this.currentRouteType = type;
|
|
530
|
+
}
|
|
531
|
+
getCurrentRouteType() {
|
|
532
|
+
return this.currentRouteType;
|
|
533
|
+
}
|
|
523
534
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CoreConstantsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
524
535
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CoreConstantsService, providedIn: 'root' });
|
|
525
536
|
}
|
|
@@ -539,6 +550,10 @@ class OptionsService {
|
|
|
539
550
|
* Maneja las peticiones a la API
|
|
540
551
|
*/
|
|
541
552
|
connection = inject(ConnectionService);
|
|
553
|
+
/**
|
|
554
|
+
* Platform ID para verificar si estamos en el navegador
|
|
555
|
+
*/
|
|
556
|
+
platformId = inject(PLATFORM_ID);
|
|
542
557
|
/**
|
|
543
558
|
* Constantes del core
|
|
544
559
|
*/
|
|
@@ -649,7 +664,26 @@ class OptionsService {
|
|
|
649
664
|
* @returns
|
|
650
665
|
*/
|
|
651
666
|
removeAccents(str) {
|
|
652
|
-
|
|
667
|
+
if (isPlatformBrowser(this.platformId) && typeof String.prototype.normalize === 'function') {
|
|
668
|
+
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
669
|
+
}
|
|
670
|
+
// Fallback para SSR - remover acentos manualmente
|
|
671
|
+
return str.replace(/[àáâãäå]/g, 'a')
|
|
672
|
+
.replace(/[èéêë]/g, 'e')
|
|
673
|
+
.replace(/[ìíîï]/g, 'i')
|
|
674
|
+
.replace(/[òóôõö]/g, 'o')
|
|
675
|
+
.replace(/[ùúûü]/g, 'u')
|
|
676
|
+
.replace(/[ýÿ]/g, 'y')
|
|
677
|
+
.replace(/[ñ]/g, 'n')
|
|
678
|
+
.replace(/[ç]/g, 'c')
|
|
679
|
+
.replace(/[ÀÁÂÃÄÅ]/g, 'A')
|
|
680
|
+
.replace(/[ÈÉÊË]/g, 'E')
|
|
681
|
+
.replace(/[ÌÍÎÏ]/g, 'I')
|
|
682
|
+
.replace(/[ÒÓÔÕÖ]/g, 'O')
|
|
683
|
+
.replace(/[ÙÚÛÜ]/g, 'U')
|
|
684
|
+
.replace(/[ÝŸ]/g, 'Y')
|
|
685
|
+
.replace(/[Ñ]/g, 'N')
|
|
686
|
+
.replace(/[Ç]/g, 'C');
|
|
653
687
|
}
|
|
654
688
|
/**
|
|
655
689
|
* Realiza un mapeo de los datos para darle un formato mas entendible a la
|
|
@@ -866,7 +900,9 @@ class FacebookPixelService {
|
|
|
866
900
|
this.renderer.appendChild(this.document?.body, new_analityc_script);
|
|
867
901
|
this.enabled = true;
|
|
868
902
|
}
|
|
869
|
-
|
|
903
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
904
|
+
setTimeout(() => this.callEvent('initialize'), 1000);
|
|
905
|
+
}
|
|
870
906
|
}
|
|
871
907
|
/**
|
|
872
908
|
* Ejecuta el evento pasado por parametro.
|
|
@@ -1083,7 +1119,9 @@ class GoogleAnalyticsService {
|
|
|
1083
1119
|
this.renderer.appendChild(this.document?.head, declaration);
|
|
1084
1120
|
this.enabled = true;
|
|
1085
1121
|
}
|
|
1086
|
-
|
|
1122
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
1123
|
+
setTimeout(() => this.startListeningPageViews(gtm_id), 1000);
|
|
1124
|
+
}
|
|
1087
1125
|
}
|
|
1088
1126
|
/**
|
|
1089
1127
|
*
|
|
@@ -2748,6 +2786,31 @@ class User {
|
|
|
2748
2786
|
}
|
|
2749
2787
|
}
|
|
2750
2788
|
|
|
2789
|
+
/**
|
|
2790
|
+
* Función auxiliar para remover acentos de forma segura para SSR
|
|
2791
|
+
*/
|
|
2792
|
+
function safeRemoveAccents(str) {
|
|
2793
|
+
if (typeof window !== 'undefined' && typeof String.prototype.normalize === 'function') {
|
|
2794
|
+
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
2795
|
+
}
|
|
2796
|
+
// Fallback para SSR - remover acentos manualmente
|
|
2797
|
+
return str.replace(/[àáâãäå]/g, 'a')
|
|
2798
|
+
.replace(/[èéêë]/g, 'e')
|
|
2799
|
+
.replace(/[ìíîï]/g, 'i')
|
|
2800
|
+
.replace(/[òóôõö]/g, 'o')
|
|
2801
|
+
.replace(/[ùúûü]/g, 'u')
|
|
2802
|
+
.replace(/[ýÿ]/g, 'y')
|
|
2803
|
+
.replace(/[ñ]/g, 'n')
|
|
2804
|
+
.replace(/[ç]/g, 'c')
|
|
2805
|
+
.replace(/[ÀÁÂÃÄÅ]/g, 'A')
|
|
2806
|
+
.replace(/[ÈÉÊË]/g, 'E')
|
|
2807
|
+
.replace(/[ÌÍÎÏ]/g, 'I')
|
|
2808
|
+
.replace(/[ÒÓÔÕÖ]/g, 'O')
|
|
2809
|
+
.replace(/[ÙÚÛÜ]/g, 'U')
|
|
2810
|
+
.replace(/[ÝŸ]/g, 'Y')
|
|
2811
|
+
.replace(/[Ñ]/g, 'N')
|
|
2812
|
+
.replace(/[Ç]/g, 'C');
|
|
2813
|
+
}
|
|
2751
2814
|
class Filter {
|
|
2752
2815
|
data = [];
|
|
2753
2816
|
multi = false;
|
|
@@ -2767,7 +2830,7 @@ class Filter {
|
|
|
2767
2830
|
throw new Error("Method not implemented.");
|
|
2768
2831
|
}
|
|
2769
2832
|
removeAccents = (str) => {
|
|
2770
|
-
return str
|
|
2833
|
+
return safeRemoveAccents(str);
|
|
2771
2834
|
};
|
|
2772
2835
|
setSelected(element, value) {
|
|
2773
2836
|
//console.log(element, value);
|
|
@@ -2831,17 +2894,24 @@ class Filter {
|
|
|
2831
2894
|
};
|
|
2832
2895
|
}
|
|
2833
2896
|
|
|
2897
|
+
/**
|
|
2898
|
+
* CategoryFilter
|
|
2899
|
+
* - Gestiona el filtro de categorías.
|
|
2900
|
+
* - Mantiene la estructura de categorías (parents/children) y permite:
|
|
2901
|
+
* * Inicializar desde OptionsService.
|
|
2902
|
+
* * Marcar elementos según slug o query param.
|
|
2903
|
+
* * Generar porción de query string (&category=...).
|
|
2904
|
+
*/
|
|
2834
2905
|
class CategoryFilter extends Filter {
|
|
2835
|
-
initialValues;
|
|
2836
2906
|
_optionsService = inject(OptionsService);
|
|
2907
|
+
_filtersService = inject(FiltersService);
|
|
2837
2908
|
data = [];
|
|
2838
2909
|
multi = false;
|
|
2839
|
-
constructor(
|
|
2910
|
+
constructor(initialValue) {
|
|
2840
2911
|
super();
|
|
2841
|
-
this.initialValues = initialValues;
|
|
2842
2912
|
this._optionsService.getCategories().subscribe(res => {
|
|
2843
|
-
|
|
2844
|
-
this.
|
|
2913
|
+
this.setContent(res, initialValue);
|
|
2914
|
+
this._filtersService.refreshFilters();
|
|
2845
2915
|
});
|
|
2846
2916
|
}
|
|
2847
2917
|
type() {
|
|
@@ -2853,44 +2923,122 @@ class CategoryFilter extends Filter {
|
|
|
2853
2923
|
this.data = categories;
|
|
2854
2924
|
}
|
|
2855
2925
|
getContent() {
|
|
2856
|
-
|
|
2926
|
+
return this.data;
|
|
2857
2927
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2928
|
+
/**
|
|
2929
|
+
* toUrlParams
|
|
2930
|
+
* - Recorre la estructura y devuelve la porción de query string para la categoría seleccionada.
|
|
2931
|
+
* - Prioriza el primer selected encontrado (solo una categoría se envía).
|
|
2932
|
+
*/
|
|
2933
|
+
toUrlParams() {
|
|
2934
|
+
const selectedCodes = [];
|
|
2935
|
+
const walk = (nodes) => {
|
|
2936
|
+
nodes.forEach(n => {
|
|
2937
|
+
if (n.selected) {
|
|
2938
|
+
selectedCodes.push(n.code || n.value);
|
|
2939
|
+
}
|
|
2940
|
+
if (Array.isArray(n.children) && n.children.length) {
|
|
2941
|
+
walk(n.children);
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
};
|
|
2945
|
+
walk(this.data);
|
|
2946
|
+
if (selectedCodes.length > 0) {
|
|
2947
|
+
return `&category=${encodeURIComponent(selectedCodes[0])}`;
|
|
2948
|
+
}
|
|
2949
|
+
return '';
|
|
2873
2950
|
}
|
|
2874
2951
|
createElement(filter, original, initialValue) {
|
|
2875
2952
|
if (filter.type == 'sub') {
|
|
2876
2953
|
filter.multi = false;
|
|
2877
2954
|
filter.shape = 'text';
|
|
2878
2955
|
filter.value = original.code;
|
|
2879
|
-
filter.selected = (initialValue &&
|
|
2956
|
+
filter.selected = (initialValue &&
|
|
2957
|
+
this.removeAccents(original.slug?.toLowerCase()) ===
|
|
2958
|
+
this.removeAccents(String(initialValue).toLowerCase()));
|
|
2880
2959
|
filter.children = filter.children.map((child, i) => this.createElement(child, original.children[i], initialValue));
|
|
2881
2960
|
}
|
|
2882
2961
|
else {
|
|
2883
2962
|
filter.value = original.code;
|
|
2884
|
-
filter.selected = (initialValue &&
|
|
2963
|
+
filter.selected = (initialValue &&
|
|
2964
|
+
this.removeAccents(original.slug?.toLowerCase()) ===
|
|
2965
|
+
this.removeAccents(String(initialValue).toLowerCase()));
|
|
2885
2966
|
}
|
|
2886
2967
|
return filter;
|
|
2887
2968
|
}
|
|
2969
|
+
/**
|
|
2970
|
+
* setFromSlug
|
|
2971
|
+
* - Marca como selected los nodos cuyo slug/path/name coincide con el slug proporcionado.
|
|
2972
|
+
* - Utiliza removeAccents para comparar sin tildes y en minúsculas.
|
|
2973
|
+
* - No retorna valor, solo modifica this.data.
|
|
2974
|
+
*/
|
|
2975
|
+
setFromSlug(slug) {
|
|
2976
|
+
const target = this.removeAccents(String(slug).toLowerCase());
|
|
2977
|
+
const walk = (nodes) => {
|
|
2978
|
+
nodes.forEach(n => {
|
|
2979
|
+
// ADDED: iniciar sin selección explícita (asegura estado consistente)
|
|
2980
|
+
n.selected = false;
|
|
2981
|
+
const candidates = [
|
|
2982
|
+
n.slug,
|
|
2983
|
+
n.path && String(n.path).split('/').filter(Boolean).pop(),
|
|
2984
|
+
n.name
|
|
2985
|
+
]
|
|
2986
|
+
.filter(Boolean)
|
|
2987
|
+
.map((s) => this.removeAccents(s.toLowerCase()));
|
|
2988
|
+
if (candidates.includes(target)) {
|
|
2989
|
+
n.selected = true;
|
|
2990
|
+
}
|
|
2991
|
+
if (Array.isArray(n.children) && n.children.length) {
|
|
2992
|
+
walk(n.children);
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
};
|
|
2996
|
+
walk(this.data);
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* hydrateFromQuery
|
|
3000
|
+
* - Marca la categoría según el parámetro de query 'category' si existe.
|
|
3001
|
+
* - Busca el code exacto en la estructura y marca el nodo correspondiente.
|
|
3002
|
+
* - Loggea si se encontró o no la categoría.
|
|
3003
|
+
*/
|
|
3004
|
+
hydrateFromQuery(query) {
|
|
3005
|
+
const raw = query?.['category'];
|
|
3006
|
+
if (!raw)
|
|
3007
|
+
return;
|
|
3008
|
+
const code = String(raw).trim();
|
|
3009
|
+
if (!code)
|
|
3010
|
+
return;
|
|
3011
|
+
const mark = (nodes) => {
|
|
3012
|
+
let found = false;
|
|
3013
|
+
nodes.forEach(n => {
|
|
3014
|
+
if (n.code === code) {
|
|
3015
|
+
n.selected = true;
|
|
3016
|
+
found = true;
|
|
3017
|
+
}
|
|
3018
|
+
else {
|
|
3019
|
+
n.selected = false;
|
|
3020
|
+
}
|
|
3021
|
+
if (Array.isArray(n.children) && n.children.length) {
|
|
3022
|
+
if (mark(n.children))
|
|
3023
|
+
found = true;
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
return found;
|
|
3027
|
+
};
|
|
3028
|
+
const ok = mark(this.data);
|
|
3029
|
+
}
|
|
2888
3030
|
}
|
|
2889
3031
|
|
|
3032
|
+
/**
|
|
3033
|
+
* AttributesFilter
|
|
3034
|
+
* - Gestiona el filtro de atributos (estructura de padres/children).
|
|
3035
|
+
* - Soporta hidratación desde query params y aplicación de códigos seleccionados.
|
|
3036
|
+
*/
|
|
2890
3037
|
class AttributesFilter extends Filter {
|
|
2891
3038
|
_optionsService = inject(OptionsService);
|
|
2892
3039
|
data = [];
|
|
2893
3040
|
multi = false;
|
|
3041
|
+
_codesFromQuery = null;
|
|
2894
3042
|
constructor(initialValue, options) {
|
|
2895
3043
|
super();
|
|
2896
3044
|
if (options) {
|
|
@@ -2898,7 +3046,6 @@ class AttributesFilter extends Filter {
|
|
|
2898
3046
|
}
|
|
2899
3047
|
else {
|
|
2900
3048
|
this._optionsService.getAttributes().subscribe(res => {
|
|
2901
|
-
//console.log(res);
|
|
2902
3049
|
this.setContent(res, initialValue);
|
|
2903
3050
|
});
|
|
2904
3051
|
}
|
|
@@ -2908,10 +3055,18 @@ class AttributesFilter extends Filter {
|
|
|
2908
3055
|
const attributes = this._optionsService.generateMenu(options);
|
|
2909
3056
|
attributes.forEach((filter) => filter = this.createElement(filter, options, initialValue));
|
|
2910
3057
|
this.data = attributes;
|
|
3058
|
+
// Si ya teníamos códigos de la URL guardados, aplicarlos ahora
|
|
3059
|
+
if (this._codesFromQuery && this._codesFromQuery.length) {
|
|
3060
|
+
// ADDED: Aplicar códigos guardados tras cargar la data.
|
|
3061
|
+
this.applyCodes(this._codesFromQuery);
|
|
3062
|
+
}
|
|
2911
3063
|
}
|
|
2912
3064
|
getContent() {
|
|
2913
3065
|
return this.data;
|
|
2914
3066
|
}
|
|
3067
|
+
/**
|
|
3068
|
+
* ADDED: toUrlParams construye la porción de query string que representa las selecciones de atributos.
|
|
3069
|
+
*/
|
|
2915
3070
|
toUrlParams(actual_url = '&attributeCodes=', sublist, already) {
|
|
2916
3071
|
let elements = sublist || this.data;
|
|
2917
3072
|
let aux_url = '';
|
|
@@ -2933,12 +3088,14 @@ class AttributesFilter extends Filter {
|
|
|
2933
3088
|
}
|
|
2934
3089
|
}
|
|
2935
3090
|
});
|
|
2936
|
-
//console.log(actual_url + aux_url);
|
|
2937
3091
|
return actual_url + aux_url;
|
|
2938
3092
|
}
|
|
2939
3093
|
;
|
|
2940
3094
|
cleanResult(text) { return text.replace('&attributeCodes=', ''); }
|
|
2941
3095
|
;
|
|
3096
|
+
/**
|
|
3097
|
+
* ADDED: createElement transforma cada nodo original en FilterElement y marca selección inicial si corresponde.
|
|
3098
|
+
*/
|
|
2942
3099
|
createElement(filter, original, initialValue) {
|
|
2943
3100
|
if (filter.type == 'sub') {
|
|
2944
3101
|
filter.multi = false;
|
|
@@ -2951,10 +3108,65 @@ class AttributesFilter extends Filter {
|
|
|
2951
3108
|
else {
|
|
2952
3109
|
filter.value = original.code;
|
|
2953
3110
|
filter.multi = false;
|
|
2954
|
-
filter.selected = (initialValue &&
|
|
3111
|
+
filter.selected = (initialValue &&
|
|
3112
|
+
this.removeAccents(original.slug?.toLowerCase()) === this.removeAccents(initialValue?.toLowerCase()));
|
|
2955
3113
|
}
|
|
2956
3114
|
return filter;
|
|
2957
3115
|
}
|
|
3116
|
+
/** hidratar desde la URL */
|
|
3117
|
+
hydrateFromQuery(params) {
|
|
3118
|
+
const raw = params['attributeCodes'];
|
|
3119
|
+
if (!raw) {
|
|
3120
|
+
// nada en la URL → limpió filtro
|
|
3121
|
+
this._codesFromQuery = null;
|
|
3122
|
+
this.clearSelection();
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
const codes = String(raw)
|
|
3126
|
+
.split('<and>')
|
|
3127
|
+
.map(c => c.trim())
|
|
3128
|
+
.filter(Boolean);
|
|
3129
|
+
if (!codes.length) {
|
|
3130
|
+
this._codesFromQuery = null;
|
|
3131
|
+
this.clearSelection();
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
this._codesFromQuery = codes;
|
|
3135
|
+
// Si ya tengo data cargada, aplico ahora;
|
|
3136
|
+
// si no, se aplicará en setContent().
|
|
3137
|
+
if (this.data && this.data.length) {
|
|
3138
|
+
// ADDED: Aplicar códigos inmediatamente porque la data ya está disponible.
|
|
3139
|
+
this.applyCodes(codes);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* ADDED: applyCodes recorre la estructura y marca selected=true en los children cuyo código está en 'codes'.
|
|
3144
|
+
* Además marca el parent como seleccionado si alguno de sus hijos lo está.
|
|
3145
|
+
*/
|
|
3146
|
+
applyCodes(codes) {
|
|
3147
|
+
this.data?.forEach((parent) => {
|
|
3148
|
+
parent.selected = false;
|
|
3149
|
+
(parent.children || []).forEach((child) => {
|
|
3150
|
+
const code = child.code || child.value;
|
|
3151
|
+
const isSelected = codes.includes(code);
|
|
3152
|
+
child.selected = isSelected;
|
|
3153
|
+
if (isSelected) {
|
|
3154
|
+
parent.selected = true;
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* ADDED: clearSelection desmarca todos los parents y children (restablece el filtro).
|
|
3161
|
+
*/
|
|
3162
|
+
clearSelection() {
|
|
3163
|
+
this.data?.forEach((parent) => {
|
|
3164
|
+
parent.selected = false;
|
|
3165
|
+
(parent.children || []).forEach((child) => {
|
|
3166
|
+
child.selected = false;
|
|
3167
|
+
});
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
2958
3170
|
}
|
|
2959
3171
|
|
|
2960
3172
|
class DynamicsFilter extends Filter {
|
|
@@ -3115,8 +3327,8 @@ class PriceRangeFilter extends Filter {
|
|
|
3115
3327
|
if (options.minPrice != null && options.maxPrice != null) {
|
|
3116
3328
|
this.minPrice = options.minPrice;
|
|
3117
3329
|
this.maxPrice = options.maxPrice;
|
|
3118
|
-
this.currentMinPrice = options.currentMinPrice ??
|
|
3119
|
-
this.currentMaxPrice = options.currentMaxPrice ??
|
|
3330
|
+
this.currentMinPrice = options.currentMinPrice ?? null;
|
|
3331
|
+
this.currentMaxPrice = options.currentMaxPrice ?? null;
|
|
3120
3332
|
this._loaded = true;
|
|
3121
3333
|
}
|
|
3122
3334
|
};
|
|
@@ -3161,9 +3373,13 @@ class FilterFactory {
|
|
|
3161
3373
|
create(filterType, productsFilter) {
|
|
3162
3374
|
switch (filterType) {
|
|
3163
3375
|
case 'categories':
|
|
3164
|
-
return new CategoryFilter(productsFilter
|
|
3376
|
+
return new CategoryFilter(productsFilter?.type === 'categories'
|
|
3377
|
+
? productsFilter.value
|
|
3378
|
+
: null);
|
|
3165
3379
|
case 'attributes':
|
|
3166
|
-
return new AttributesFilter(productsFilter
|
|
3380
|
+
return new AttributesFilter(productsFilter?.type === 'attributes'
|
|
3381
|
+
? productsFilter.value
|
|
3382
|
+
: null);
|
|
3167
3383
|
case 'dynamics':
|
|
3168
3384
|
return new DynamicsFilter();
|
|
3169
3385
|
case 'sort':
|
|
@@ -3775,6 +3991,7 @@ class FiltersService {
|
|
|
3775
3991
|
limit: 0
|
|
3776
3992
|
};
|
|
3777
3993
|
_filtersInitialized = false;
|
|
3994
|
+
_lastRouteQuery = null;
|
|
3778
3995
|
constructor() {
|
|
3779
3996
|
//this._defaultFilters = this._consts.getDefaultFilters()
|
|
3780
3997
|
}
|
|
@@ -3795,10 +4012,20 @@ class FiltersService {
|
|
|
3795
4012
|
let extra_params = '';
|
|
3796
4013
|
this._filtersSubject.value.forEach(filter => {
|
|
3797
4014
|
const extra = filter.toUrlParams();
|
|
3798
|
-
extra.split('=')[1]
|
|
4015
|
+
if (extra && extra.split('=')[1] !== '') {
|
|
4016
|
+
extra_params += extra;
|
|
4017
|
+
}
|
|
3799
4018
|
});
|
|
3800
|
-
|
|
3801
|
-
|
|
4019
|
+
// Si en la URL ya venía `attributeCodes` pero aún ningún filtro lo agregó,
|
|
4020
|
+
// lo volvemos a sumar a mano. Esto evita que al refrescar (SSR) se pierda el filtro
|
|
4021
|
+
// aunque el AttributesFilter todavía no haya terminado de hidratarse.
|
|
4022
|
+
const attributeCodesFromRoute = this._lastRouteQuery?.['attributeCodes'];
|
|
4023
|
+
if (attributeCodesFromRoute && !extra_params.includes('attributeCodes=')) {
|
|
4024
|
+
extra_params += `&attributeCodes=${attributeCodesFromRoute}`;
|
|
4025
|
+
}
|
|
4026
|
+
if (search_value) {
|
|
4027
|
+
extra_params += '&criteria[search][type]=contains&criteria[search][value]=' + search_value;
|
|
4028
|
+
}
|
|
3802
4029
|
return this.productsFilterApi(extra_params);
|
|
3803
4030
|
}
|
|
3804
4031
|
isUpdated(paginationSettings) {
|
|
@@ -3814,7 +4041,7 @@ class FiltersService {
|
|
|
3814
4041
|
this._paginationSettings = paginationSettings;
|
|
3815
4042
|
return change;
|
|
3816
4043
|
}
|
|
3817
|
-
setFilters(paginationSettings, search_value) {
|
|
4044
|
+
setFilters(paginationSettings, search_value, routeQueryParams) {
|
|
3818
4045
|
this._paginationSettings = paginationSettings;
|
|
3819
4046
|
let final_filters = [];
|
|
3820
4047
|
let filtersToProcess = this._optionsFilters?.includes('all')
|
|
@@ -3827,15 +4054,30 @@ class FiltersService {
|
|
|
3827
4054
|
if (filter) {
|
|
3828
4055
|
final_filters.push(filter);
|
|
3829
4056
|
}
|
|
3830
|
-
else {
|
|
3831
|
-
// console.warn(`❌ Failed to create filter for type: ${filterType}`);
|
|
3832
|
-
}
|
|
3833
4057
|
});
|
|
3834
4058
|
});
|
|
3835
4059
|
this._defaultFilters?.forEach(filterDefault => {
|
|
3836
4060
|
let filter = final_filters.find(filter => filter.type() == filterDefault.filter_type);
|
|
3837
4061
|
filter && filterDefault.codes.forEach(value => filter.setSelected(value));
|
|
3838
4062
|
});
|
|
4063
|
+
// hidratar price_range desde la URL
|
|
4064
|
+
if (routeQueryParams) {
|
|
4065
|
+
const pr = final_filters.find(f => f.type() === 'price_range');
|
|
4066
|
+
if (pr) {
|
|
4067
|
+
const rawMin = routeQueryParams['price_min'];
|
|
4068
|
+
const rawMax = routeQueryParams['price_max'];
|
|
4069
|
+
const min = rawMin != null ? Number(rawMin) : null;
|
|
4070
|
+
const max = rawMax != null ? Number(rawMax) : null;
|
|
4071
|
+
if ((min != null && !Number.isNaN(min)) || (max != null && !Number.isNaN(max))) {
|
|
4072
|
+
if (min != null && !Number.isNaN(min)) {
|
|
4073
|
+
pr.currentMinPrice = min;
|
|
4074
|
+
}
|
|
4075
|
+
if (max != null && !Number.isNaN(max)) {
|
|
4076
|
+
pr.currentMaxPrice = max;
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
3839
4081
|
this._filtersSubject.next(final_filters);
|
|
3840
4082
|
}
|
|
3841
4083
|
getFilters(paginationSettings) {
|
|
@@ -3850,9 +4092,7 @@ class FiltersService {
|
|
|
3850
4092
|
runInInjectionContext(this.environmentInjector, () => {
|
|
3851
4093
|
const filterFactory = new FilterFactory();
|
|
3852
4094
|
filtersToProcess?.forEach(filterType => {
|
|
3853
|
-
// console.log('Creating filter for type:', filterType);
|
|
3854
4095
|
const filter = filterFactory.create(filterType, settings);
|
|
3855
|
-
// console.log('Created filter:', filter);
|
|
3856
4096
|
if (filter) {
|
|
3857
4097
|
final_filters.push(filter);
|
|
3858
4098
|
}
|
|
@@ -3877,7 +4117,6 @@ class FiltersService {
|
|
|
3877
4117
|
if (filterObj.type() !== 'price_range') {
|
|
3878
4118
|
let index = final_filters.findIndex(filter => filter.type() == filterObj.type());
|
|
3879
4119
|
final_filters[index].setSelected(filterElem, filterElem.value || filterElem.code);
|
|
3880
|
-
console.log(index, final_filters);
|
|
3881
4120
|
}
|
|
3882
4121
|
this._filtersSubject.next(final_filters);
|
|
3883
4122
|
// }
|
|
@@ -3887,13 +4126,6 @@ class FiltersService {
|
|
|
3887
4126
|
runInInjectionContext(this.environmentInjector, () => {
|
|
3888
4127
|
const filterFactory = new FilterFactory();
|
|
3889
4128
|
const filter = [];
|
|
3890
|
-
/* claves.forEach((key:any) => {
|
|
3891
|
-
filter.push(filterFactory.create(key))
|
|
3892
|
-
paginationFilters[key].forEach((value:any) => {
|
|
3893
|
-
console.log(filter[0].createElement(value,paginationFilters[key]));
|
|
3894
|
-
})
|
|
3895
|
-
|
|
3896
|
-
}) */
|
|
3897
4129
|
this._filtersSubject.value.forEach((value) => {
|
|
3898
4130
|
if (value.type() == "attributes") {
|
|
3899
4131
|
//value.data = [];
|
|
@@ -3913,6 +4145,95 @@ class FiltersService {
|
|
|
3913
4145
|
pr.setSelected(min, max);
|
|
3914
4146
|
this._filtersSubject.next([...filters]);
|
|
3915
4147
|
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Fuerza a emitir de nuevo los filtros actuales.
|
|
4150
|
+
* Útil cuando un filtro async (categorías, atributos) termina de cargarse
|
|
4151
|
+
* y necesitamos que PaginationService vuelva a armar la URL.
|
|
4152
|
+
*/
|
|
4153
|
+
refreshFilters() {
|
|
4154
|
+
const current = this._filtersSubject.value;
|
|
4155
|
+
// Emitimos una copia para que los subscribers detecten el cambio
|
|
4156
|
+
this._filtersSubject.next([...current]);
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Punto central donde se hidratan los filtros a partir de la URL:
|
|
4160
|
+
* - Lee type/value de la ruta (ej. /collection/categories/:value).
|
|
4161
|
+
* - Lee query params (category, price_min, price_max, attributeCodes, search, etc.).
|
|
4162
|
+
* - Crea instancias de filtros y les delega la hidratación específica.
|
|
4163
|
+
*
|
|
4164
|
+
* Este método se llama tanto en SSR como en navegador.
|
|
4165
|
+
*/
|
|
4166
|
+
hydrateFromRoute(paginationSettings, routeParams, routeQuery) {
|
|
4167
|
+
// hidratar search desde la URL ===
|
|
4168
|
+
const searchFromUrl = routeQuery?.['search'];
|
|
4169
|
+
if (typeof searchFromUrl === 'string' && searchFromUrl.trim() !== '') {
|
|
4170
|
+
// Guardamos el valor para que otros componentes (Header, Collection) lo reutilicen
|
|
4171
|
+
this._consts.searchValue = searchFromUrl.trim();
|
|
4172
|
+
}
|
|
4173
|
+
else {
|
|
4174
|
+
// Si no viene `search` en la URL, limpiamos el estado global para no arrastrar búsquedas viejas
|
|
4175
|
+
this._consts.searchValue = '';
|
|
4176
|
+
}
|
|
4177
|
+
// Guardamos la última query completa para usarla en generateFinalApi (parche attributeCodes)
|
|
4178
|
+
this._lastRouteQuery = routeQuery;
|
|
4179
|
+
// Guardamos las settings actuales (type, value, limit, etc.)
|
|
4180
|
+
this._paginationSettings = paginationSettings;
|
|
4181
|
+
// Creamos los filtros base (attributes, categories, dynamics, sort, price_range, etc.)
|
|
4182
|
+
let final_filters = [];
|
|
4183
|
+
let filtersToProcess = this._optionsFilters?.includes('all')
|
|
4184
|
+
? ['attributes', 'categories', 'dynamics', 'sort', 'price_range']
|
|
4185
|
+
: this._optionsFilters;
|
|
4186
|
+
runInInjectionContext(this.environmentInjector, () => {
|
|
4187
|
+
const filterFactory = new FilterFactory();
|
|
4188
|
+
filtersToProcess?.forEach(filterType => {
|
|
4189
|
+
const filter = filterFactory.create(filterType, paginationSettings);
|
|
4190
|
+
if (filter) {
|
|
4191
|
+
final_filters.push(filter);
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
});
|
|
4195
|
+
// Aplicamos defaultFilters si corresponde
|
|
4196
|
+
this._defaultFilters?.forEach(filterDefault => {
|
|
4197
|
+
const filter = final_filters.find(f => f.type() === filterDefault.filter_type);
|
|
4198
|
+
filter && filterDefault.codes.forEach(value => filter.setSelected(value));
|
|
4199
|
+
});
|
|
4200
|
+
// Hidratar filtros con slug y query params genéricos
|
|
4201
|
+
final_filters.forEach(f => {
|
|
4202
|
+
const anyFilter = f;
|
|
4203
|
+
// type/value en la URL: /collection/categories/:value
|
|
4204
|
+
const urlType = routeParams['type'];
|
|
4205
|
+
const urlValue = routeParams['value'];
|
|
4206
|
+
// Por ejemplo, el CategoryFilter implementa setFromSlug para marcar la categoría correcta
|
|
4207
|
+
if (urlType === 'categories' && urlValue && typeof anyFilter.setFromSlug === 'function') {
|
|
4208
|
+
anyFilter.setFromSlug(urlValue);
|
|
4209
|
+
}
|
|
4210
|
+
// query params genéricos: ?category=0101&price_min=...&price_max=...&attributeCodes=...
|
|
4211
|
+
// Cada filtro que lo soporte implementa hydrateFromQuery y decide qué leer.
|
|
4212
|
+
if (typeof anyFilter.hydrateFromQuery === 'function') {
|
|
4213
|
+
anyFilter.hydrateFromQuery(routeQuery);
|
|
4214
|
+
}
|
|
4215
|
+
});
|
|
4216
|
+
// Hidratar específicamente el PriceRangeFilter desde la URL
|
|
4217
|
+
const priceFilter = final_filters.find((f) => f instanceof PriceRangeFilter);
|
|
4218
|
+
if (priceFilter && routeQuery) {
|
|
4219
|
+
const minFromUrl = routeQuery['price_min'];
|
|
4220
|
+
const maxFromUrl = routeQuery['price_max'];
|
|
4221
|
+
if (minFromUrl != null || maxFromUrl != null) {
|
|
4222
|
+
const min = minFromUrl != null ? Number(minFromUrl) : priceFilter.minPrice;
|
|
4223
|
+
const max = maxFromUrl != null ? Number(maxFromUrl) : priceFilter.maxPrice;
|
|
4224
|
+
// usamos la API del filtro para setear el rango
|
|
4225
|
+
priceFilter.setSelected(min, max);
|
|
4226
|
+
}
|
|
4227
|
+
else {
|
|
4228
|
+
// si no hay nada en la URL, lo reseteamos
|
|
4229
|
+
priceFilter.reset();
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
// Emitimos filtros ya hidratados para que PaginationService y Collection reaccionen
|
|
4233
|
+
this._filtersSubject.next(final_filters);
|
|
4234
|
+
// Marcamos que el sistema de filtros está listo (útil para mostrar spinners, etc.)
|
|
4235
|
+
this._readySubject.next(true);
|
|
4236
|
+
}
|
|
3916
4237
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FiltersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3917
4238
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FiltersService, providedIn: 'root' });
|
|
3918
4239
|
}
|
|
@@ -3924,17 +4245,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
3924
4245
|
}], ctorParameters: () => [] });
|
|
3925
4246
|
|
|
3926
4247
|
/**
|
|
3927
|
-
* Servicio para manejar la paginación de
|
|
3928
|
-
*
|
|
4248
|
+
* Servicio para manejar la paginación y la carga de productos.
|
|
4249
|
+
*
|
|
4250
|
+
* Se encarga de:
|
|
4251
|
+
* - Escuchar los cambios en los filtros (categorías, atributos, precio, búsqueda, etc.).
|
|
4252
|
+
* - Construir la URL al backend con esos filtros.
|
|
4253
|
+
* - Pedir los productos al endpoint de product-search.
|
|
4254
|
+
* - Exponer un observable con la última página de productos (`paginationData$`)
|
|
4255
|
+
* y otros helpers (precio mínimo/máximo, siguiente página, etc.).
|
|
3929
4256
|
*/
|
|
3930
4257
|
class PaginationService {
|
|
3931
4258
|
_connectionService = inject(ConnectionService);
|
|
3932
4259
|
_filtersService = inject(FiltersService);
|
|
3933
4260
|
_constants = inject(CoreConstantsService);
|
|
3934
|
-
|
|
4261
|
+
/**
|
|
4262
|
+
* Flujo principal: a partir de los filtros → arma URL → consulta backend → devuelve productos.
|
|
4263
|
+
*
|
|
4264
|
+
* Pasos:
|
|
4265
|
+
* 1) Espera a que `FiltersService` emita filtros válidos.
|
|
4266
|
+
* 2) Construye la URL final con `buildUrl()`.
|
|
4267
|
+
* 3) Evita repetir llamadas si la URL no cambió (`distinctUntilChanged`).
|
|
4268
|
+
* 4) Llama al backend con `getData(url)`.
|
|
4269
|
+
* 5) Comparte el último resultado con todos los suscriptores (`shareReplay(1)`).
|
|
4270
|
+
*/
|
|
4271
|
+
paginationData$ = this._filtersService.filters$.pipe(tap(filters => {
|
|
4272
|
+
}), filter(filters => {
|
|
4273
|
+
if (!filters || !filters.length)
|
|
4274
|
+
return false;
|
|
4275
|
+
const categoryFilter = filters.find(f => f.type() === 'categories');
|
|
4276
|
+
const hasCategorySelected = !!categoryFilter?.getSelectedList()?.length;
|
|
4277
|
+
const isCategoryRoute = this._constants.currentRouteType === 'categories';
|
|
4278
|
+
// Si estamos en ruta de categorías y todavía no se marcó ninguna,
|
|
4279
|
+
// esperamos a que el CategoryFilter se hidrate antes de disparar la llamada.
|
|
4280
|
+
if (isCategoryRoute && !hasCategorySelected) {
|
|
4281
|
+
return false;
|
|
4282
|
+
}
|
|
4283
|
+
return true;
|
|
4284
|
+
}),
|
|
4285
|
+
// 2) Convertimos filtros -> URL
|
|
4286
|
+
map(filters => {
|
|
3935
4287
|
const url = this.buildUrl(filters);
|
|
3936
|
-
return
|
|
3937
|
-
})
|
|
4288
|
+
return url;
|
|
4289
|
+
}),
|
|
4290
|
+
// 3) Solo seguimos si la URL cambió respecto de la emisión anterior
|
|
4291
|
+
distinctUntilChanged(),
|
|
4292
|
+
// 4) Llamamos al backend con la URL construida
|
|
4293
|
+
tap(url => {
|
|
4294
|
+
}), switchMap(url => this.getData(url)),
|
|
4295
|
+
// 5) Reutilizar resultado si alguien más se suscribe (evita repetir la petición)
|
|
4296
|
+
shareReplay(1));
|
|
3938
4297
|
_dataPagination = signal({
|
|
3939
4298
|
attributes: [],
|
|
3940
4299
|
category: [],
|
|
@@ -4003,7 +4362,8 @@ class PaginationService {
|
|
|
4003
4362
|
this._dataPagination.set({ ...response, called: true });
|
|
4004
4363
|
this._finished = (response.page == response.pages);
|
|
4005
4364
|
this._waiting = false;
|
|
4006
|
-
// Emitir los valores de price_min y price_max a través de priceRangeSubject
|
|
4365
|
+
// Emitir los valores de price_min y price_max a través de priceRangeSubject,
|
|
4366
|
+
// para que otros componentes (ej. filtro de precio) puedan mostrarlos.
|
|
4007
4367
|
this.priceRangeSubject.next({
|
|
4008
4368
|
price_min: response.price_min,
|
|
4009
4369
|
price_max: response.price_max
|
|
@@ -4063,27 +4423,36 @@ class PaginationService {
|
|
|
4063
4423
|
* @returns
|
|
4064
4424
|
*/
|
|
4065
4425
|
buildUrl(filters) {
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
return this._filtersService.generateFinalApi();
|
|
4071
|
-
}
|
|
4426
|
+
const url = this._constants.searchValue
|
|
4427
|
+
? this._filtersService.generateFinalApi(this._constants.searchValue)
|
|
4428
|
+
: this._filtersService.generateFinalApi();
|
|
4429
|
+
return url;
|
|
4072
4430
|
}
|
|
4073
4431
|
/**
|
|
4074
|
-
* Devuelve un observable de los productos que
|
|
4432
|
+
* Devuelve un observable de los productos que devolvió la última página obtenida.
|
|
4075
4433
|
* @param url
|
|
4076
4434
|
* @returns
|
|
4077
4435
|
*/
|
|
4078
4436
|
getData(url) {
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4437
|
+
// Detectar la página desde la URL (?page=n). Si no viene, asumimos page=1.
|
|
4438
|
+
const pageMatch = url.match(/[?&]page=(\d+)/);
|
|
4439
|
+
const page = pageMatch ? Number(pageMatch[1]) : 1;
|
|
4440
|
+
// Si es la primera página, reseteamos estado interno
|
|
4441
|
+
if (page === 1) {
|
|
4442
|
+
this._resetSubject.next(true);
|
|
4443
|
+
this._nextProductsSubject.next([]);
|
|
4444
|
+
}
|
|
4445
|
+
// Llamamos al backend con el page correcto
|
|
4446
|
+
return this._connectionService.get(url, { limit: 10, page }).pipe(tap((res) => {
|
|
4447
|
+
// Actualizar datos de paginación + price_min/price_max
|
|
4084
4448
|
res.links ? this.updatePageData(res) : this.finish(res);
|
|
4085
|
-
})
|
|
4086
|
-
|
|
4449
|
+
}), map((res) => {
|
|
4450
|
+
const items = res.items ?? [];
|
|
4451
|
+
// Mantener nextProducts$ sincronizado (por compatibilidad)
|
|
4452
|
+
this._nextProductsSubject.next(items);
|
|
4453
|
+
// Lo que ve paginationData$ (switchMap) son estos items
|
|
4454
|
+
return items;
|
|
4455
|
+
}));
|
|
4087
4456
|
}
|
|
4088
4457
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PaginationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4089
4458
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PaginationService, providedIn: 'root' });
|
|
@@ -4148,9 +4517,11 @@ class ProductsService {
|
|
|
4148
4517
|
* @param paginationSettings
|
|
4149
4518
|
* @param searchValue
|
|
4150
4519
|
*/
|
|
4151
|
-
getProductsForFilter(paginationSettings, searchValue) {
|
|
4520
|
+
getProductsForFilter(paginationSettings, searchValue, routeQueryParams) {
|
|
4152
4521
|
searchValue ? this.searchValue.set(searchValue) : this.searchValue.set('');
|
|
4153
|
-
|
|
4522
|
+
if (paginationSettings) {
|
|
4523
|
+
this._filtersService.setFilters(paginationSettings, searchValue, routeQueryParams);
|
|
4524
|
+
}
|
|
4154
4525
|
}
|
|
4155
4526
|
/**
|
|
4156
4527
|
* Actualiza los productos con los de la siguiente pagina.
|
|
@@ -6275,10 +6646,8 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6275
6646
|
constructor() {
|
|
6276
6647
|
super();
|
|
6277
6648
|
this._channelService.channel$.subscribe(cfg => {
|
|
6278
|
-
// console.log('Channel configuration:', cfg);
|
|
6279
6649
|
this.showPricesOnlyToLoggedUsers = !!cfg.showPricesOnlyToLoggedUsers;
|
|
6280
6650
|
this.hidePrices = !!cfg.hidePrices;
|
|
6281
|
-
// console.log('hidePrices:', this.hidePrices);
|
|
6282
6651
|
});
|
|
6283
6652
|
}
|
|
6284
6653
|
coreConstantsService = inject(CoreConstantsService);
|
|
@@ -6286,6 +6655,7 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6286
6655
|
cdr = inject(ChangeDetectorRef); // Inyectamos ChangeDetectorRef para forzar la actualización
|
|
6287
6656
|
ngOnInit() {
|
|
6288
6657
|
this.channel = this.coreConstantsService.getChannel();
|
|
6658
|
+
this.searchValue = this.coreConstantsService.searchValue || '';
|
|
6289
6659
|
this.onWindowScroll();
|
|
6290
6660
|
this.detectRouteChange(); // Llamamos a la función que detecta el cambio de ruta
|
|
6291
6661
|
// Usar el Observable del AuthService
|
|
@@ -6322,17 +6692,21 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6322
6692
|
});
|
|
6323
6693
|
}
|
|
6324
6694
|
onWindowScroll() {
|
|
6325
|
-
|
|
6326
|
-
|
|
6695
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
6696
|
+
const scrollTop = window.scrollY;
|
|
6697
|
+
this.isScrolled = scrollTop > 80;
|
|
6698
|
+
}
|
|
6327
6699
|
}
|
|
6328
6700
|
isHomeFunction() {
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
if (
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6701
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
6702
|
+
const headerElement = document.querySelector('header');
|
|
6703
|
+
if (headerElement) {
|
|
6704
|
+
if (this.router.url !== '/home') {
|
|
6705
|
+
headerElement.classList.add('show-menu');
|
|
6706
|
+
}
|
|
6707
|
+
else {
|
|
6708
|
+
headerElement.classList.remove('show-menu');
|
|
6709
|
+
}
|
|
6336
6710
|
}
|
|
6337
6711
|
}
|
|
6338
6712
|
}
|
|
@@ -6356,27 +6730,38 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6356
6730
|
}
|
|
6357
6731
|
};
|
|
6358
6732
|
borrarInput(inputId) {
|
|
6359
|
-
if (
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
input.value = '';
|
|
6363
|
-
}
|
|
6364
|
-
}
|
|
6365
|
-
else {
|
|
6366
|
-
const inputs = ['searchInput1'];
|
|
6367
|
-
inputs.forEach((id) => {
|
|
6368
|
-
const input = document.getElementById(id);
|
|
6733
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
6734
|
+
if (inputId) {
|
|
6735
|
+
const input = document.getElementById(inputId);
|
|
6369
6736
|
if (input) {
|
|
6370
6737
|
input.value = '';
|
|
6371
6738
|
}
|
|
6372
|
-
}
|
|
6739
|
+
}
|
|
6740
|
+
else {
|
|
6741
|
+
const inputs = ['searchInput1'];
|
|
6742
|
+
inputs.forEach((id) => {
|
|
6743
|
+
const input = document.getElementById(id);
|
|
6744
|
+
if (input) {
|
|
6745
|
+
input.value = '';
|
|
6746
|
+
}
|
|
6747
|
+
});
|
|
6748
|
+
}
|
|
6373
6749
|
}
|
|
6374
6750
|
this.searchValue = '';
|
|
6375
6751
|
this.coreConstantsService.searchValue = '';
|
|
6376
|
-
|
|
6752
|
+
// En lugar de relanzar la búsqueda con string vacío,
|
|
6753
|
+
// actualizamos la URL para quitar el query param `search` y resetear la paginación.
|
|
6754
|
+
this.router.navigate(['/collection'], {
|
|
6755
|
+
queryParams: {
|
|
6756
|
+
search: null,
|
|
6757
|
+
page: null
|
|
6758
|
+
},
|
|
6759
|
+
queryParamsHandling: 'merge'
|
|
6760
|
+
});
|
|
6377
6761
|
}
|
|
6378
6762
|
setupMobileMenu() {
|
|
6379
|
-
|
|
6763
|
+
if (!isPlatformBrowser(this.platformId) || typeof document === 'undefined')
|
|
6764
|
+
return;
|
|
6380
6765
|
const menuMobile = document.querySelector('.menuMobile');
|
|
6381
6766
|
if (!(menuMobile instanceof HTMLElement))
|
|
6382
6767
|
return;
|
|
@@ -6411,6 +6796,8 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6411
6796
|
});
|
|
6412
6797
|
}
|
|
6413
6798
|
setupSearchInputs() {
|
|
6799
|
+
if (!isPlatformBrowser(this.platformId))
|
|
6800
|
+
return;
|
|
6414
6801
|
const inputs = ['searchInput1', 'searchInput2'];
|
|
6415
6802
|
inputs.forEach(id => {
|
|
6416
6803
|
const input = document.getElementById(id);
|
|
@@ -7273,7 +7660,7 @@ class BlockProductsEcComponent extends BlockEcComponent {
|
|
|
7273
7660
|
* Permite personalización de las imágenes de las flechas mediante @Input.
|
|
7274
7661
|
*/
|
|
7275
7662
|
setupSwiperNavigation() {
|
|
7276
|
-
if (this.meta?.styles?.carrousel !== false) {
|
|
7663
|
+
if (this.meta?.styles?.carrousel !== false && isPlatformBrowser(this.platformId)) {
|
|
7277
7664
|
// Usar setTimeout para asegurar que el swiper esté inicializado
|
|
7278
7665
|
setTimeout(() => {
|
|
7279
7666
|
this.initializeSwiperWithCustomNavigation();
|
|
@@ -7285,6 +7672,8 @@ class BlockProductsEcComponent extends BlockEcComponent {
|
|
|
7285
7672
|
* Esta función puede ser movida al componente base para reutilización.
|
|
7286
7673
|
*/
|
|
7287
7674
|
initializeSwiperWithCustomNavigation() {
|
|
7675
|
+
if (!isPlatformBrowser(this.platformId))
|
|
7676
|
+
return;
|
|
7288
7677
|
const prevButton = document.getElementById(`${this.meta?.code}-prev`);
|
|
7289
7678
|
const nextButton = document.getElementById(`${this.meta?.code}-next`);
|
|
7290
7679
|
const swiperElement = document.getElementById(this.meta?.code);
|
|
@@ -7650,7 +8039,9 @@ class MagnizoomEcComponent {
|
|
|
7650
8039
|
this.image = this.document.createElement('img');
|
|
7651
8040
|
this.image.onload = () => {
|
|
7652
8041
|
this.lensSize = { width: this.image.width / 2, height: this.image.height / 2 };
|
|
7653
|
-
|
|
8042
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
8043
|
+
setTimeout(() => this.render());
|
|
8044
|
+
}
|
|
7654
8045
|
};
|
|
7655
8046
|
this.image.src = src;
|
|
7656
8047
|
}
|
|
@@ -8160,6 +8551,7 @@ class CollectionEcComponent {
|
|
|
8160
8551
|
_productsService = inject(ProductsService);
|
|
8161
8552
|
_activeRoute = inject(ActivatedRoute);
|
|
8162
8553
|
_optionsService = inject(OptionsService);
|
|
8554
|
+
_filtersService = inject(FiltersService);
|
|
8163
8555
|
params$ = this._activeRoute.params;
|
|
8164
8556
|
//public ready = this._optionsService.ready
|
|
8165
8557
|
queryParams$ = this._activeRoute.queryParams;
|
|
@@ -8171,40 +8563,63 @@ class CollectionEcComponent {
|
|
|
8171
8563
|
defaultFilters = [];
|
|
8172
8564
|
loading = false;
|
|
8173
8565
|
countProducts = signal(0);
|
|
8174
|
-
loaded = false;
|
|
8566
|
+
loaded = signal(false);
|
|
8175
8567
|
optionsFilters = ['all'];
|
|
8176
8568
|
filters_sort = [];
|
|
8177
|
-
_filtersService = inject(FiltersService);
|
|
8178
8569
|
filters$ = this._filtersService.filters$;
|
|
8179
8570
|
ready$ = this._filtersService.ready$;
|
|
8180
8571
|
window;
|
|
8181
8572
|
isList = false;
|
|
8573
|
+
destroy$ = new Subject();
|
|
8182
8574
|
setAsList = (value) => this.isList = value;
|
|
8183
8575
|
ngOnInit() {
|
|
8184
|
-
this.
|
|
8185
|
-
|
|
8576
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
8577
|
+
this.window?.scroll(0, 0);
|
|
8578
|
+
}
|
|
8579
|
+
combineLatest([this.params$, this.queryParams$])
|
|
8580
|
+
.pipe(map(([params, query]) => ({ params, query })), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), takeUntil(this.destroy$))
|
|
8581
|
+
.subscribe(({ params, query }) => {
|
|
8582
|
+
// Guardamos el tipo de ruta actual en las constantes (ej: 'categories', 'sections', etc.)
|
|
8583
|
+
const routeType = params['type'] || null;
|
|
8584
|
+
this.constanst.setCurrentRouteType(routeType);
|
|
8585
|
+
const paginationSettings = {
|
|
8586
|
+
latest: true,
|
|
8587
|
+
limit: 10,
|
|
8588
|
+
type: params['type'] || null,
|
|
8589
|
+
value: params['value'] || null
|
|
8590
|
+
};
|
|
8591
|
+
// Punto clave:
|
|
8592
|
+
// A partir de la URL (params + query) reconstruimos los filtros
|
|
8593
|
+
// (categorías, atributos, rango de precios, etc.)
|
|
8594
|
+
// Esto permite:
|
|
8595
|
+
// - Soportar F5 / recarga sin perder filtros
|
|
8596
|
+
// - Navegar con URL compartibles (deep linking)
|
|
8597
|
+
this._filtersService.hydrateFromRoute(paginationSettings, params, query);
|
|
8598
|
+
});
|
|
8186
8599
|
}
|
|
8187
8600
|
//protected readonly questions = signal<Question[]>([]);
|
|
8188
8601
|
total = 0;
|
|
8189
8602
|
platformId = inject(PLATFORM_ID);
|
|
8190
8603
|
constructor() {
|
|
8604
|
+
// Guardamos window sólo en Browser para evitar errores en SSR
|
|
8191
8605
|
if (isPlatformBrowser(this.platformId)) {
|
|
8192
8606
|
this.window = window;
|
|
8193
8607
|
}
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
value: params['value'] || null
|
|
8203
|
-
};
|
|
8204
|
-
this._productsService.getProductsForFilter(paginationSettings, queryParams["search"]);
|
|
8608
|
+
// Nos suscribimos al stream de productos.
|
|
8609
|
+
// Cuando ProductsService emite, marcamos `loaded` en true, pero solo en Browser.
|
|
8610
|
+
// En SSR no lo marcamos para evitar cambios de estado innecesarios.
|
|
8611
|
+
this.products$
|
|
8612
|
+
.pipe(takeUntil(this.destroy$))
|
|
8613
|
+
.subscribe(products => {
|
|
8614
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
8615
|
+
this.loaded.set(true);
|
|
8205
8616
|
}
|
|
8206
8617
|
});
|
|
8207
8618
|
}
|
|
8619
|
+
ngOnDestroy() {
|
|
8620
|
+
this.destroy$.next();
|
|
8621
|
+
this.destroy$.complete();
|
|
8622
|
+
}
|
|
8208
8623
|
onScroll() {
|
|
8209
8624
|
this.loading = true;
|
|
8210
8625
|
this._productsService.updateProducts();
|
|
@@ -8832,6 +9247,7 @@ class FiltersEcComponent {
|
|
|
8832
9247
|
injector = inject(Injector);
|
|
8833
9248
|
isAuthenticated$ = this._authService.isAuthenticated();
|
|
8834
9249
|
hidePrices = false;
|
|
9250
|
+
route = inject(ActivatedRoute);
|
|
8835
9251
|
setSelect;
|
|
8836
9252
|
ngOnInit() {
|
|
8837
9253
|
}
|
|
@@ -8861,26 +9277,56 @@ class FiltersEcComponent {
|
|
|
8861
9277
|
.pop()
|
|
8862
9278
|
selectedOption && this._filtersService.setFilterSelected(this.filters[0], selectedOption) */
|
|
8863
9279
|
}
|
|
9280
|
+
/**
|
|
9281
|
+
* Maneja el click sobre un elemento de filtro (categoría, atributo, etc.).
|
|
9282
|
+
*
|
|
9283
|
+
* - Para categorías: navega a la URL de la categoría.
|
|
9284
|
+
* - Para atributos: actualiza la query string de la URL con `attributeCodes`.
|
|
9285
|
+
* - Para otros filtros: delega en FiltersService para marcar seleccionado.
|
|
9286
|
+
*
|
|
9287
|
+
* La idea es que **la URL siempre represente los filtros aplicados**,
|
|
9288
|
+
* de modo que:
|
|
9289
|
+
* - al hacer F5 no se pierdan los filtros,
|
|
9290
|
+
* - los links sean compartibles (deep linking).
|
|
9291
|
+
*/
|
|
8864
9292
|
setSelected(filter, selected) {
|
|
8865
9293
|
if (!filter || !selected) {
|
|
8866
|
-
console.error('Filter or selected element is undefined:', { filter, selected });
|
|
8867
9294
|
return;
|
|
8868
9295
|
}
|
|
9296
|
+
// Si el elemento está marcado como no visible, no hacemos nada
|
|
9297
|
+
if (selected.isVisible === false)
|
|
9298
|
+
return;
|
|
8869
9299
|
if (typeof filter.setSelected !== 'function') {
|
|
8870
|
-
console.error('filter.setSelected is not a function. Filter might not be an instance of the expected class:', filter);
|
|
8871
9300
|
return;
|
|
8872
9301
|
}
|
|
8873
9302
|
try {
|
|
8874
|
-
// this._filtersService.setFilterSelected(filter, selected);
|
|
8875
9303
|
if (filter.type() === 'categories') {
|
|
9304
|
+
// Para categorías usamos navegación por path (ej: /collection/categories/camas-elasticas)
|
|
8876
9305
|
if (selected.path) {
|
|
8877
9306
|
this.router.navigate([selected.path]);
|
|
8878
9307
|
}
|
|
9308
|
+
return;
|
|
8879
9309
|
}
|
|
8880
|
-
|
|
8881
|
-
//
|
|
8882
|
-
|
|
9310
|
+
if (filter.type() === 'attributes') {
|
|
9311
|
+
// El backend espera que le mandemos el "code" del atributo en el query param
|
|
9312
|
+
const code = selected.code || selected.value || null;
|
|
9313
|
+
// Actualizamos la URL manteniendo la ruta, pero agregando `attributeCodes`
|
|
9314
|
+
// Ejemplo de resultado:
|
|
9315
|
+
// /collection/categories/camas-elasticas?attributeCodes=ABC123&...
|
|
9316
|
+
//
|
|
9317
|
+
// También reseteamos la página a null para que vuelva a la primera página
|
|
9318
|
+
// cuando se aplica un nuevo filtro.
|
|
9319
|
+
this.router.navigate([], {
|
|
9320
|
+
relativeTo: this.route,
|
|
9321
|
+
queryParams: {
|
|
9322
|
+
attributeCodes: code,
|
|
9323
|
+
page: null
|
|
9324
|
+
},
|
|
9325
|
+
queryParamsHandling: 'merge'
|
|
9326
|
+
});
|
|
9327
|
+
return;
|
|
8883
9328
|
}
|
|
9329
|
+
this._filtersService.setFilterSelected(filter, selected);
|
|
8884
9330
|
}
|
|
8885
9331
|
catch (error) {
|
|
8886
9332
|
console.error("Error while setting selected filter:", error);
|
|
@@ -8914,7 +9360,9 @@ class FiltersEcComponent {
|
|
|
8914
9360
|
return true;
|
|
8915
9361
|
};
|
|
8916
9362
|
scrollUp = () => {
|
|
8917
|
-
|
|
9363
|
+
if (typeof window !== 'undefined') {
|
|
9364
|
+
window.scroll(0, 0);
|
|
9365
|
+
}
|
|
8918
9366
|
return true;
|
|
8919
9367
|
};
|
|
8920
9368
|
hasAppliedFilters() {
|
|
@@ -10957,9 +11405,11 @@ class RelatedProductsEcComponent extends BlockEcComponent {
|
|
|
10957
11405
|
this._relatedProductsSubject.next(relatedProducts);
|
|
10958
11406
|
res.map((products) => this._analyticsService.callEvent('view_item_list', { products: products.items, item_list_name: products.title || 'Related Products', item_list_id: products.id || 'related-products' }));
|
|
10959
11407
|
// Inicializar swiper después de que los datos estén disponibles
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
11408
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
11409
|
+
setTimeout(() => {
|
|
11410
|
+
this.initSwiper();
|
|
11411
|
+
}, 100);
|
|
11412
|
+
}
|
|
10963
11413
|
});
|
|
10964
11414
|
}
|
|
10965
11415
|
initSwiper() {
|
|
@@ -11828,6 +12278,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
11828
12278
|
|
|
11829
12279
|
class PriceRangeFilterComponent {
|
|
11830
12280
|
_filtersService = inject(FiltersService);
|
|
12281
|
+
router = inject(Router);
|
|
12282
|
+
route = inject(ActivatedRoute);
|
|
11831
12283
|
priceGap = 1;
|
|
11832
12284
|
roundStep = 5;
|
|
11833
12285
|
filter;
|
|
@@ -11864,7 +12316,15 @@ class PriceRangeFilterComponent {
|
|
|
11864
12316
|
return;
|
|
11865
12317
|
const min = filter.currentMinPrice ?? filter.minPrice;
|
|
11866
12318
|
const max = filter.currentMaxPrice ?? filter.maxPrice;
|
|
11867
|
-
this.
|
|
12319
|
+
this.router.navigate([], {
|
|
12320
|
+
relativeTo: this.route,
|
|
12321
|
+
queryParams: {
|
|
12322
|
+
price_min: min !== filter.minPrice ? min : null,
|
|
12323
|
+
price_max: max !== filter.maxPrice ? max : null,
|
|
12324
|
+
page: null,
|
|
12325
|
+
},
|
|
12326
|
+
queryParamsHandling: 'merge'
|
|
12327
|
+
});
|
|
11868
12328
|
}
|
|
11869
12329
|
getMinValue(filter) {
|
|
11870
12330
|
return (filter instanceof PriceRangeFilter && filter.currentMinPrice != null)
|