151 lines
3.4 KiB
TypeScript
151 lines
3.4 KiB
TypeScript
// Category types and utilities
|
|
|
|
export interface Category {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
path: string;
|
|
level: number;
|
|
parent_id: number | null;
|
|
category_count: number;
|
|
children?: Category[];
|
|
}
|
|
|
|
export interface CategoryTreeNode {
|
|
id: string;
|
|
name: string;
|
|
children?: CategoryTreeNode[];
|
|
data?: Category;
|
|
}
|
|
|
|
export interface CategoryStats {
|
|
total: number;
|
|
sectors: number;
|
|
business_types: number;
|
|
sub_categories: number;
|
|
leaf_categories: number;
|
|
}
|
|
|
|
// Convert flat categories to tree structure
|
|
export function buildCategoryTree(categories: Category[]): CategoryTreeNode[] {
|
|
const map = new Map<string, CategoryTreeNode>();
|
|
const roots: CategoryTreeNode[] = [];
|
|
|
|
// First pass: create all nodes
|
|
for (const cat of categories) {
|
|
map.set(cat.path, {
|
|
id: cat.path,
|
|
name: cat.name,
|
|
children: [],
|
|
data: cat,
|
|
});
|
|
}
|
|
|
|
// Second pass: link children to parents
|
|
for (const cat of categories) {
|
|
const node = map.get(cat.path)!;
|
|
const pathParts = cat.path.split('.');
|
|
|
|
if (pathParts.length === 1) {
|
|
// Root node
|
|
roots.push(node);
|
|
} else {
|
|
// Find parent
|
|
const parentPath = pathParts.slice(0, -1).join('.');
|
|
const parent = map.get(parentPath);
|
|
if (parent) {
|
|
parent.children!.push(node);
|
|
} else {
|
|
// Parent not found, add as root
|
|
roots.push(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort children alphabetically
|
|
const sortChildren = (nodes: CategoryTreeNode[]) => {
|
|
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
|
for (const node of nodes) {
|
|
if (node.children && node.children.length > 0) {
|
|
sortChildren(node.children);
|
|
}
|
|
}
|
|
};
|
|
|
|
sortChildren(roots);
|
|
|
|
return roots;
|
|
}
|
|
|
|
// Convert to react-d3-tree format
|
|
export function toD3Tree(nodes: CategoryTreeNode[]): any {
|
|
return nodes.map((node) => ({
|
|
name: node.name,
|
|
attributes: {
|
|
level: node.data?.level,
|
|
count: node.data?.category_count,
|
|
},
|
|
children: node.children && node.children.length > 0
|
|
? toD3Tree(node.children)
|
|
: undefined,
|
|
}));
|
|
}
|
|
|
|
// Get breadcrumb path for a category
|
|
export function getCategoryBreadcrumb(path: string, categories: Category[]): Category[] {
|
|
const parts = path.split('.');
|
|
const breadcrumb: Category[] = [];
|
|
|
|
for (let i = 1; i <= parts.length; i++) {
|
|
const partialPath = parts.slice(0, i).join('.');
|
|
const cat = categories.find((c) => c.path === partialPath);
|
|
if (cat) {
|
|
breadcrumb.push(cat);
|
|
}
|
|
}
|
|
|
|
return breadcrumb;
|
|
}
|
|
|
|
// Search categories
|
|
export function searchCategories(categories: Category[], query: string): Category[] {
|
|
const lowerQuery = query.toLowerCase();
|
|
return categories.filter(
|
|
(cat) =>
|
|
cat.name.toLowerCase().includes(lowerQuery) ||
|
|
cat.path.toLowerCase().includes(lowerQuery)
|
|
);
|
|
}
|
|
|
|
// Get level name
|
|
export function getLevelName(level: number): string {
|
|
switch (level) {
|
|
case 1:
|
|
return 'Sector';
|
|
case 2:
|
|
return 'Business Type';
|
|
case 3:
|
|
return 'Sub-category';
|
|
case 4:
|
|
return 'Category';
|
|
default:
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
// Get level color
|
|
export function getLevelColor(level: number): string {
|
|
switch (level) {
|
|
case 1:
|
|
return 'bg-blue-500';
|
|
case 2:
|
|
return 'bg-green-500';
|
|
case 3:
|
|
return 'bg-yellow-500';
|
|
case 4:
|
|
return 'bg-purple-500';
|
|
default:
|
|
return 'bg-gray-500';
|
|
}
|
|
}
|