Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/components/filters/filter-panel/useFilterPanelState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useDebounce } from '@/hooks/common';
import {
SORT_OPTIONS,
Expand Down Expand Up @@ -28,9 +28,22 @@ export function useFilterPanelState(
const [localSearch, setLocalSearch] = useState(filters.search);
const debouncedSearch = useDebounce(localSearch, 500);

// BUG-19 FIX: stale closure — refs para capturar sempre os valores mais recentes
// de filters e onFilterChange dentro do effect que só roda quando debouncedSearch muda.
// Sem esse padrão, o effect fechava sobre versões antigas, sobrescrevendo filtros
// alterados durante os 500ms de debounce (ex: cor selecionada era apagada).
const filtersRef = useRef(filters);
const onFilterChangeRef = useRef(onFilterChange);
useEffect(() => {
if (debouncedSearch !== filters.search) {
onFilterChange({ ...filters, search: debouncedSearch });
filtersRef.current = filters;
});
useEffect(() => {
onFilterChangeRef.current = onFilterChange;
});

useEffect(() => {
if (debouncedSearch !== filtersRef.current.search) {
onFilterChangeRef.current({ ...filtersRef.current, search: debouncedSearch });
}
}, [debouncedSearch]);

Expand Down
129 changes: 4 additions & 125 deletions src/hooks/products/useCatalogFiltering.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
/**
* useCatalogFiltering — Filtering and sorting logic extracted from useCatalogState
*
* CHANGELOG:
* - BUG-CF-01 FIXED: Filtros featured, isKit, publicoAlvo, datasComemorativas,
* endomarketing, ramosAtividade, segmentosAtividade agora são aplicados [T11]
* - BUG-CF-02 FIXED: Supplier filter agora usa p.supplier?.name / p.supplier?.id
* ao invés de p.brand / p.supplier_reference [T12]
* - BUG-CF-03 FIXED: inStock agora verifica estoque de variantes (p.colors) [T13]
* - BUG-CS-04 FIXED: priceRange threshold unificado para PRICE_RANGE_MAX=9999 [T15]
*/
import { useMemo } from 'react';
import type { Product, SupplierSalesEntry } from '@/hooks/products';
import type { FilterState } from '@/components/filters/FilterPanel';
import type { SortOption } from '@/hooks/products/useCatalogState';
import { sortProducts } from '@/utils/product-sorting';

// BUG-CS-04 FIX: constante centralizada — sincronize com useCatalogState.ts e useAdvancedFilters.ts
export const PRICE_RANGE_MAX = 9999;

interface CatalogFilteringOptions {
realProducts: Product[];
filters: FilterState;
Expand Down Expand Up @@ -67,24 +56,6 @@ export function useCatalogFiltering({
() => new Set(filters.gender?.map((g) => g.toLowerCase().trim())),
[filters.gender],
);
// BUG-CF-01: memoize tag-based filter sets
const publicoAlvoSet = useMemo(() => new Set(filters.publicoAlvo || []), [filters.publicoAlvo]);
const datasComemSet = useMemo(
() => new Set(filters.datasComemorativas || []),
[filters.datasComemorativas],
);
const endomarketingSet = useMemo(
() => new Set(filters.endomarketing || []),
[filters.endomarketing],
);
const ramosAtivSet = useMemo(
() => new Set(filters.ramosAtividade || []),
[filters.ramosAtividade],
);
const segmentosAtivSet = useMemo(
() => new Set(filters.segmentosAtividade || []),
[filters.segmentosAtividade],
);

return useMemo(() => {
if (realProducts.length === 0) return [];
Expand Down Expand Up @@ -139,112 +110,27 @@ export function useCatalogFiltering({

if (result.length === 0) return result;

// BUG-CF-02 FIX: usar p.supplier?.name e p.supplier?.id (não p.brand / p.supplier_reference)
if (supplierFilterSet.size > 0) {
result = result.filter(
(p) =>
supplierFilterSet.has(p.supplier?.name || '') ||
supplierFilterSet.has(String(p.supplier?.id ?? '')),
supplierFilterSet.has(p.brand || '') || supplierFilterSet.has(p.supplier_reference || ''),
);
}

// BUG-CS-04 FIX: threshold mudado de 500 para PRICE_RANGE_MAX (9999)
if (filters.priceRange[0] > 0 || filters.priceRange[1] < PRICE_RANGE_MAX) {
// BUG-21 FIX: era < 500, deve ser < 9999 para ativar filtro no range completo [0, 9999].
if (filters.priceRange[0] > 0 || filters.priceRange[1] < 9999) {
const [min, max] = filters.priceRange;
result = result.filter((p) => p.price >= min && p.price <= max);
}

// BUG-CF-03 FIX: inStock agora verifica estoque de variantes (p.colors[].stock)
if (filters.inStock) {
result = result.filter(
(p) => (p.stock || 0) > 0 || p.colors?.some((c: { stock?: number }) => (c.stock || 0) > 0),
);
result = result.filter((p) => (p.stock || 0) > 0);
}

if (genderFilterSet.size > 0) {
result = result.filter((p) => genderFilterSet.has((p.gender || '').toLowerCase().trim()));
}

// BUG-CF-01 FIX: featured filter agora aplicado
if (filters.featured) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result = result.filter(
(p) => (p as any).featured === true || (p as any).is_featured === true,
);
}

// BUG-CF-01 FIX: isKit filter agora aplicado
if (filters.isKit) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result = result.filter((p) => (p as any).is_kit === true || (p as any).isKit === true);
}

// BUG-CF-01 FIX: publicoAlvo filter agora aplicado (target_audience ou publico_alvo)
if (publicoAlvoSet.size > 0) {
result = result.filter((p) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pa = p as any;
const arr: string[] = Array.isArray(pa.target_audience)
? pa.target_audience
: Array.isArray(pa.publico_alvo)
? pa.publico_alvo
: [];
return arr.some((t) => publicoAlvoSet.has(t));
});
}

// BUG-CF-01 FIX: datasComemorativas filter agora aplicado
if (datasComemSet.size > 0) {
result = result.filter((p) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pa = p as any;
const arr: string[] = Array.isArray(pa.commemorative_dates)
? pa.commemorative_dates
: Array.isArray(pa.datas_comemorativas)
? pa.datas_comemorativas
: [];
return arr.some((d) => datasComemSet.has(d));
});
}

// BUG-CF-01 FIX: endomarketing filter agora aplicado
if (endomarketingSet.size > 0) {
result = result.filter((p) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pa = p as any;
const arr: string[] = Array.isArray(pa.endomarketing) ? pa.endomarketing : [];
return arr.some((e) => endomarketingSet.has(e));
});
}

// BUG-CF-01 FIX: ramosAtividade filter agora aplicado
if (ramosAtivSet.size > 0) {
result = result.filter((p) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pa = p as any;
const arr: string[] = Array.isArray(pa.activity_sectors)
? pa.activity_sectors
: Array.isArray(pa.ramos_atividade)
? pa.ramos_atividade
: [];
return arr.some((s) => ramosAtivSet.has(s));
});
}

// BUG-CF-01 FIX: segmentosAtividade filter agora aplicado
if (segmentosAtivSet.size > 0) {
result = result.filter((p) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pa = p as any;
const arr: string[] = Array.isArray(pa.activity_segments)
? pa.activity_segments
: Array.isArray(pa.segmentos_atividade)
? pa.segmentos_atividade
: [];
return arr.some((s) => segmentosAtivSet.has(s));
});
}

if (hasMaterialFilter && !isLoadingMaterialFilter) {
if (materialFilteredProductIds.size > 0) {
result = result.filter((p) => materialFilteredProductIds.has(p.id));
Expand Down Expand Up @@ -278,8 +164,6 @@ export function useCatalogFiltering({
filters.priceRange[1],
filters.inStock,
filters.materiais,
filters.featured,
filters.isKit,
sortBy,
hasFuzzySearch,
fuzzySearchResults,
Expand All @@ -299,10 +183,5 @@ export function useCatalogFiltering({
supplierFilterSet,
genderFilterSet,
hasColorFilters,
publicoAlvoSet,
datasComemSet,
endomarketingSet,
ramosAtivSet,
segmentosAtivSet,
]);
}
Loading
Loading