diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 39c1c5f..913f68f 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -25,7 +25,7 @@ import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; -import { calcGroupProgress as calcDevTaskProgress } from '@/lib/dev-task'; +import { calcGroupProgress as calcDevTaskProgress, calcActualHoursByDates } from '@/lib/dev-task'; const PRIORITY_STYLE: Record = { P0: 'bg-red-500/10 text-red-600', @@ -377,6 +377,102 @@ 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 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 personalHours = new Map(); + 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); + }); + 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); + }); + const personalRanking = Array.from(personalHours.entries()) + .map(([name, h]) => ({ name, dev: h.dev, test: h.test, total: h.dev + h.test })) + .sort((a, b) => b.total - a.total); + const maxHours = personalRanking[0]?.total || 1; + + return ( +
+ {/* 阶段日历耗时 */} +
+
阶段耗时(项目维度)
+
+
+
+ 开发阶段 + {devCalendarDays > 0 ? `${devCalendarDays}天` : '-'} +
+ {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)}人天) +
+
+
+
+ + {/* 个人耗时排名 */} +
+
个人耗时排名
+ {personalRanking.length === 0 ? ( + 暂无数据 + ) : ( +
+ {personalRanking.slice(0, 8).map((item, i) => ( +
+
+ {i + 1} + {item.name} + {item.total}h +
+
+ {item.dev > 0 &&
} + {item.test > 0 &&
} +
+
+ ))} +
+ 开发 + 测试 +
+
+ )} +
+
+ ); + })()} + {/* 双栏:日期信息 + 相关链接 */}