diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 913f68f..bf09f4f 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -379,64 +379,105 @@ export default function VersionDetailPage() { {/* 阶段耗时 + 个人耗时排名 */} {(() => { - // 项目维度:阶段日历天数 - const devStarts = versionDevTasks.filter((t) => t.startDate).map((t) => t.startDate!); - const devEnds = versionDevTasks.filter((t) => t.completedAt).map((t) => t.completedAt!); - const devCalendarStart = devStarts.length > 0 ? devStarts.sort()[0] : null; - const devCalendarEnd = devEnds.length > 0 ? devEnds.sort().reverse()[0] : null; - const devCalendarDays = devCalendarStart && devCalendarEnd ? Math.max(1, Math.ceil((new Date(devCalendarEnd).getTime() - new Date(devCalendarStart).getTime()) / (1000 * 60 * 60 * 24)) + 1) : 0; + const versionPlans = plans.filter((p) => p.versionId === version.id); - const tcStarts = versionTCs.filter((c) => c.startedAt).map((c) => c.startedAt!); - const tcEnds = versionTCs.filter((c) => c.completedAt).map((c) => c.completedAt!); - const tcCalendarStart = tcStarts.length > 0 ? tcStarts.sort()[0] : null; - const tcCalendarEnd = tcEnds.length > 0 ? tcEnds.sort().reverse()[0] : null; - const tcCalendarDays = tcCalendarStart && tcCalendarEnd ? Math.max(1, Math.ceil((new Date(tcCalendarEnd).getTime() - new Date(tcCalendarStart).getTime()) / (1000 * 60 * 60 * 24)) + 1) : 0; + // 各阶段日历天数计算辅助函数 + const calcCalendar = (starts: string[], ends: string[]) => { + const sortedStarts = starts.filter(Boolean).sort(); + const sortedEnds = ends.filter(Boolean).sort().reverse(); + const start = sortedStarts[0] || null; + const end = sortedEnds[0] || null; + const days = start && end ? Math.max(1, Math.ceil((new Date(end).getTime() - new Date(start).getTime()) / (1000 * 60 * 60 * 24)) + 1) : 0; + return { start, end, days }; + }; - // 个人维度:每人耗时汇总(开发 + 测试) - const personalHours = new Map(); + // 调研阶段 + const researchPlans = versionPlans.filter((p) => p.type === 'research'); + const researchCal = calcCalendar(researchPlans.map((p) => p.startTime), researchPlans.filter((p) => p.status === 'completed').map((p) => p.endTime)); + + // 产品方案阶段 + const productPlans = versionPlans.filter((p) => p.type === 'product'); + const productCal = calcCalendar(productPlans.map((p) => p.startTime), productPlans.filter((p) => p.status === 'completed').map((p) => p.endTime)); + + // UI设计阶段 + const uiPlans = versionPlans.filter((p) => p.type === 'ui'); + const uiCal = calcCalendar(uiPlans.map((p) => p.startTime), uiPlans.filter((p) => p.status === 'completed').map((p) => p.endTime)); + + // 开发阶段 + const devCal = calcCalendar( + versionDevTasks.filter((t) => t.startDate).map((t) => t.startDate!), + versionDevTasks.filter((t) => t.completedAt).map((t) => t.completedAt!), + ); + + // 测试阶段 + const tcCal = calcCalendar( + versionTCs.filter((c) => c.startedAt).map((c) => c.startedAt!), + versionTCs.filter((c) => c.completedAt).map((c) => c.completedAt!), + ); + + const stages = [ + { label: '调研', ...researchCal, color: 'bg-orange-400' }, + { label: '产品方案', ...productCal, color: 'bg-pink-400' }, + { label: 'UI设计', ...uiCal, color: 'bg-indigo-400' }, + { label: '开发', ...devCal, color: 'bg-blue-400' }, + { label: '测试', ...tcCal, color: 'bg-purple-400' }, + ]; + const maxDays = Math.max(...stages.map((s) => s.days), 1); + + // 个人维度:每人耗时汇总 + const personalHours = new Map(); + const addHours = (name: string, key: 'research' | 'product' | 'ui' | 'dev' | 'test', hours: number) => { + const prev = personalHours.get(name) || { research: 0, product: 0, ui: 0, dev: 0, test: 0 }; + prev[key] += hours; + personalHours.set(name, prev); + }; + + // 调研/产品/UI 用 startTime→endTime 计算 + versionPlans.forEach((p) => { + if (!p.owner || !p.startTime) return; + const end = p.status === 'completed' ? p.endTime : undefined; + const hours = calcActualHoursByDates(p.startTime.slice(0, 10), end?.slice(0, 10)); + addHours(p.owner, p.type === 'research' ? 'research' : p.type === 'product' ? 'product' : 'ui', hours); + }); versionDevTasks.forEach((t) => { if (!t.startDate || !t.assigneeId) return; - const hours = calcActualHoursByDates(t.startDate, t.completedAt); - const prev = personalHours.get(t.assigneeId) || { dev: 0, test: 0 }; - prev.dev += hours; - personalHours.set(t.assigneeId, prev); + addHours(t.assigneeId, 'dev', calcActualHoursByDates(t.startDate, t.completedAt)); }); versionTCs.forEach((c) => { if (!c.startedAt || !c.assigneeId) return; - const hours = calcActualHoursByDates(c.startedAt, c.completedAt); - const prev = personalHours.get(c.assigneeId) || { dev: 0, test: 0 }; - prev.test += hours; - personalHours.set(c.assigneeId, prev); + addHours(c.assigneeId, 'test', calcActualHoursByDates(c.startedAt, c.completedAt)); }); + const personalRanking = Array.from(personalHours.entries()) - .map(([name, h]) => ({ name, dev: h.dev, test: h.test, total: h.dev + h.test })) + .map(([name, h]) => ({ name, ...h, total: h.research + h.product + h.ui + h.dev + h.test })) .sort((a, b) => b.total - a.total); const maxHours = personalRanking[0]?.total || 1; + const totalHours = personalRanking.reduce((s, p) => s + p.total, 0); return (
{/* 阶段日历耗时 */}
阶段耗时(项目维度)
-
-
-
- 开发阶段 - {devCalendarDays > 0 ? `${devCalendarDays}天` : '-'} +
+ {stages.map((s) => ( +
+
+ {s.label} + {s.days > 0 ? `${s.days}天` : '-'} +
+
+
+
+
+ {s.start && {s.start.slice(5)}{s.end ? ` → ${s.end.slice(5)}` : ' →'}} +
- {devCalendarStart && {devCalendarStart} → {devCalendarEnd || '进行中'}} -
-
-
- 测试阶段 - {tcCalendarDays > 0 ? `${tcCalendarDays}天` : '-'} -
- {tcCalendarStart && {tcCalendarStart} → {tcCalendarEnd || '进行中'}} -
+ ))}
总人天投入 - {personalRanking.reduce((s, p) => s + p.total, 0)}h({Math.round(personalRanking.reduce((s, p) => s + p.total, 0) / 8)}人天) + {totalHours}h({Math.round(totalHours / 8)}人天)
@@ -456,13 +497,19 @@ export default function VersionDetailPage() { {item.name} {item.total}h
-
+
+ {item.research > 0 &&
} + {item.product > 0 &&
} + {item.ui > 0 &&
} {item.dev > 0 &&
} {item.test > 0 &&
}
))} -
+
+ 调研 + 产品 + UI 开发 测试