141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
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;
|
||
}
|