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.
Files changed (25) hide show
  1. package/README.md +6 -2
  2. package/esm2022/lib/classes/filters/attributes-filter.mjs +74 -4
  3. package/esm2022/lib/classes/filters/category-filter.mjs +105 -26
  4. package/esm2022/lib/classes/filters/filter-factory.mjs +7 -3
  5. package/esm2022/lib/classes/filters/price_range-filter.mjs +3 -3
  6. package/esm2022/lib/constants/core.constants.service.mjs +12 -1
  7. package/esm2022/lib/ec-components/collection-ec/collection-ec.component.mjs +41 -17
  8. package/esm2022/lib/ec-components/filters-ec/filters-ec.component.mjs +42 -9
  9. package/esm2022/lib/ec-components/header-ec/header-ec.component.mjs +12 -6
  10. package/esm2022/lib/ec-components/price-range-filter/price-range-filter.component.mjs +13 -2
  11. package/esm2022/lib/ec-services/filters.service.mjs +124 -18
  12. package/esm2022/lib/ec-services/pagination.service.mjs +70 -22
  13. package/esm2022/lib/ec-services/products.service.mjs +5 -3
  14. package/fesm2022/ng-easycommerce-v18.mjs +490 -98
  15. package/fesm2022/ng-easycommerce-v18.mjs.map +1 -1
  16. package/lib/classes/filters/attributes-filter.d.ts +24 -0
  17. package/lib/classes/filters/category-filter.d.ts +30 -3
  18. package/lib/constants/core.constants.service.d.ts +7 -0
  19. package/lib/ec-components/collection-ec/collection-ec.component.d.ts +5 -4
  20. package/lib/ec-components/filters-ec/filters-ec.component.d.ts +13 -0
  21. package/lib/ec-components/price-range-filter/price-range-filter.component.d.ts +2 -0
  22. package/lib/ec-services/filters.service.d.ts +18 -1
  23. package/lib/ec-services/pagination.service.d.ts +21 -5
  24. package/lib/ec-services/products.service.d.ts +1 -1
  25. 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(initialValues) {
2910
+ constructor(initialValue) {
2892
2911
  super();
2893
- this.initialValues = initialValues;
2894
2912
  this._optionsService.getCategories().subscribe(res => {
2895
- //console.log(res);
2896
- this.setContent(res, initialValues);
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
- throw new Error("Method not implemented.");
2926
+ return this.data;
2909
2927
  }
2910
- toUrlParams(actual_url = '', sublist) {
2911
- let elements = sublist || this.data;
2912
- elements.forEach((element) => {
2913
- if (element.type == 'sub' && !element.selected) {
2914
- const result = this.toUrlParams('', element.children);
2915
- !actual_url.includes(result) ? actual_url += result : null;
2916
- }
2917
- else if (element.selected) {
2918
- if (actual_url == '')
2919
- actual_url = '&category=' + element.value;
2920
- else
2921
- actual_url = element.value;
2922
- }
2923
- });
2924
- return actual_url;
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 && this.removeAccents(original.slug?.toLowerCase()) == this.removeAccents(initialValue?.toLowerCase()));
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 && this.removeAccents(original.slug?.toLowerCase()) == this.removeAccents(initialValue?.toLowerCase()));
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 && this.removeAccents(original.slug?.toLowerCase()) == this.removeAccents(initialValue?.toLowerCase()));
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 ?? this.minPrice;
3171
- this.currentMaxPrice = options.currentMaxPrice ?? this.maxPrice;
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 ? (productsFilter.type == 'categories' && productsFilter.value) : null);
3376
+ return new CategoryFilter(productsFilter?.type === 'categories'
3377
+ ? productsFilter.value
3378
+ : null);
3217
3379
  case 'attributes':
3218
- return new AttributesFilter(productsFilter ? (productsFilter.type == 'attributes' && productsFilter.value) : null);
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] != '' ? extra_params += extra : null;
4015
+ if (extra && extra.split('=')[1] !== '') {
4016
+ extra_params += extra;
4017
+ }
3851
4018
  });
3852
- if (search_value)
3853
- extra_params += ('&criteria[search][type]=contains&criteria[search][value]=' + search_value);
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 los productos.
3980
- * @class PaginationService
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
- paginationData$ = this._filtersService.filters$.pipe(shareReplay(1), filter(filters => filters && filters.length > 0), switchMap((filters) => {
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 this.getData(url);
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
- if (this._constants.searchValue) {
4119
- return this._filtersService.generateFinalApi(this._constants.searchValue);
4120
- }
4121
- else {
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 devolvio la última página obtenida.
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
- this._resetSubject.next(true);
4132
- this._nextProductsSubject.next([]);
4133
- this._connectionService.get(url, { limit: 10, page: 1 }).pipe(map(res => {
4134
- //console.log(res)
4135
- this._nextProductsSubject.next(res.items);
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
- })).subscribe();
4138
- return this.nextProducts$;
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
- paginationSettings && this._filtersService.setFilters(paginationSettings);
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
- this.getCollectionSearch();
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.getProducts();
8251
- this.window?.scroll(0, 0);
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
- getProducts() {
8262
- combineLatest([this.params$, this.queryParams$]).subscribe({
8263
- next: ([params, queryParams]) => {
8264
- const paginationSettings = {
8265
- latest: true,
8266
- limit: 10,
8267
- type: params['type'] || null,
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
- else if (filter.type() === 'attributes') {
8947
- // Manejar la navegación para atributos
8948
- this._filtersService.setFilterSelected(filter, selected);
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
- window.scroll(0, 0);
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._filtersService.updatePriceRangeFilter(min, max);
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)