feat(版本): 优化概览与只读状态
关键改动: - 增加需求排序和版本只读状态规则及测试 - 完善版本概览阶段耗时、项目页和工作台展示 - 优化小宝预警请求节流、建议状态和风险过滤 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import type { Stage } from './stage';
|
||||
import { STAGES, type Stage } from './stage';
|
||||
import type { VersionPlan } from './version-plan';
|
||||
import type { DevTask } from './dev-task';
|
||||
import type { TestCase } from './test-case';
|
||||
import type { Bug, BugSeverity } from './bug';
|
||||
import type { OvertimeRecord } from './overtime';
|
||||
import type { VersionStatus } from './version-status';
|
||||
import { getActualHours as getDevTaskActualHours } from './dev-task';
|
||||
import { getTestCaseActualHours } from './test-case';
|
||||
import { getBugActualHours } from './bug';
|
||||
import { calcActualElapsedHours } from './work-hours';
|
||||
import { formatDateTime } from './format';
|
||||
|
||||
export interface StageEffortMetric {
|
||||
actualHours: number;
|
||||
@@ -16,6 +18,13 @@ export interface StageEffortMetric {
|
||||
showEstimates?: boolean;
|
||||
}
|
||||
|
||||
export interface StageProgressState {
|
||||
percent: number;
|
||||
status: 'idle' | 'active' | 'done';
|
||||
}
|
||||
|
||||
export type StageProgressWithEffort = StageProgressState & StageEffortMetric;
|
||||
|
||||
export interface PersonalEffortItem {
|
||||
name: string;
|
||||
actualHours: number;
|
||||
@@ -37,6 +46,16 @@ export interface VersionOverviewEffortTotals {
|
||||
overtimeHours: number;
|
||||
}
|
||||
|
||||
export interface VersionTimelineSummary {
|
||||
actualStartIso: string | null;
|
||||
expectedReleaseIso: string | null;
|
||||
actualReleaseIso: string | null;
|
||||
actualEndIso: string | null;
|
||||
isTerminalVersion: boolean;
|
||||
actualHours: number;
|
||||
overdueDays: number;
|
||||
}
|
||||
|
||||
function roundHalf(hours: number): number {
|
||||
return Math.round(hours * 2) / 2;
|
||||
}
|
||||
@@ -117,6 +136,100 @@ export function calcStageEffortMetrics(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeStageProgressWithEffort(
|
||||
progress: Partial<Record<Stage, StageProgressState>>,
|
||||
effortMetrics: Record<Stage, StageEffortMetric>,
|
||||
): Record<Stage, StageProgressWithEffort> {
|
||||
return STAGES.reduce((acc, stage) => {
|
||||
const state = progress[stage.key];
|
||||
acc[stage.key] = {
|
||||
percent: state?.percent ?? 0,
|
||||
status: state?.status ?? 'idle',
|
||||
...effortMetrics[stage.key],
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<Stage, StageProgressWithEffort>);
|
||||
}
|
||||
|
||||
export function getVersionCardDefaultExpanded(status: VersionStatus): boolean {
|
||||
return status === 'developing';
|
||||
}
|
||||
|
||||
export function formatVersionOverviewDateTime(value?: string | null): string {
|
||||
if (!value) return '-';
|
||||
return value.includes('T') ? formatDateTime(value) : value;
|
||||
}
|
||||
|
||||
export function buildVersionTimelineSummary(input: {
|
||||
status: VersionStatus;
|
||||
startDate?: string | null;
|
||||
expectedReleaseDate?: string | null;
|
||||
releaseDate?: string | null;
|
||||
plans: VersionPlan[];
|
||||
devTasks: DevTask[];
|
||||
testCases: TestCase[];
|
||||
bugs: Bug[];
|
||||
now?: Date;
|
||||
}): VersionTimelineSummary {
|
||||
const now = input.now ?? new Date();
|
||||
const startDates: string[] = [];
|
||||
|
||||
input.plans.forEach((plan) => {
|
||||
if (plan.actualStartAt) {
|
||||
startDates.push(plan.actualStartAt);
|
||||
} else if (plan.status === 'pending' && plan.startTime && new Date(plan.startTime) <= now) {
|
||||
startDates.push(plan.startTime);
|
||||
}
|
||||
});
|
||||
input.devTasks.forEach((task) => {
|
||||
if (task.actualStartAt) startDates.push(task.actualStartAt);
|
||||
});
|
||||
input.testCases.forEach((testCase) => {
|
||||
if (testCase.startedAt) startDates.push(testCase.startedAt);
|
||||
});
|
||||
|
||||
const actualStartIso = startDates.length > 0 ? startDates.sort()[0] : (input.startDate ?? null);
|
||||
|
||||
const endDates: string[] = [];
|
||||
input.plans.forEach((plan) => {
|
||||
if (plan.completedAt) endDates.push(plan.completedAt);
|
||||
});
|
||||
input.devTasks.forEach((task) => {
|
||||
if (task.actualEndAt) endDates.push(task.actualEndAt);
|
||||
});
|
||||
input.testCases.forEach((testCase) => {
|
||||
if (testCase.completedAt) endDates.push(testCase.completedAt);
|
||||
});
|
||||
input.bugs.forEach((bug) => {
|
||||
if (bug.closedAt) endDates.push(bug.closedAt);
|
||||
else if (bug.resolvedAt) endDates.push(bug.resolvedAt);
|
||||
else if ((bug.status === 'closed' || bug.status === 'rejected') && bug.updatedAt) endDates.push(bug.updatedAt);
|
||||
});
|
||||
|
||||
const actualEndIso = endDates.length > 0 ? endDates.sort().reverse()[0] : null;
|
||||
const isTerminalVersion = input.status === 'released' || input.status === 'closed';
|
||||
const actualHours = calcActualElapsedHours(actualStartIso, isTerminalVersion ? actualEndIso : now.toISOString());
|
||||
|
||||
let overdueDays = 0;
|
||||
if (input.expectedReleaseDate && input.releaseDate) {
|
||||
const endDate = new Date(input.releaseDate);
|
||||
const deadlineDate = new Date(input.expectedReleaseDate);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
deadlineDate.setHours(0, 0, 0, 0);
|
||||
overdueDays = Math.floor((endDate.getTime() - deadlineDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
return {
|
||||
actualStartIso,
|
||||
expectedReleaseIso: input.expectedReleaseDate ?? null,
|
||||
actualReleaseIso: input.releaseDate ?? null,
|
||||
actualEndIso,
|
||||
isTerminalVersion,
|
||||
actualHours,
|
||||
overdueDays,
|
||||
};
|
||||
}
|
||||
|
||||
export function calcVersionOverviewEffortTotals(input: {
|
||||
plans: VersionPlan[];
|
||||
devTasks: DevTask[];
|
||||
|
||||
Reference in New Issue
Block a user