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): 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 { const plansByVersion = groupByVersionId(plans); const requirementVersionMap = new Map(); for (const requirement of requirements) { if (!requirement.versionId) continue; requirementVersionMap.set(requirement.id, requirement.versionId); } const devTasksByVersion = new Map(); 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 = {}; 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(items: T[]): Map { const map = new Map(); for (const item of items) { const group = map.get(item.versionId) ?? []; group.push(item); map.set(item.versionId, group); } return map; }