Files
ftb-project-management/apps/web/lib/version-plan.ts
Script Generator da1f98968b fix: 调研/产品/UI阶段耗时改为精确时间戳计算
- VersionPlan 新增 actualStartAt 字段
- updatePlan 流转到 in_progress 时自动记录 actualStartAt
- updatePlan 流转到 completed 时自动记录 completedAt(如未设)
- 概览阶段耗时改用 actualStartAt/completedAt 计算(不再用计划排期)
- 个人耗时汇总同步使用实际时间戳

所有阶段(调研/产品/UI/开发/测试)现在统一用精确时间戳算耗时。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 16:24:46 +08:00

96 lines
3.0 KiB
TypeScript

export type PlanTaskStatus = 'pending' | 'in_progress' | 'completed';
export interface PlanTask {
id: string;
title: string;
status: PlanTaskStatus;
}
export interface VersionPlan {
id: string;
versionId: string;
type: 'research' | 'product' | 'ui';
title: string;
owner: string;
startTime: string;
endTime: string;
status: 'pending' | 'in_progress' | 'completed';
tasks?: PlanTask[];
completedRequirementIds?: string[];
linkedRequirementIds?: string[];
resultType?: 'link' | 'file';
resultUrl?: string;
resultFileName?: string;
resultFileData?: string;
remark?: string;
overdueReason?: string;
actualStartAt?: string;
createdAt: string;
completedAt?: string;
addedBy: string;
}
export type PlanType = VersionPlan['type'];
export function calcPlanProgress(tasks?: PlanTask[]): number {
if (!tasks || tasks.length === 0) return 0;
const completed = tasks.filter((t) => t.status === 'completed').length;
return Math.round((completed / tasks.length) * 100);
}
export function calcLinkedReqProgress(linkedIds?: string[], completedIds?: string[]): number {
if (!linkedIds || linkedIds.length === 0) return 0;
const done = (completedIds || []).filter((id) => linkedIds.includes(id)).length;
return Math.round((done / linkedIds.length) * 100);
}
export function calcPlanDuration(start: string, end: string): { days: number; hours: number } {
const s = new Date(start).getTime();
const e = new Date(end).getTime();
if (isNaN(s) || isNaN(e) || e < s) return { days: 0, hours: 0 };
const diff = Math.ceil((e - s) / (1000 * 60 * 60 * 24));
const totalDays = Math.max(1, diff); // 当天开始当天结束至少1天
return { days: totalDays, hours: totalDays * 8 };
}
export function formatDuration(days: number, _hours: number): string {
if (days === 0) return '0天';
return `${days}`;
}
export function calcTotalDuration(plans: VersionPlan[]): string {
const today = new Date().toISOString().slice(0, 10);
const activePlans = plans.filter((p) => p.status !== 'pending' || p.startTime <= today);
if (activePlans.length === 0) return '0天';
const intervals = activePlans.map((p) => {
const start = new Date(p.startTime).getTime();
const end = p.completedAt
? new Date(p.completedAt).getTime()
: p.startTime <= today
? new Date(today).getTime()
: new Date(p.startTime).getTime();
return { start, end };
}).filter((i) => i.end > i.start).sort((a, b) => a.start - b.start);
if (intervals.length === 0) return '0天';
let totalMs = 0;
let currentStart = intervals[0].start;
let currentEnd = intervals[0].end;
for (let i = 1; i < intervals.length; i++) {
if (intervals[i].start <= currentEnd) {
currentEnd = Math.max(currentEnd, intervals[i].end);
} else {
totalMs += currentEnd - currentStart;
currentStart = intervals[i].start;
currentEnd = intervals[i].end;
}
}
totalMs += currentEnd - currentStart;
const totalDays = Math.ceil(totalMs / (1000 * 60 * 60 * 24));
return `${totalDays}`;
}