fix(search): drop unresolvable category filter options instead of faking a label

P0-4: the Catalog/Search category facet showed placeholder labels
like 'Категория 2008', 'Категория 22612' for every option — traced
to search.facade.ts buildFilterGroups() synthesizing 'Category {id}'
for every distinct product.categoryID with no attempt to resolve a
real name.

Investigated further: the mismatch isn't a missing-lookup bug, it's
a real data gap. Product categoryID values in the mock catalog
fixture don't correspond to any id in CategoryFacade's category
tree (a much smaller, separately-curated mock dataset) — there is
no real category name to show for these ids today.

Wired CategoryFacade into SearchFacade and resolve each option's
real title when the id does match; when it doesn't (the common case
with current mock data), the option is dropped rather than showing
a fabricated technical-looking label. The category filter group
simply doesn't render when nothing resolves, which is honest given
the data, instead of looking like broken/unseeded content in front
of a client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-19 22:51:19 +04:00
parent 5b063d5462
commit 374774901e

View File

@@ -4,6 +4,7 @@ import { Observable, Subject, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import { Category } from '../../../core/categories/models/category-domain.model'; import { Category } from '../../../core/categories/models/category-domain.model';
import { Product, ProductListResult } from '../../../core/products/models/product-domain.model'; import { Product, ProductListResult } from '../../../core/products/models/product-domain.model';
import { CategoryFacade } from '../../../facades/platform/category.facade';
import { ProductFacade } from '../../../facades/platform/product.facade'; import { ProductFacade } from '../../../facades/platform/product.facade';
import { TranslateService } from '../../../i18n/translate.service'; import { TranslateService } from '../../../i18n/translate.service';
import { SearchAutocompleteService } from '../services/search-autocomplete.service'; import { SearchAutocompleteService } from '../services/search-autocomplete.service';
@@ -34,6 +35,7 @@ interface LegacySearchState {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class SearchFacade { export class SearchFacade {
private readonly productFacade = inject(ProductFacade); private readonly productFacade = inject(ProductFacade);
private readonly categoryFacade = inject(CategoryFacade);
private readonly translate = inject(TranslateService); private readonly translate = inject(TranslateService);
private readonly autocompleteService = inject(SearchAutocompleteService); private readonly autocompleteService = inject(SearchAutocompleteService);
private readonly historyService = inject(SearchHistoryService); private readonly historyService = inject(SearchHistoryService);
@@ -278,7 +280,9 @@ export class SearchFacade {
id: 'category', id: 'category',
label: this.translate.t('catalog.filterCategory'), label: this.translate.t('catalog.filterCategory'),
type: 'radio', type: 'radio',
options: categories.map(value => ({ id: `cat-${value}`, label: this.translate.t('catalog.filterCategoryValue', { value }), value })), options: categories
.map(value => ({ id: `cat-${value}`, label: this.resolveCategoryLabel(value), value }))
.filter((option): option is { id: string; label: string; value: string } => option.label !== null),
enabled: enabled.has('category'), enabled: enabled.has('category'),
}, },
{ {
@@ -545,6 +549,19 @@ export class SearchFacade {
return [...new Set(values.map(value => value.trim()).filter(Boolean))]; return [...new Set(values.map(value => value.trim()).filter(Boolean))];
} }
/**
* Returns the real category title, or null when the id has no match in the loaded
* category tree — callers drop unresolved options instead of showing a fabricated
* "Category {id}" label (see RELEASE-CANDIDATE-AUDIT.md P0-4).
*/
private resolveCategoryLabel(categoryId: string): string | null {
const id = Number(categoryId);
if (!Number.isFinite(id)) {
return null;
}
return this.categoryFacade.allCategories().find(category => category.id === id)?.title ?? null;
}
private toColorHex(value: string): string { private toColorHex(value: string): string {
const normalized = value.trim().toLowerCase(); const normalized = value.trim().toLowerCase();