Files
ftb-project-management/apps/web/lib/version-progress.ts
2026-07-03 09:43:36 +08:00

141 lines
4.9 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) => isDevTaskInVersion(t, versionId, vReqIds));
const vTestCases = testCases.filter((c) => c.versionId === versionId);
return calcScopedVersionProgress(vPlans, vDevTasks, vTestCases);
}
export function calcScopedVersionProgress(
vPlans: VersionPlan[],
vDevTasks: DevTask[],
vTestCases: TestCase[],
): number {
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;
}
function isDevTaskInVersion(task: DevTask, versionId: string, requirementIds: Set<string>): boolean {
if (task.versionId) return task.versionId === versionId;
return requirementIds.has(task.requirementId);
}
/**
* 批量为多个版本算进度,返回 versionId → 进度 (0-100) 的 Map
*/
export function buildVersionProgressMap(
versions: VersionWithContext[],
plans: VersionPlan[],
requirements: Requirement[],
devTasks: DevTask[],
testCases: TestCase[],
): Record<string, number> {
const plansByVersion = groupByVersionId(plans);
const requirementVersionMap = new Map<string, string>();
for (const requirement of requirements) {
if (!requirement.versionId) continue;
requirementVersionMap.set(requirement.id, requirement.versionId);
}
const devTasksByVersion = new Map<string, DevTask[]>();
for (const task of devTasks) {
const versionId = task.versionId || requirementVersionMap.get(task.requirementId);
if (!versionId) continue;
const items = devTasksByVersion.get(versionId) ?? [];
items.push(task);
devTasksByVersion.set(versionId, items);
}
const testCasesByVersion = groupByVersionId(testCases);
const map: Record<string, number> = {};
for (const v of versions) {
map[v.id] = calcScopedVersionProgress(
plansByVersion.get(v.id) ?? [],
devTasksByVersion.get(v.id) ?? [],
testCasesByVersion.get(v.id) ?? [],
);
}
return map;
}
function groupByVersionId<T extends { versionId: string }>(items: T[]): Map<string, T[]> {
const map = new Map<string, T[]>();
for (const item of items) {
const group = map.get(item.versionId) ?? [];
group.push(item);
map.set(item.versionId, group);
}
return map;
}