Files
ftb-project-management/apps/web/lib/version-progress.ts
2026-06-30 11:00:07 +08:00

97 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getRequirementCoverageSummary } from './version-plan';
import type { VersionPlan } from './version-plan';
import type { Requirement } from './requirement';
import type { DevTask } from './dev-task';
import type { TestCase } from './test-case';
import type { VersionWithContext } from './derive';
import { STATUS_PROGRESS, getEstimateHours } from './dev-task';
/**
* 计算单个版本的整体进度0-100
* 算法:调研/产品/UI 计划进度 + 开发任务加权进度 + 测试执行率,求平均
*/
export function calcVersionProgress(
versionId: string,
plans: VersionPlan[],
requirements: Requirement[],
devTasks: DevTask[],
testCases: TestCase[],
): number {
const vPlans = plans.filter((p) => p.versionId === versionId);
const vReqs = requirements.filter((r) => r.versionId === versionId);
const vReqIds = new Set(vReqs.map((r) => r.id));
const vDevTasks = devTasks.filter((t) => vReqIds.has(t.requirementId));
const vTestCases = testCases.filter((c) => c.versionId === versionId);
const segments: number[] = [];
const researchPlans = vPlans.filter((p) => p.type === 'research');
if (researchPlans.length > 0) {
const totals = researchPlans.reduce((acc, p) => {
const tasks = p.tasks || [];
acc.total += tasks.length;
acc.done += tasks.filter((t) => t.status === 'completed').length;
return acc;
}, { total: 0, done: 0 });
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
}
const productPlans = vPlans.filter((p) => p.type === 'product');
if (productPlans.length > 0) {
const totals = productPlans.reduce((acc, p) => {
const summary = getRequirementCoverageSummary(p);
acc.total += summary.total;
acc.done += summary.completed;
return acc;
}, { total: 0, done: 0 });
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
}
const uiPlans = vPlans.filter((p) => p.type === 'ui');
if (uiPlans.length > 0) {
const totals = uiPlans.reduce((acc, p) => {
const summary = getRequirementCoverageSummary(p);
acc.total += summary.total;
acc.done += summary.completed;
return acc;
}, { total: 0, done: 0 });
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
}
if (vDevTasks.length > 0) {
const totalEstimate = vDevTasks.reduce((sum, t) => sum + getEstimateHours(t), 0);
let devProgress: number;
if (totalEstimate === 0) {
devProgress = vDevTasks.reduce((sum, t) => sum + STATUS_PROGRESS[t.status], 0) / vDevTasks.length;
} else {
const weighted = vDevTasks.reduce((sum, t) => sum + getEstimateHours(t) * STATUS_PROGRESS[t.status], 0);
devProgress = weighted / totalEstimate;
}
segments.push(devProgress);
}
if (vTestCases.length > 0) {
const executed = vTestCases.filter((c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked').length;
segments.push((executed / vTestCases.length) * 100);
}
return segments.length > 0 ? Math.round(segments.reduce((s, x) => s + x, 0) / segments.length) : 0;
}
/**
* 批量为多个版本算进度,返回 versionId → 进度 (0-100) 的 Map
*/
export function buildVersionProgressMap(
versions: VersionWithContext[],
plans: VersionPlan[],
requirements: Requirement[],
devTasks: DevTask[],
testCases: TestCase[],
): Record<string, number> {
const map: Record<string, number> = {};
for (const v of versions) {
map[v.id] = calcVersionProgress(v.id, plans, requirements, devTasks, testCases);
}
return map;
}