Compare commits
No commits in common. "b8fa089c5f409c1ababb48382479c3f66d18ea23" and "b37be36ddc6088110ae4b9ede61d31555a7b2fc9" have entirely different histories.
b8fa089c5f
...
b37be36ddc
3 changed files with 0 additions and 222 deletions
|
|
@ -1,32 +0,0 @@
|
||||||
import { useMemo } from "react";
|
|
||||||
import {
|
|
||||||
getTaxonomyV1,
|
|
||||||
findById,
|
|
||||||
findByPath,
|
|
||||||
getLeaves,
|
|
||||||
getParentById,
|
|
||||||
type Taxonomy,
|
|
||||||
type TaxonomyNode,
|
|
||||||
type TaxonomyLeaf,
|
|
||||||
} from "../services/categoryTaxonomyService";
|
|
||||||
|
|
||||||
export interface UseCategoryTaxonomyResult {
|
|
||||||
taxonomy: Taxonomy;
|
|
||||||
findById: (id: number) => TaxonomyNode | null;
|
|
||||||
findByPath: (path: string[]) => TaxonomyNode | null;
|
|
||||||
getLeaves: () => TaxonomyLeaf[];
|
|
||||||
getParentById: (id: number) => TaxonomyNode | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCategoryTaxonomy(): UseCategoryTaxonomyResult {
|
|
||||||
return useMemo<UseCategoryTaxonomyResult>(
|
|
||||||
() => ({
|
|
||||||
taxonomy: getTaxonomyV1(),
|
|
||||||
findById,
|
|
||||||
findByPath,
|
|
||||||
getLeaves,
|
|
||||||
getParentById,
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import { describe, it, expect, beforeEach } from "vitest";
|
|
||||||
import {
|
|
||||||
getTaxonomyV1,
|
|
||||||
findById,
|
|
||||||
findByPath,
|
|
||||||
getLeaves,
|
|
||||||
getParentById,
|
|
||||||
resetTaxonomyCache,
|
|
||||||
} from "./categoryTaxonomyService";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
resetTaxonomyCache();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getTaxonomyV1", () => {
|
|
||||||
it("returns version v1 with non-empty roots", () => {
|
|
||||||
const tax = getTaxonomyV1();
|
|
||||||
expect(tax.version).toBe("v1");
|
|
||||||
expect(tax.roots.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("caches the taxonomy across calls (same reference)", () => {
|
|
||||||
const a = getTaxonomyV1();
|
|
||||||
const b = getTaxonomyV1();
|
|
||||||
expect(a).toBe(b);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findById", () => {
|
|
||||||
it("finds a root-level node", () => {
|
|
||||||
const node = findById(1000);
|
|
||||||
expect(node).not.toBeNull();
|
|
||||||
expect(node?.name).toBe("Revenus");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds a nested leaf node", () => {
|
|
||||||
const node = findById(1011);
|
|
||||||
expect(node).not.toBeNull();
|
|
||||||
expect(node?.i18n_key).toBe("categoriesSeed.revenus.emploi.paie");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null for an unknown id", () => {
|
|
||||||
expect(findById(99999)).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findByPath", () => {
|
|
||||||
it("returns null for an empty path", () => {
|
|
||||||
expect(findByPath([])).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds a root by single-segment path", () => {
|
|
||||||
const node = findByPath(["Revenus"]);
|
|
||||||
expect(node?.id).toBe(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds a nested leaf by multi-segment path", () => {
|
|
||||||
const node = findByPath(["Revenus", "Emploi", "Paie régulière"]);
|
|
||||||
expect(node?.id).toBe(1011);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null when a segment does not match", () => {
|
|
||||||
expect(findByPath(["Revenus", "Emploi", "NoSuchLeaf"])).toBeNull();
|
|
||||||
expect(findByPath(["Unknown"])).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getLeaves", () => {
|
|
||||||
it("returns only nodes with no children", () => {
|
|
||||||
const leaves = getLeaves();
|
|
||||||
expect(leaves.length).toBeGreaterThan(0);
|
|
||||||
for (const leaf of leaves) {
|
|
||||||
expect(leaf.children.length).toBe(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes a known leaf by id", () => {
|
|
||||||
const leaves = getLeaves();
|
|
||||||
expect(leaves.some((l) => l.id === 1011)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getParentById", () => {
|
|
||||||
it("returns the direct parent of a leaf", () => {
|
|
||||||
const parent = getParentById(1011);
|
|
||||||
expect(parent?.id).toBe(1010);
|
|
||||||
expect(parent?.name).toBe("Emploi");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns the root as parent of a level-2 node", () => {
|
|
||||||
const parent = getParentById(1010);
|
|
||||||
expect(parent?.id).toBe(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null for a root-level node", () => {
|
|
||||||
expect(getParentById(1000)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null for an unknown id", () => {
|
|
||||||
expect(getParentById(99999)).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import taxonomyData from "../data/categoryTaxonomyV1.json";
|
|
||||||
|
|
||||||
export type TaxonomyCategoryType = "expense" | "income" | "transfer";
|
|
||||||
|
|
||||||
export interface TaxonomyNode {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
i18n_key: string;
|
|
||||||
type: TaxonomyCategoryType;
|
|
||||||
color: string;
|
|
||||||
sort_order: number;
|
|
||||||
children: TaxonomyNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TaxonomyLeaf = TaxonomyNode & { children: [] };
|
|
||||||
|
|
||||||
export type TaxonomyRoot = TaxonomyNode;
|
|
||||||
|
|
||||||
export interface Taxonomy {
|
|
||||||
version: string;
|
|
||||||
description: string;
|
|
||||||
roots: TaxonomyRoot[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let cached: Taxonomy | null = null;
|
|
||||||
|
|
||||||
function loadTaxonomy(): Taxonomy {
|
|
||||||
if (cached !== null) return cached;
|
|
||||||
cached = taxonomyData as Taxonomy;
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTaxonomyV1(): Taxonomy {
|
|
||||||
return loadTaxonomy();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findById(id: number): TaxonomyNode | null {
|
|
||||||
const stack: TaxonomyNode[] = [...loadTaxonomy().roots];
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const node = stack.pop() as TaxonomyNode;
|
|
||||||
if (node.id === id) return node;
|
|
||||||
stack.push(...node.children);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findByPath(path: string[]): TaxonomyNode | null {
|
|
||||||
if (path.length === 0) return null;
|
|
||||||
const { roots } = loadTaxonomy();
|
|
||||||
let current: TaxonomyNode | undefined = roots.find((r) => r.name === path[0]);
|
|
||||||
for (let i = 1; i < path.length && current !== undefined; i++) {
|
|
||||||
current = current.children.find((c) => c.name === path[i]);
|
|
||||||
}
|
|
||||||
return current ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLeaves(): TaxonomyLeaf[] {
|
|
||||||
const leaves: TaxonomyLeaf[] = [];
|
|
||||||
const walk = (node: TaxonomyNode): void => {
|
|
||||||
if (node.children.length === 0) {
|
|
||||||
leaves.push(node as TaxonomyLeaf);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const child of node.children) walk(child);
|
|
||||||
};
|
|
||||||
for (const root of loadTaxonomy().roots) walk(root);
|
|
||||||
return leaves;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getParentById(id: number): TaxonomyNode | null {
|
|
||||||
const visit = (node: TaxonomyNode): TaxonomyNode | null => {
|
|
||||||
for (const child of node.children) {
|
|
||||||
if (child.id === id) return node;
|
|
||||||
const deeper = visit(child);
|
|
||||||
if (deeper !== null) return deeper;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
for (const root of loadTaxonomy().roots) {
|
|
||||||
const result = visit(root);
|
|
||||||
if (result !== null) return result;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetTaxonomyCache(): void {
|
|
||||||
cached = null;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue