ng-easycommerce-v18 0.3.22-beta.2 → 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 +6 -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/price_range-filter.mjs +3 -3
- package/esm2022/lib/constants/core.constants.service.mjs +12 -1
- 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 +12 -6
- package/esm2022/lib/ec-components/price-range-filter/price-range-filter.component.mjs +13 -2
- package/esm2022/lib/ec-services/filters.service.mjs +124 -18
- 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 +490 -98
- 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/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
|
}
|
|
@@ -2883,17 +2894,24 @@ class Filter {
|
|
|
2883
2894
|
};
|
|
2884
2895
|
}
|
|
2885
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
|
+
*/
|
|
2886
2905
|
class CategoryFilter extends Filter {
|
|
2887
|
-
initialValues;
|
|
2888
2906
|
_optionsService = inject(OptionsService);
|
|
2907
|
+
_filtersService = inject(FiltersService);
|
|
2889
2908
|
data = [];
|
|
2890
2909
|
multi = false;
|
|
2891
|
-
constructor(
|
|
2910
|
+
constructor(initialValue) {
|
|
2892
2911
|
super();
|
|
2893
|
-
this.initialValues = initialValues;
|
|
2894
2912
|
this._optionsService.getCategories().subscribe(res => {
|
|
2895
|
-
|
|
2896
|
-
this.
|
|
2913
|
+
this.setContent(res, initialValue);
|
|
2914
|
+
this._filtersService.refreshFilters();
|
|
2897
2915
|
});
|
|
2898
2916
|
}
|
|
2899
2917
|
type() {
|
|
@@ -2905,44 +2923,122 @@ class CategoryFilter extends Filter {
|
|
|
2905
2923
|
this.data = categories;
|
|
2906
2924
|
}
|
|
2907
2925
|
getContent() {
|
|
2908
|
-
|
|
2926
|
+
return this.data;
|
|
2909
2927
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
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 '';
|
|
2925
2950
|
}
|
|
2926
2951
|
createElement(filter, original, initialValue) {
|
|
2927
2952
|
if (filter.type == 'sub') {
|
|
2928
2953
|
filter.multi = false;
|
|
2929
2954
|
filter.shape = 'text';
|
|
2930
2955
|
filter.value = original.code;
|
|
2931
|
-
filter.selected = (initialValue &&
|
|
2956
|
+
filter.selected = (initialValue &&
|
|
2957
|
+
this.removeAccents(original.slug?.toLowerCase()) ===
|
|
2958
|
+
this.removeAccents(String(initialValue).toLowerCase()));
|
|
2932
2959
|
filter.children = filter.children.map((child, i) => this.createElement(child, original.children[i], initialValue));
|
|
2933
2960
|
}
|
|
2934
2961
|
else {
|
|
2935
2962
|
filter.value = original.code;
|
|
2936
|
-
filter.selected = (initialValue &&
|
|
2963
|
+
filter.selected = (initialValue &&
|
|
2964
|
+
this.removeAccents(original.slug?.toLowerCase()) ===
|
|
2965
|
+
this.removeAccents(String(initialValue).toLowerCase()));
|
|
2937
2966
|
}
|
|
2938
2967
|
return filter;
|
|
2939
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
|
+
}
|
|
2940
3030
|
}
|
|
2941
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
|
+
*/
|
|
2942
3037
|
class AttributesFilter extends Filter {
|
|
2943
3038
|
_optionsService = inject(OptionsService);
|
|
2944
3039
|
data = [];
|
|
2945
3040
|
multi = false;
|
|
3041
|
+
_codesFromQuery = null;
|
|
2946
3042
|
constructor(initialValue, options) {
|
|
2947
3043
|
super();
|
|
2948
3044
|
if (options) {
|
|
@@ -2950,7 +3046,6 @@ class AttributesFilter extends Filter {
|
|
|
2950
3046
|
}
|
|
2951
3047
|
else {
|
|
2952
3048
|
this._optionsService.getAttributes().subscribe(res => {
|
|
2953
|
-
//console.log(res);
|
|
2954
3049
|
this.setContent(res, initialValue);
|
|
2955
3050
|
});
|
|
2956
3051
|
}
|
|
@@ -2960,10 +3055,18 @@ class AttributesFilter extends Filter {
|
|
|
2960
3055
|
const attributes = this._optionsService.generateMenu(options);
|
|
2961
3056
|
attributes.forEach((filter) => filter = this.createElement(filter, options, initialValue));
|
|
2962
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
|
+
}
|
|
2963
3063
|
}
|
|
2964
3064
|
getContent() {
|
|
2965
3065
|
return this.data;
|
|
2966
3066
|
}
|
|
3067
|
+
/**
|
|
3068
|
+
* ADDED: toUrlParams construye la porción de query string que representa las selecciones de atributos.
|
|
3069
|
+
*/
|
|
2967
3070
|
toUrlParams(actual_url = '&attributeCodes=', sublist, already) {
|
|
2968
3071
|
let elements = sublist || this.data;
|
|
2969
3072
|
let aux_url = '';
|
|
@@ -2985,12 +3088,14 @@ class AttributesFilter extends Filter {
|
|
|
2985
3088
|
}
|
|
2986
3089
|
}
|
|
2987
3090
|
});
|
|
2988
|
-
//console.log(actual_url + aux_url);
|
|
2989
3091
|
return actual_url + aux_url;
|
|
2990
3092
|
}
|
|
2991
3093
|
;
|
|
2992
3094
|
cleanResult(text) { return text.replace('&attributeCodes=', ''); }
|
|
2993
3095
|
;
|
|
3096
|
+
/**
|
|
3097
|
+
* ADDED: createElement transforma cada nodo original en FilterElement y marca selección inicial si corresponde.
|
|
3098
|
+
*/
|
|
2994
3099
|
createElement(filter, original, initialValue) {
|
|
2995
3100
|
if (filter.type == 'sub') {
|
|
2996
3101
|
filter.multi = false;
|
|
@@ -3003,10 +3108,65 @@ class AttributesFilter extends Filter {
|
|
|
3003
3108
|
else {
|
|
3004
3109
|
filter.value = original.code;
|
|
3005
3110
|
filter.multi = false;
|
|
3006
|
-
filter.selected = (initialValue &&
|
|
3111
|
+
filter.selected = (initialValue &&
|
|
3112
|
+
this.removeAccents(original.slug?.toLowerCase()) === this.removeAccents(initialValue?.toLowerCase()));
|
|
3007
3113
|
}
|
|
3008
3114
|
return filter;
|
|
3009
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
|
+
}
|
|
3010
3170
|
}
|
|
3011
3171
|
|
|
3012
3172
|
class DynamicsFilter extends Filter {
|
|
@@ -3167,8 +3327,8 @@ class PriceRangeFilter extends Filter {
|
|
|
3167
3327
|
if (options.minPrice != null && options.maxPrice != null) {
|
|
3168
3328
|
this.minPrice = options.minPrice;
|
|
3169
3329
|
this.maxPrice = options.maxPrice;
|
|
3170
|
-
this.currentMinPrice = options.currentMinPrice ??
|
|
3171
|
-
this.currentMaxPrice = options.currentMaxPrice ??
|
|
3330
|
+
this.currentMinPrice = options.currentMinPrice ?? null;
|
|
3331
|
+
this.currentMaxPrice = options.currentMaxPrice ?? null;
|
|
3172
3332
|
this._loaded = true;
|
|
3173
3333
|
}
|
|
3174
3334
|
};
|
|
@@ -3213,9 +3373,13 @@ class FilterFactory {
|
|
|
3213
3373
|
create(filterType, productsFilter) {
|
|
3214
3374
|
switch (filterType) {
|
|
3215
3375
|
case 'categories':
|
|
3216
|
-
return new CategoryFilter(productsFilter
|
|
3376
|
+
return new CategoryFilter(productsFilter?.type === 'categories'
|
|
3377
|
+
? productsFilter.value
|
|
3378
|
+
: null);
|
|
3217
3379
|
case 'attributes':
|
|
3218
|
-
return new AttributesFilter(productsFilter
|
|
3380
|
+
return new AttributesFilter(productsFilter?.type === 'attributes'
|
|
3381
|
+
? productsFilter.value
|
|
3382
|
+
: null);
|
|
3219
3383
|
case 'dynamics':
|
|
3220
3384
|
return new DynamicsFilter();
|
|
3221
3385
|
case 'sort':
|
|
@@ -3827,6 +3991,7 @@ class FiltersService {
|
|
|
3827
3991
|
limit: 0
|
|
3828
3992
|
};
|
|
3829
3993
|
_filtersInitialized = false;
|
|
3994
|
+
_lastRouteQuery = null;
|
|
3830
3995
|
constructor() {
|
|
3831
3996
|
//this._defaultFilters = this._consts.getDefaultFilters()
|
|
3832
3997
|
}
|
|
@@ -3847,10 +4012,20 @@ class FiltersService {
|
|
|
3847
4012
|
let extra_params = '';
|
|
3848
4013
|
this._filtersSubject.value.forEach(filter => {
|
|
3849
4014
|
const extra = filter.toUrlParams();
|
|
3850
|
-
extra.split('=')[1]
|
|
4015
|
+
if (extra && extra.split('=')[1] !== '') {
|
|
4016
|
+
extra_params += extra;
|
|
4017
|
+
}
|
|
3851
4018
|
});
|
|
3852
|
-
|
|
3853
|
-
|
|
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
|
+
}
|
|
3854
4029
|
return this.productsFilterApi(extra_params);
|
|
3855
4030
|
}
|
|
3856
4031
|
isUpdated(paginationSettings) {
|
|
@@ -3866,7 +4041,7 @@ class FiltersService {
|
|
|
3866
4041
|
this._paginationSettings = paginationSettings;
|
|
3867
4042
|
return change;
|
|
3868
4043
|
}
|
|
3869
|
-
setFilters(paginationSettings, search_value) {
|
|
4044
|
+
setFilters(paginationSettings, search_value, routeQueryParams) {
|
|
3870
4045
|
this._paginationSettings = paginationSettings;
|
|
3871
4046
|
let final_filters = [];
|
|
3872
4047
|
let filtersToProcess = this._optionsFilters?.includes('all')
|
|
@@ -3879,15 +4054,30 @@ class FiltersService {
|
|
|
3879
4054
|
if (filter) {
|
|
3880
4055
|
final_filters.push(filter);
|
|
3881
4056
|
}
|
|
3882
|
-
else {
|
|
3883
|
-
// console.warn(`❌ Failed to create filter for type: ${filterType}`);
|
|
3884
|
-
}
|
|
3885
4057
|
});
|
|
3886
4058
|
});
|
|
3887
4059
|
this._defaultFilters?.forEach(filterDefault => {
|
|
3888
4060
|
let filter = final_filters.find(filter => filter.type() == filterDefault.filter_type);
|
|
3889
4061
|
filter && filterDefault.codes.forEach(value => filter.setSelected(value));
|
|
3890
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
|
+
}
|
|
3891
4081
|
this._filtersSubject.next(final_filters);
|
|
3892
4082
|
}
|
|
3893
4083
|
getFilters(paginationSettings) {
|
|
@@ -3902,9 +4092,7 @@ class FiltersService {
|
|
|
3902
4092
|
runInInjectionContext(this.environmentInjector, () => {
|
|
3903
4093
|
const filterFactory = new FilterFactory();
|
|
3904
4094
|
filtersToProcess?.forEach(filterType => {
|
|
3905
|
-
// console.log('Creating filter for type:', filterType);
|
|
3906
4095
|
const filter = filterFactory.create(filterType, settings);
|
|
3907
|
-
// console.log('Created filter:', filter);
|
|
3908
4096
|
if (filter) {
|
|
3909
4097
|
final_filters.push(filter);
|
|
3910
4098
|
}
|
|
@@ -3929,7 +4117,6 @@ class FiltersService {
|
|
|
3929
4117
|
if (filterObj.type() !== 'price_range') {
|
|
3930
4118
|
let index = final_filters.findIndex(filter => filter.type() == filterObj.type());
|
|
3931
4119
|
final_filters[index].setSelected(filterElem, filterElem.value || filterElem.code);
|
|
3932
|
-
console.log(index, final_filters);
|
|
3933
4120
|
}
|
|
3934
4121
|
this._filtersSubject.next(final_filters);
|
|
3935
4122
|
// }
|
|
@@ -3939,13 +4126,6 @@ class FiltersService {
|
|
|
3939
4126
|
runInInjectionContext(this.environmentInjector, () => {
|
|
3940
4127
|
const filterFactory = new FilterFactory();
|
|
3941
4128
|
const filter = [];
|
|
3942
|
-
/* claves.forEach((key:any) => {
|
|
3943
|
-
filter.push(filterFactory.create(key))
|
|
3944
|
-
paginationFilters[key].forEach((value:any) => {
|
|
3945
|
-
console.log(filter[0].createElement(value,paginationFilters[key]));
|
|
3946
|
-
})
|
|
3947
|
-
|
|
3948
|
-
}) */
|
|
3949
4129
|
this._filtersSubject.value.forEach((value) => {
|
|
3950
4130
|
if (value.type() == "attributes") {
|
|
3951
4131
|
//value.data = [];
|
|
@@ -3965,6 +4145,95 @@ class FiltersService {
|
|
|
3965
4145
|
pr.setSelected(min, max);
|
|
3966
4146
|
this._filtersSubject.next([...filters]);
|
|
3967
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
|
+
}
|
|
3968
4237
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FiltersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3969
4238
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FiltersService, providedIn: 'root' });
|
|
3970
4239
|
}
|
|
@@ -3976,17 +4245,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
3976
4245
|
}], ctorParameters: () => [] });
|
|
3977
4246
|
|
|
3978
4247
|
/**
|
|
3979
|
-
* Servicio para manejar la paginación de
|
|
3980
|
-
*
|
|
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.).
|
|
3981
4256
|
*/
|
|
3982
4257
|
class PaginationService {
|
|
3983
4258
|
_connectionService = inject(ConnectionService);
|
|
3984
4259
|
_filtersService = inject(FiltersService);
|
|
3985
4260
|
_constants = inject(CoreConstantsService);
|
|
3986
|
-
|
|
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 => {
|
|
3987
4287
|
const url = this.buildUrl(filters);
|
|
3988
|
-
return
|
|
3989
|
-
})
|
|
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));
|
|
3990
4297
|
_dataPagination = signal({
|
|
3991
4298
|
attributes: [],
|
|
3992
4299
|
category: [],
|
|
@@ -4055,7 +4362,8 @@ class PaginationService {
|
|
|
4055
4362
|
this._dataPagination.set({ ...response, called: true });
|
|
4056
4363
|
this._finished = (response.page == response.pages);
|
|
4057
4364
|
this._waiting = false;
|
|
4058
|
-
// 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.
|
|
4059
4367
|
this.priceRangeSubject.next({
|
|
4060
4368
|
price_min: response.price_min,
|
|
4061
4369
|
price_max: response.price_max
|
|
@@ -4115,27 +4423,36 @@ class PaginationService {
|
|
|
4115
4423
|
* @returns
|
|
4116
4424
|
*/
|
|
4117
4425
|
buildUrl(filters) {
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
return this._filtersService.generateFinalApi();
|
|
4123
|
-
}
|
|
4426
|
+
const url = this._constants.searchValue
|
|
4427
|
+
? this._filtersService.generateFinalApi(this._constants.searchValue)
|
|
4428
|
+
: this._filtersService.generateFinalApi();
|
|
4429
|
+
return url;
|
|
4124
4430
|
}
|
|
4125
4431
|
/**
|
|
4126
|
-
* Devuelve un observable de los productos que
|
|
4432
|
+
* Devuelve un observable de los productos que devolvió la última página obtenida.
|
|
4127
4433
|
* @param url
|
|
4128
4434
|
* @returns
|
|
4129
4435
|
*/
|
|
4130
4436
|
getData(url) {
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
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
|
|
4136
4448
|
res.links ? this.updatePageData(res) : this.finish(res);
|
|
4137
|
-
})
|
|
4138
|
-
|
|
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
|
+
}));
|
|
4139
4456
|
}
|
|
4140
4457
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PaginationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4141
4458
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PaginationService, providedIn: 'root' });
|
|
@@ -4200,9 +4517,11 @@ class ProductsService {
|
|
|
4200
4517
|
* @param paginationSettings
|
|
4201
4518
|
* @param searchValue
|
|
4202
4519
|
*/
|
|
4203
|
-
getProductsForFilter(paginationSettings, searchValue) {
|
|
4520
|
+
getProductsForFilter(paginationSettings, searchValue, routeQueryParams) {
|
|
4204
4521
|
searchValue ? this.searchValue.set(searchValue) : this.searchValue.set('');
|
|
4205
|
-
|
|
4522
|
+
if (paginationSettings) {
|
|
4523
|
+
this._filtersService.setFilters(paginationSettings, searchValue, routeQueryParams);
|
|
4524
|
+
}
|
|
4206
4525
|
}
|
|
4207
4526
|
/**
|
|
4208
4527
|
* Actualiza los productos con los de la siguiente pagina.
|
|
@@ -6327,10 +6646,8 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6327
6646
|
constructor() {
|
|
6328
6647
|
super();
|
|
6329
6648
|
this._channelService.channel$.subscribe(cfg => {
|
|
6330
|
-
// console.log('Channel configuration:', cfg);
|
|
6331
6649
|
this.showPricesOnlyToLoggedUsers = !!cfg.showPricesOnlyToLoggedUsers;
|
|
6332
6650
|
this.hidePrices = !!cfg.hidePrices;
|
|
6333
|
-
// console.log('hidePrices:', this.hidePrices);
|
|
6334
6651
|
});
|
|
6335
6652
|
}
|
|
6336
6653
|
coreConstantsService = inject(CoreConstantsService);
|
|
@@ -6338,6 +6655,7 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6338
6655
|
cdr = inject(ChangeDetectorRef); // Inyectamos ChangeDetectorRef para forzar la actualización
|
|
6339
6656
|
ngOnInit() {
|
|
6340
6657
|
this.channel = this.coreConstantsService.getChannel();
|
|
6658
|
+
this.searchValue = this.coreConstantsService.searchValue || '';
|
|
6341
6659
|
this.onWindowScroll();
|
|
6342
6660
|
this.detectRouteChange(); // Llamamos a la función que detecta el cambio de ruta
|
|
6343
6661
|
// Usar el Observable del AuthService
|
|
@@ -6431,12 +6749,19 @@ class HeaderEcComponent extends MenuEcComponent {
|
|
|
6431
6749
|
}
|
|
6432
6750
|
this.searchValue = '';
|
|
6433
6751
|
this.coreConstantsService.searchValue = '';
|
|
6434
|
-
|
|
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
|
+
});
|
|
6435
6761
|
}
|
|
6436
6762
|
setupMobileMenu() {
|
|
6437
|
-
if (!isPlatformBrowser(this.platformId))
|
|
6763
|
+
if (!isPlatformBrowser(this.platformId) || typeof document === 'undefined')
|
|
6438
6764
|
return;
|
|
6439
|
-
// console.log('setupMobileMenu called');
|
|
6440
6765
|
const menuMobile = document.querySelector('.menuMobile');
|
|
6441
6766
|
if (!(menuMobile instanceof HTMLElement))
|
|
6442
6767
|
return;
|
|
@@ -8226,6 +8551,7 @@ class CollectionEcComponent {
|
|
|
8226
8551
|
_productsService = inject(ProductsService);
|
|
8227
8552
|
_activeRoute = inject(ActivatedRoute);
|
|
8228
8553
|
_optionsService = inject(OptionsService);
|
|
8554
|
+
_filtersService = inject(FiltersService);
|
|
8229
8555
|
params$ = this._activeRoute.params;
|
|
8230
8556
|
//public ready = this._optionsService.ready
|
|
8231
8557
|
queryParams$ = this._activeRoute.queryParams;
|
|
@@ -8237,40 +8563,63 @@ class CollectionEcComponent {
|
|
|
8237
8563
|
defaultFilters = [];
|
|
8238
8564
|
loading = false;
|
|
8239
8565
|
countProducts = signal(0);
|
|
8240
|
-
loaded = false;
|
|
8566
|
+
loaded = signal(false);
|
|
8241
8567
|
optionsFilters = ['all'];
|
|
8242
8568
|
filters_sort = [];
|
|
8243
|
-
_filtersService = inject(FiltersService);
|
|
8244
8569
|
filters$ = this._filtersService.filters$;
|
|
8245
8570
|
ready$ = this._filtersService.ready$;
|
|
8246
8571
|
window;
|
|
8247
8572
|
isList = false;
|
|
8573
|
+
destroy$ = new Subject();
|
|
8248
8574
|
setAsList = (value) => this.isList = value;
|
|
8249
8575
|
ngOnInit() {
|
|
8250
|
-
this.
|
|
8251
|
-
|
|
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
|
+
});
|
|
8252
8599
|
}
|
|
8253
8600
|
//protected readonly questions = signal<Question[]>([]);
|
|
8254
8601
|
total = 0;
|
|
8255
8602
|
platformId = inject(PLATFORM_ID);
|
|
8256
8603
|
constructor() {
|
|
8604
|
+
// Guardamos window sólo en Browser para evitar errores en SSR
|
|
8257
8605
|
if (isPlatformBrowser(this.platformId)) {
|
|
8258
8606
|
this.window = window;
|
|
8259
8607
|
}
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
value: params['value'] || null
|
|
8269
|
-
};
|
|
8270
|
-
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);
|
|
8271
8616
|
}
|
|
8272
8617
|
});
|
|
8273
8618
|
}
|
|
8619
|
+
ngOnDestroy() {
|
|
8620
|
+
this.destroy$.next();
|
|
8621
|
+
this.destroy$.complete();
|
|
8622
|
+
}
|
|
8274
8623
|
onScroll() {
|
|
8275
8624
|
this.loading = true;
|
|
8276
8625
|
this._productsService.updateProducts();
|
|
@@ -8898,6 +9247,7 @@ class FiltersEcComponent {
|
|
|
8898
9247
|
injector = inject(Injector);
|
|
8899
9248
|
isAuthenticated$ = this._authService.isAuthenticated();
|
|
8900
9249
|
hidePrices = false;
|
|
9250
|
+
route = inject(ActivatedRoute);
|
|
8901
9251
|
setSelect;
|
|
8902
9252
|
ngOnInit() {
|
|
8903
9253
|
}
|
|
@@ -8927,26 +9277,56 @@ class FiltersEcComponent {
|
|
|
8927
9277
|
.pop()
|
|
8928
9278
|
selectedOption && this._filtersService.setFilterSelected(this.filters[0], selectedOption) */
|
|
8929
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
|
+
*/
|
|
8930
9292
|
setSelected(filter, selected) {
|
|
8931
9293
|
if (!filter || !selected) {
|
|
8932
|
-
console.error('Filter or selected element is undefined:', { filter, selected });
|
|
8933
9294
|
return;
|
|
8934
9295
|
}
|
|
9296
|
+
// Si el elemento está marcado como no visible, no hacemos nada
|
|
9297
|
+
if (selected.isVisible === false)
|
|
9298
|
+
return;
|
|
8935
9299
|
if (typeof filter.setSelected !== 'function') {
|
|
8936
|
-
console.error('filter.setSelected is not a function. Filter might not be an instance of the expected class:', filter);
|
|
8937
9300
|
return;
|
|
8938
9301
|
}
|
|
8939
9302
|
try {
|
|
8940
|
-
// this._filtersService.setFilterSelected(filter, selected);
|
|
8941
9303
|
if (filter.type() === 'categories') {
|
|
9304
|
+
// Para categorías usamos navegación por path (ej: /collection/categories/camas-elasticas)
|
|
8942
9305
|
if (selected.path) {
|
|
8943
9306
|
this.router.navigate([selected.path]);
|
|
8944
9307
|
}
|
|
9308
|
+
return;
|
|
8945
9309
|
}
|
|
8946
|
-
|
|
8947
|
-
//
|
|
8948
|
-
|
|
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;
|
|
8949
9328
|
}
|
|
9329
|
+
this._filtersService.setFilterSelected(filter, selected);
|
|
8950
9330
|
}
|
|
8951
9331
|
catch (error) {
|
|
8952
9332
|
console.error("Error while setting selected filter:", error);
|
|
@@ -8980,7 +9360,9 @@ class FiltersEcComponent {
|
|
|
8980
9360
|
return true;
|
|
8981
9361
|
};
|
|
8982
9362
|
scrollUp = () => {
|
|
8983
|
-
|
|
9363
|
+
if (typeof window !== 'undefined') {
|
|
9364
|
+
window.scroll(0, 0);
|
|
9365
|
+
}
|
|
8984
9366
|
return true;
|
|
8985
9367
|
};
|
|
8986
9368
|
hasAppliedFilters() {
|
|
@@ -11896,6 +12278,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
|
|
|
11896
12278
|
|
|
11897
12279
|
class PriceRangeFilterComponent {
|
|
11898
12280
|
_filtersService = inject(FiltersService);
|
|
12281
|
+
router = inject(Router);
|
|
12282
|
+
route = inject(ActivatedRoute);
|
|
11899
12283
|
priceGap = 1;
|
|
11900
12284
|
roundStep = 5;
|
|
11901
12285
|
filter;
|
|
@@ -11932,7 +12316,15 @@ class PriceRangeFilterComponent {
|
|
|
11932
12316
|
return;
|
|
11933
12317
|
const min = filter.currentMinPrice ?? filter.minPrice;
|
|
11934
12318
|
const max = filter.currentMaxPrice ?? filter.maxPrice;
|
|
11935
|
-
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
|
+
});
|
|
11936
12328
|
}
|
|
11937
12329
|
getMinValue(filter) {
|
|
11938
12330
|
return (filter instanceof PriceRangeFilter && filter.currentMinPrice != null)
|