Files
ftb-project-management/apps/web/lib/requirement-scope.ts
2026-07-02 09:30:50 +08:00

98 lines
2.8 KiB
TypeScript

import type { Requirement, RequirementStatus } from './requirement';
export type RequirementScopeSelection =
| { type: 'all' }
| { type: 'product'; productId: string }
| { type: 'project'; projectId: string };
export interface RequirementScopeCount {
total: number;
byStatus: Partial<Record<RequirementStatus, number>>;
}
export interface RequirementProjectScopeNode {
id: string;
name: string;
count: RequirementScopeCount;
}
export interface RequirementProductScopeNode {
id: string;
name: string;
count: RequirementScopeCount;
projects: RequirementProjectScopeNode[];
}
export function filterRequirementsByScope(
requirements: Requirement[],
scope: RequirementScopeSelection,
): Requirement[] {
if (scope.type === 'product') {
return requirements.filter((requirement) => requirement.productId === scope.productId);
}
if (scope.type === 'project') {
return requirements.filter((requirement) => requirement.projectId === scope.projectId);
}
return requirements;
}
export function buildRequirementScopeTree(
overview: { id: string; name: string; projects: { id: string; name: string }[] }[],
requirements: Requirement[],
): RequirementProductScopeNode[] {
return overview.map((product) => {
const productRequirements = requirements.filter((requirement) => requirement.productId === product.id);
return {
id: product.id,
name: product.name,
count: countRequirements(productRequirements),
projects: product.projects.map((project) => {
const projectRequirements = requirements.filter((requirement) => requirement.projectId === project.id);
return {
id: project.id,
name: project.name,
count: countRequirements(projectRequirements),
};
}),
};
});
}
export function filterRequirementScopeTreeByKeyword(
tree: RequirementProductScopeNode[],
keyword: string,
): RequirementProductScopeNode[] {
const query = normalizeScopeKeyword(keyword);
if (!query) return tree;
const filtered: RequirementProductScopeNode[] = [];
for (const product of tree) {
if (normalizeScopeKeyword(product.name).includes(query)) {
filtered.push(product);
continue;
}
const projects = product.projects.filter((project) => normalizeScopeKeyword(project.name).includes(query));
if (projects.length > 0) {
filtered.push({ ...product, projects });
}
}
return filtered;
}
function countRequirements(requirements: Requirement[]): RequirementScopeCount {
const byStatus: Partial<Record<RequirementStatus, number>> = {};
for (const requirement of requirements) {
byStatus[requirement.status] = (byStatus[requirement.status] ?? 0) + 1;
}
return {
total: requirements.length,
byStatus,
};
}
function normalizeScopeKeyword(value: string): string {
return value.trim().toLowerCase();
}