diff --git a/src/app/data/availableFilters.ts b/src/app/data/availableFilters.ts index 25ee9404..2ab7f4ec 100644 --- a/src/app/data/availableFilters.ts +++ b/src/app/data/availableFilters.ts @@ -4,103 +4,6 @@ export type Filter = { } const availableFilters: Filter[] = [ - { - name: 'technical_approach', - children: [ - { - name: 'Infrastructure as a Service (IaaS)', - children: [ - { name: 'Compute' }, - { name: 'Network' }, - { name: 'Storage' }, - { name: 'Security' }, - ], - }, - { name: 'Container as a Service (CaaS)' }, - { - name: 'Platform as a Service (PaaS)', - children: [ - { name: 'Database' }, - { name: 'Development and Testing' }, - { name: 'Business Analytics' }, - { name: 'Process management' }, - { name: 'Knowledge management' }, - { name: 'Data management' }, - ], - }, - { name: 'Software as a Service (SaaS)' }, - { name: 'Artificial Intelligence and Machine Learning' }, - { - name: 'Data as a Service (DaaS)', - children: [{ name: 'Data product distribution and exchange' }], - }, - { name: 'Cybersecurity and Data Privacy' }, - { name: 'Internet of Things (IoT)' }, - ], - }, - { - name: 'business_domain', - children: [ - { name: 'Automotive' }, - { name: 'Agriculture, Forestry, Fishing' }, - { name: 'Blockchain (DLT)' }, - { name: 'Beauty and Perfume' }, - { name: 'Cleaning and Facility Management Services' }, - { - name: 'Community Groups, Social, Political and Religious', - children: [{ name: 'Governmental Administration and Regulation' }], - }, - { name: 'Education' }, - { name: 'Construction' }, - { name: 'Employment, Recruitment, HR' }, - { - name: 'Energy and Utility Suppliers', - children: [ - { name: 'Electricity' }, - { name: 'Gas' }, - { name: 'Waste Collection, Treatment and Disposal Activities' }, - { name: 'Water Supply' }, - ], - }, - { name: 'Financial Services and Insurance' }, - { name: 'Healthcare' }, - { name: 'IT' }, - { name: 'Leisure and Entertainment' }, - { name: 'Legal, Public Order, Security' }, - { - name: 'Manufacturing', - children: [{ name: 'Manufacturing of Metal Products' }, { name: 'Other (manufacturing)' }], - }, - { name: 'Mining and Drilling' }, - { name: 'Project Management, Marketing and Admin' }, - { name: 'Personal Services' }, - { name: 'Restaurants, Bars, Cafes, Catering' }, - { name: 'Real Estate' }, - { name: 'Publishing, Printing and Photography' }, - { name: 'Tourism and Accommodation' }, - { name: 'Science and Engineering' }, - { name: 'Trade' }, - { - name: 'Transportation and Transportation infrastructure', - children: [ - { name: 'Transport of Freight' }, - { name: 'Transport of Persons' }, - { name: 'Other' }, - ], - }, - ], - }, - { - name: 'professional_services', - children: [ - { name: 'Implementation Services' }, - { name: 'Consulting' }, - { - name: 'Service Management', - children: [{ name: 'Operations' }, { name: 'Maintenance' }, { name: 'Governance' }], - }, - ], - }, { name: 'compliance_profile', children: [ diff --git a/src/app/pages/admin/categories/categories.component.ts b/src/app/pages/admin/categories/categories.component.ts index d98a129a..8b75de42 100644 --- a/src/app/pages/admin/categories/categories.component.ts +++ b/src/app/pages/admin/categories/categories.component.ts @@ -61,6 +61,7 @@ export class CategoriesComponent implements OnDestroy { initCatalogs(){ this.loading=true; this.categories=[]; + this.unformattedCategories=[]; let aux = this.localStorage.getObject('login_items') as LoginInfo; if(aux.logged_as==aux.id){ this.partyId = aux.partyId; @@ -69,7 +70,7 @@ export class CategoriesComponent implements OnDestroy { this.partyId = loggedOrg.partyId } - this.getCategories(); + void this.getCategories(); initFlowbite(); } @@ -81,7 +82,7 @@ export class CategoriesComponent implements OnDestroy { this.eventMessage.emitUpdateCategory(cat); } - getCategories(){ + async getCategories(){ /*this.api.getCatalog(this.selectedCatalog.id).then(data => { if(data.category){ for (let i=0; i { - for(let i=0; i < data.length; i++){ - this.findChildren(data[i],data); - this.unformattedCategories.push(data[i]); - } - this.loading=false; - this.cdr.detectChanges(); - initFlowbite(); - }) - } - - findChildren(parent:any,data:any[]){ - let childs = data.filter((p => p.parentId === parent.id)); - parent["children"] = childs; - if(parent.isRoot == true){ - this.categories.push(parent) - } else { - this.saveChildren(this.categories,parent) - } - if(childs.length != 0){ - for(let i=0; i < childs.length; i++){ - this.findChildren(childs[i],data) - } - } - } + const rootCategories = await this.api.getDefaultCategories().catch(() => []); + const roots = Array.isArray(rootCategories) ? rootCategories : []; - findChildrenByParent(parent:any){ - let childs: any[] = [] - this.api.getCategoriesByParentId(parent.id).then(c => { - childs=c; - parent["children"] = childs; - if(parent.isRoot == true){ - this.categories.push(parent) - } else { - this.saveChildren(this.categories,parent) - } - if(childs.length != 0){ - for(let i=0; i < childs.length; i++){ - this.findChildrenByParent(childs[i]) - } - } - initFlowbite(); - }) + this.unformattedCategories = [...roots]; + const categoryTrees = await Promise.all( + roots.map((root: any) => this.loadCategorySubtree(root)) + ); + this.categories = categoryTrees.filter((cat): cat is Category => !!cat); + this.loading=false; + this.cdr.detectChanges(); + initFlowbite(); } - saveChildren(superCategories:any[],parent:any){ - for(let i=0; i < superCategories.length; i++){ - let children = superCategories[i].children; - if (children != undefined){ - let check = children.find((element: { id: any; }) => element.id == parent.id) - if (check != undefined) { - let idx = children.findIndex((element: { id: any; }) => element.id == parent.id) - children[idx] = parent - superCategories[i].children = children - } - this.saveChildren(children,parent) - } - } + private async loadCategorySubtree(parent:any): Promise { + const children = await this.api.getCategoriesByParentId(parent.id).catch(() => []); + const childList = Array.isArray(children) ? children : []; + const resolvedChildren = await Promise.all( + childList.map((child: any) => this.loadCategorySubtree(child)) + ); + + return { + ...parent, + children: resolvedChildren + }; } /*addParent(parentId:any){ @@ -187,7 +154,8 @@ export class CategoriesComponent implements OnDestroy { } this.loading=true; this.categories=[]; - this.getCategories(); + this.unformattedCategories=[]; + void this.getCategories(); console.log('filter') } diff --git a/src/app/pages/admin/categories/create-category/create-category.component.ts b/src/app/pages/admin/categories/create-category/create-category.component.ts index e0f8c204..99d74ee5 100644 --- a/src/app/pages/admin/categories/create-category/create-category.component.ts +++ b/src/app/pages/admin/categories/create-category/create-category.component.ts @@ -101,74 +101,44 @@ export class CreateCategoryComponent implements OnInit, OnDestroy { this.partyId = loggedOrg.partyId } } - this.getCategories(); + void this.getCategories(); } goBack() { this.eventMessage.emitAdminCategories(true); } - getCategories(){ + async getCategories(){ console.log('Getting categories...') - this.api.getLaunchedCategories().then(data => { - for(let i=0; i < data.length; i++){ - this.findChildren(data[i],data); - this.unformattedCategories.push(data[i]); - } - this.loading=false; - this.cdr.detectChanges(); - initFlowbite(); - }) - } + this.loading = true; + this.categories = []; + this.unformattedCategories = []; - findChildren(parent:any,data:any[]){ - let childs = data.filter((p => p.parentId === parent.id)); - parent["children"] = childs; - if(parent.isRoot == true){ - this.categories.push(parent) - } else { - this.saveChildren(this.categories,parent) - } - if(childs.length != 0){ - for(let i=0; i < childs.length; i++){ - this.findChildren(childs[i],data) - } - } - } + const rootCategories = await this.api.getDefaultCategories().catch(() => []); + const roots = Array.isArray(rootCategories) ? rootCategories : []; - findChildrenByParent(parent:any){ - let childs: any[] = [] - this.api.getCategoriesByParentId(parent.id).then(c => { - childs=c; - parent["children"] = childs; - if(parent.isRoot == true){ - this.categories.push(parent) - } else { - this.saveChildren(this.categories,parent) - } - if(childs.length != 0){ - for(let i=0; i < childs.length; i++){ - this.findChildrenByParent(childs[i]) - } - } - initFlowbite(); - }) + this.unformattedCategories = [...roots]; + const categoryTrees = await Promise.all( + roots.map((root: any) => this.loadCategorySubtree(root)) + ); + this.categories = categoryTrees.filter(Boolean); + this.loading=false; + this.cdr.detectChanges(); + initFlowbite(); } - saveChildren(superCategories:any[],parent:any){ - for(let i=0; i < superCategories.length; i++){ - let children = superCategories[i].children; - if (children != undefined){ - let check = children.find((element: { id: any; }) => element.id == parent.id) - if (check != undefined) { - let idx = children.findIndex((element: { id: any; }) => element.id == parent.id) - children[idx] = parent - superCategories[i].children = children - } - this.saveChildren(children,parent) - } - } + private async loadCategorySubtree(parent:any): Promise { + const children = await this.api.getCategoriesByParentId(parent.id).catch(() => []); + const childList = Array.isArray(children) ? children : []; + const resolvedChildren = await Promise.all( + childList.map((child: any) => this.loadCategorySubtree(child)) + ); + + return { + ...parent, + children: resolvedChildren + }; } toggleGeneral(){ diff --git a/src/app/pages/admin/categories/update-category/update-category.component.ts b/src/app/pages/admin/categories/update-category/update-category.component.ts index 34ba301a..21710408 100644 --- a/src/app/pages/admin/categories/update-category/update-category.component.ts +++ b/src/app/pages/admin/categories/update-category/update-category.component.ts @@ -106,7 +106,7 @@ export class UpdateCategoryComponent implements OnInit, OnDestroy { this.partyId = loggedOrg.partyId } } - this.getCategories(); + void this.getCategories(); this.populateCatInfo(); } @@ -130,75 +130,74 @@ export class UpdateCategoryComponent implements OnInit, OnDestroy { this.eventMessage.emitAdminCategories(true); } - getCategories(){ + async getCategories(){ console.log('Getting categories...') - this.api.getLaunchedCategories().then(data => { - for(let i=0; i < data.length; i++){ - this.findChildren(data[i],data); - this.unformattedCategories.push(data[i]); - } - this.loading=false; - if(this.category.isRoot==false){ - const index = this.categories.findIndex(item => item.id === this.category.parentId); - if (index !== -1) { - this.selectedCategory=this.categories[index] - this.selected=[] - this.selected.push(this.selectedCategory) - } - } - this.cdr.detectChanges(); - initFlowbite(); - }) - } + this.loading = true; + this.categories = []; + this.unformattedCategories = []; - findChildren(parent:any,data:any[]){ - let childs = data.filter((p => p.parentId === parent.id)); - parent["children"] = childs; - if(parent.isRoot == true){ - this.categories.push(parent) - } else { - this.saveChildren(this.categories,parent) - } - if(childs.length != 0){ - for(let i=0; i < childs.length; i++){ - this.findChildren(childs[i],data) + const rootCategories = await this.api.getDefaultCategories().catch(() => []); + const roots = Array.isArray(rootCategories) ? rootCategories : []; + + this.unformattedCategories = [...roots]; + const categoryTrees = await Promise.all( + roots.map((root: any) => this.loadCategorySubtree(root)) + ); + + this.categories = this.removeCategoryFromTree( + categoryTrees.filter(Boolean), + this.category?.id + ); + this.loading=false; + + if(this.category.isRoot==false){ + const parentCategory = this.findCategoryById(this.categories, this.category.parentId); + if (parentCategory) { + this.selectedCategory = parentCategory; + this.selected = [parentCategory]; } } + this.cdr.detectChanges(); + initFlowbite(); } - findChildrenByParent(parent:any){ - let childs: any[] = [] - this.api.getCategoriesByParentId(parent.id).then(c => { - childs=c; - parent["children"] = childs; - if(parent.isRoot == true){ - this.categories.push(parent) - } else { - this.saveChildren(this.categories,parent) - } - if(childs.length != 0){ - for(let i=0; i < childs.length; i++){ - this.findChildrenByParent(childs[i]) - } - } - initFlowbite(); - }) + private async loadCategorySubtree(parent:any): Promise { + const children = await this.api.getCategoriesByParentId(parent.id).catch(() => []); + const childList = Array.isArray(children) ? children : []; + const resolvedChildren = await Promise.all( + childList.map((child: any) => this.loadCategorySubtree(child)) + ); + return { + ...parent, + children: resolvedChildren + }; } - saveChildren(superCategories:any[],parent:any) { - for(let i=0; i < superCategories.length; i++){ - let children = superCategories[i].children; - if (children != undefined){ - let check = children.find((element: { id: any; }) => element.id == parent.id) - if (check != undefined) { - let idx = children.findIndex((element: { id: any; }) => element.id == parent.id) - children[idx] = parent - superCategories[i].children = children - } - this.saveChildren(children,parent) + private findCategoryById(categories: any[], categoryId: string): any | undefined { + for (const category of categories || []) { + if (category?.id === categoryId) { + return category; + } + const foundInChildren = this.findCategoryById(category?.children || [], categoryId); + if (foundInChildren) { + return foundInChildren; } } + return undefined; + } + + private removeCategoryFromTree(categories: any[], categoryId: string): any[] { + if (!categoryId) { + return categories || []; + } + + return (categories || []) + .filter((category) => category?.id !== categoryId) + .map((category) => ({ + ...category, + children: this.removeCategoryFromTree(category?.children || [], categoryId) + })); } toggleGeneral() { diff --git a/src/app/shared/categories-filter/categories-filter.component.ts b/src/app/shared/categories-filter/categories-filter.component.ts index b819f099..60433a02 100644 --- a/src/app/shared/categories-filter/categories-filter.component.ts +++ b/src/app/shared/categories-filter/categories-filter.component.ts @@ -39,9 +39,8 @@ export class CategoriesFilterComponent implements OnInit, OnDestroy { // AI Search facets aiSearchEnabled = environment.AI_SEARCH_ENABLED; aiFacets: Record> = {}; - aiFacetCategories: Category[] = []; - originalCategories: Category[] = []; // Store original categories from API - aiSearchPerformed: boolean = false; // Track if AI search has been performed + dynamicAiCategories: Category[] = []; + configuredAiCategories: Category[] = []; protected readonly faCircleCheck = faCircleCheck; protected readonly faCircle = faCircle; @@ -70,14 +69,16 @@ export class CategoriesFilterComponent implements OnInit, OnDestroy { } } else if(ev.type === 'AiSearchFacets' && this.aiSearchEnabled){ const facets = ev.value as Record>; - if (facets && Object.keys(facets).length > 0 && !this.aiSearchPerformed) { - this.aiSearchPerformed = true; - this.aiFacets = facets; - this.updateAiFacetCategories(); - this.cdr.detectChanges(); - } + this.aiFacets = facets || {}; + this.updateAiFacetCategories(); + this.cdr.detectChanges(); } else if(ev.type === 'AiSearchCleared' && this.aiSearchEnabled){ - this.restoreOriginalCategories(); + this.aiFacets = {}; + this.categories = [ + ...this.cloneCategories(this.dynamicAiCategories), + ...this.cloneCategories(this.configuredAiCategories) + ]; + initFlowbite(); this.cdr.detectChanges(); } }) @@ -90,40 +91,21 @@ export class CategoriesFilterComponent implements OnInit, OnDestroy { } if (this.aiSearchEnabled) { - this.categories = this.convertFiltersToCategories(availableFilters); - this.originalCategories = [...this.categories]; + await this.loadCatalogCategories(); + this.dynamicAiCategories = this.convertDynamicCategoriesToAiFilterCategories(this.categories); + this.configuredAiCategories = this.convertFiltersToCategories(availableFilters); + this.categories = [ + ...this.cloneCategories(this.dynamicAiCategories), + ...this.cloneCategories(this.configuredAiCategories) + ]; this.cdr.detectChanges(); initFlowbite(); return; } - if(this.catalogId!=undefined){ - let data = await this.api.getCatalog(this.catalogId); - if(data.category){ - for (let i=0; i> + ): Category[] { + return (dynamicRoots || []).map(root => { + const rootFacetData = this.getFacetDataForDynamicRoot(root, facets); + return this.applyDynamicCategoryCounts(root, rootFacetData, facets); + }); + } + + private getFacetDataForDynamicRoot( + root: Category, + facets: Record> + ): Record { + const candidates = [ + String(root.id || ''), + this.toAiFacetKey(root.name || ''), + String(root.name || '') + ].filter(Boolean); + + for (const key of candidates) { + if (facets?.[key]) { + return facets[key]; + } } - this.categories = this.originalCategories.map(rootCat => { - const facetKey = rootCat.id; // e.g. 'technical_approach' - const facetData = this.aiFacets[facetKey || ''] || {}; + return {}; + } + + private applyDynamicCategoryCounts( + category: Category, + rootFacetData: Record, + facets: Record> + ): Category { + const children = (category.children || []).map(child => this.applyDynamicCategoryCounts(child, rootFacetData, facets)); + const count = this.resolveDynamicCategoryCount(category, rootFacetData, facets); + + return { + ...category, + count: typeof count === 'number' && Number.isFinite(count) && count > 0 ? count : undefined, + children + }; + } + + private resolveDynamicCategoryCount( + category: Category, + rootFacetData: Record, + facets: Record> + ): number | undefined { + const localCount = this.readCountFromFacetMap(rootFacetData, category.name, category.id); + if (Number.isFinite(localCount) && (localCount as number) > 0) { + return localCount; + } + + const selfFacetMap = facets?.[String(category.id || '')] || facets?.[String(category.name || '')]; + if (selfFacetMap && typeof selfFacetMap === 'object') { + const selfFacetTotal = this.sumFacetCounts(selfFacetMap); + if (selfFacetTotal > 0) { + return selfFacetTotal; + } + } + + const globalCount = this.findGlobalFacetCount(category, facets); + if (globalCount > 0) { + return globalCount; + } + + return undefined; + } + + private readCountFromFacetMap( + facetMap: Record | undefined, + categoryName?: string, + categoryId?: string + ): number | undefined { + if (!facetMap) { + return undefined; + } + + const byName = categoryName !== undefined ? Number(facetMap[categoryName]) : NaN; + if (Number.isFinite(byName) && byName > 0) { + return byName; + } + + const byId = categoryId !== undefined ? Number(facetMap[categoryId]) : NaN; + if (Number.isFinite(byId) && byId > 0) { + return byId; + } + + return undefined; + } + + private findGlobalFacetCount( + category: Category, + facets: Record> + ): number { + let total = 0; + for (const facetMap of Object.values(facets || {})) { + const count = this.readCountFromFacetMap(facetMap, category.name, category.id); + if (count && count > 0) { + total += count; + } + } + return total; + } + + private sumFacetCounts(facetMap: Record): number { + return Object.values(facetMap || {}).reduce((sum, value) => { + const numericValue = Number(value); + return Number.isFinite(numericValue) && numericValue > 0 ? sum + numericValue : sum; + }, 0); + } + + private applyFacetCountsToConfiguredCategories( + configuredRoots: Category[], + facets: Record> + ): Category[] { + return (configuredRoots || []).map(root => { + const facetKey = String(root.id || ''); + const facetData = facets?.[facetKey] || {}; return { - ...rootCat, - children: this.applyFacetCounts(rootCat.children || [], facetData, facetKey || '') + ...root, + children: this.applyFacetCountsRecursively(root.children || [], facetData) }; }); + } - initFlowbite(); + private applyFacetCountsRecursively( + categories: Category[], + facetData: Record + ): Category[] { + return (categories || []).map(category => { + const children = this.applyFacetCountsRecursively(category.children || [], facetData); + const rawCount = facetData[category.name]; + const count = Number(rawCount); + + return { + ...category, + count: Number.isFinite(count) && count > 0 ? count : undefined, + children + }; + }); + } + + private cloneCategories(categories: Category[]): Category[] { + return (categories || []).map(category => ({ + ...category, + children: this.cloneCategories(category.children || []) + })); } - private applyFacetCounts(children: Category[], facetData: Record, rootFilterKey: string): Category[] { - const result: Category[] = []; - for (const child of children) { - const count = facetData[child.name] ?? 0; - const updatedChildren = child.children && child.children.length > 0 - ? this.applyFacetCounts(child.children, facetData, rootFilterKey) - : []; - - if (count > 0 || updatedChildren.length > 0) { - result.push({ - ...child, - count: count > 0 ? count : child.count, - children: updatedChildren - }); + private convertDynamicCategoriesToAiFilterCategories(categories: Category[]): Category[] { + return (categories || []).map(rootCategory => { + const rootKey = this.toAiFacetKey(rootCategory.name || ''); + return this.convertDynamicCategoryNode(rootCategory, rootKey, true); + }); + } + + private convertDynamicCategoryNode(category: Category, rootKey: string, isRoot: boolean): Category { + const nodeId = isRoot ? rootKey : `${rootKey}::${category.name}`; + + return { + ...category, + id: nodeId, + sanitizedId: isRoot + ? this.sanitizeIdForCss(rootKey) + : `${this.sanitizeIdForCss(rootKey)}-${this.sanitizeIdForCss(category.name || '')}`, + children: (category.children || []).map(child => this.convertDynamicCategoryNode(child, rootKey, false)) + }; + } + + private async loadCatalogCategories(): Promise { + this.categories = []; + const hasCatalogId = this.catalogId !== undefined && this.catalogId !== null && String(this.catalogId).trim() !== ''; + + if (hasCatalogId) { + const data = await this.api.getCatalog(this.catalogId).catch(() => null); + const catalogCategoryRefs: any[] = Array.isArray(data?.category) ? data.category : []; + if (catalogCategoryRefs.length > 0) { + const rootTrees = await Promise.all( + catalogCategoryRefs.map(async (categoryRef: any) => { + if (!categoryRef?.id) { + return null; + } + const rootCategory = await this.api.getCategoryById(categoryRef.id).catch(() => null); + const parentId = rootCategory?.parentId ? String(rootCategory.parentId) : ''; + const isStrictRoot = !!rootCategory?.id && rootCategory.isRoot === true && !parentId; + if (!isStrictRoot) { + return null; + } + return this.loadCategorySubtree(rootCategory); + }) + ); + this.categories = rootTrees.filter((root): root is Category => root !== null); + return; } } - return result; + + const launched = await this.api.getLaunchedCategories(); + const launchedList = Array.isArray(launched) ? launched : []; + for (const category of launchedList) { + this.findChildren(category, launchedList); + } + } + + private async loadCategorySubtree(parent: any): Promise { + const children = await this.api.getCategoriesByParentId(parent.id).catch(() => []); + const childList = Array.isArray(children) ? children : []; + const resolvedChildren = await Promise.all( + childList.map((child: any) => this.loadCategorySubtree(child)) + ); + + return { + ...parent, + children: resolvedChildren + }; } private formatFacetName(key: string): string { @@ -343,16 +496,6 @@ export class CategoriesFilterComponent implements OnInit, OnDestroy { .join(' '); } - private restoreOriginalCategories(): void { - if (this.originalCategories.length > 0) { - this.categories = [...this.originalCategories]; - this.aiSearchPerformed = false; - this.aiFacetCategories = []; - this.aiFacets = {}; - initFlowbite(); - } - } - private convertFiltersToCategories(filters: Filter[]): Category[] { return filters.map(filter => this.convertFilterToCategory(filter, true, filter.name)); } @@ -360,14 +503,14 @@ export class CategoriesFilterComponent implements OnInit, OnDestroy { private convertFilterToCategory(filter: Filter, isRoot: boolean = false, rootFilterName?: string): Category { const filterKey = rootFilterName || filter.name; const sanitizedId = this.sanitizeIdForCss(filter.name); - const category: Category = { + + return { id: isRoot ? filter.name : `${filterKey}::${filter.name}`, name: isRoot ? this.formatFacetName(filter.name) : filter.name, - isRoot: isRoot, - children: filter.children ? filter.children.map(child => this.convertFilterToCategory(child, false, filterKey)) : [], + isRoot, + children: (filter.children || []).map(child => this.convertFilterToCategory(child, false, filterKey)), sanitizedId: isRoot ? sanitizedId : `${this.sanitizeIdForCss(filterKey)}-${sanitizedId}` }; - return category; } private sanitizeIdForCss(str: string): string { @@ -378,4 +521,14 @@ export class CategoriesFilterComponent implements OnInit, OnDestroy { .toLowerCase(); } -} \ No newline at end of file + private toAiFacetKey(value: string): string { + return (value || '') + .trim() + .toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); + } + +}