diff --git a/backend/src/routes/overview.ts b/backend/src/routes/overview.ts index e47951e..009ab0e 100644 --- a/backend/src/routes/overview.ts +++ b/backend/src/routes/overview.ts @@ -6,6 +6,19 @@ import dayjs from 'dayjs'; export const overviewRoutes = new Hono(); +async function getRecentCommits() { + const recent = await db.select().from(gitCommits).orderBy(desc(gitCommits.committedAt)).limit(15); + const allUsers = await db.select().from(users); + const userMap = new Map(allUsers.map(u => [u.id, u.displayName])); + return recent.map(c => ({ + sha: c.sha?.slice(0, 7) || '', + message: (c.message || '').split('\n')[0].slice(0, 60), + authorName: c.userId ? userMap.get(c.userId) || c.authorName : c.authorName, + repoName: c.repoName, + committedAt: c.committedAt instanceof Date ? c.committedAt.toISOString() : c.committedAt, + })); +} + overviewRoutes.get('/overview', async (c) => { const period = c.req.query('period'); const projectIds = c.req.query('projectIds')?.split(',').filter(Boolean) || []; @@ -140,33 +153,92 @@ overviewRoutes.get('/overview', async (c) => { }); } - // 6. PR Merge Time (last 12 weeks) - const mergedPRs = await db.select().from(gitPRs) - .where(gte(gitPRs.mergedAt, twelveWeeksAgo)); + // 6. 本周待完成 KR(截止日期在本周范围内,且未完成的) + const weekStart = dayjs().startOf('week'); + const weekEnd = dayjs().endOf('week'); - const prWeekMap: Record = {}; - for (let i = 0; i < 12; i++) { - const ws = dayjs().subtract(11 - i, 'week').startOf('week').format('YYYY-MM-DD'); - prWeekMap[ws] = { totalHours: 0, count: 0 }; + const allKRsRaw = await db.select().from(keyResults); + const allObjsMap = new Map((await db.select().from(objectives)).map(o => [o.id, o])); + + const thisWeekKRs = allKRsRaw.filter(kr => { + if (!kr.endDate) return false; + const end = dayjs(kr.endDate); + // 严格筛选:截止日期在本周一到本周日之间 + return (end.isAfter(weekStart) || end.isSame(weekStart, 'day')) && + (end.isBefore(weekEnd) || end.isSame(weekEnd, 'day')); + }).sort((a, b) => { + // 未完成的排前面,已完成的排后面;同状态按截止日期排 + const aDone = (a.currentValue || 0) >= (a.targetValue || 100) ? 1 : 0; + const bDone = (b.currentValue || 0) >= (b.targetValue || 100) ? 1 : 0; + if (aDone !== bDone) return aDone - bDone; + return dayjs(a.endDate).valueOf() - dayjs(b.endDate).valueOf(); + }); + + const urgentKRs = []; + for (const kr of thisWeekKRs.slice(0, 20)) { + const obj = allObjsMap.get(kr.objectiveId); + const owner = obj?.ownerId + ? await db.query.users.findFirst({ where: eq(users.id, obj.ownerId) }) + : null; + const proj = obj?.projectId + ? await db.query.projects.findFirst({ where: eq(projects.id, obj.projectId) }) + : null; + + const endDate = kr.endDate || ''; + const isOverdue = dayjs(endDate).isBefore(dayjs().startOf('day')); + const daysLeft = dayjs(endDate).startOf('day').diff(dayjs().startOf('day'), 'day'); + + const isCompleted = (kr.currentValue || 0) >= (kr.targetValue || 100); + + urgentKRs.push({ + id: kr.id, + title: kr.title, + progress: kr.currentValue || 0, + endDate, + isOverdue: isOverdue && !isCompleted, + isCompleted, + daysLeft, + objectiveTitle: obj?.title || '', + ownerName: owner?.displayName || '未指定', + projectName: proj?.name || '', + projectIdentifier: proj?.identifier || '', + }); } - for (const pr of mergedPRs) { - if (pr.mergedAt && pr.mergeTimeHours !== null && pr.state === 'merged') { - const ws = dayjs(pr.mergedAt).startOf('week').format('YYYY-MM-DD'); - if (prWeekMap[ws]) { - prWeekMap[ws].totalHours += pr.mergeTimeHours || 0; - prWeekMap[ws].count++; - } - } + // 7. 历史逾期未完成(截止日期在本周之前且未完成的) + const overdueKRs = allKRsRaw.filter(kr => { + if (!kr.endDate) return false; + if ((kr.currentValue || 0) >= (kr.targetValue || 100)) return false; + return dayjs(kr.endDate).isBefore(weekStart); + }).sort((a, b) => dayjs(a.endDate!).valueOf() - dayjs(b.endDate!).valueOf()); + + const overdueList = []; + for (const kr of overdueKRs.slice(0, 20)) { + const obj = allObjsMap.get(kr.objectiveId); + const owner = obj?.ownerId + ? await db.query.users.findFirst({ where: eq(users.id, obj.ownerId) }) + : null; + const proj = obj?.projectId + ? await db.query.projects.findFirst({ where: eq(projects.id, obj.projectId) }) + : null; + const daysOverdue = dayjs().startOf('day').diff(dayjs(kr.endDate!).startOf('day'), 'day'); + overdueList.push({ + id: kr.id, + title: kr.title, + progress: kr.currentValue || 0, + endDate: kr.endDate, + daysOverdue, + ownerName: owner?.displayName || '未指定', + projectName: proj?.name || '', + projectIdentifier: proj?.identifier || '', + }); } - const prMergeTime = { - weeks: Object.entries(prWeekMap).map(([weekStart, data]) => ({ - weekStart, - avgHours: data.count > 0 ? Math.round(data.totalHours / data.count * 10) / 10 : 0, - prCount: data.count, - })), - }; + const weeklyTotal = urgentKRs.length; + const weeklyCompleted = urgentKRs.filter(kr => kr.isCompleted).length; + const weeklyAvgProgress = weeklyTotal > 0 + ? Math.round(urgentKRs.reduce((s, kr) => s + kr.progress, 0) / weeklyTotal) + : 0; return c.json({ code: 0, @@ -176,7 +248,10 @@ overviewRoutes.get('/overview', async (c) => { projectProgress, weeklyCodeActivity, okrProgress, - prMergeTime, + urgentKRs, + weeklyKRStats: { total: weeklyTotal, completed: weeklyCompleted, avgProgress: weeklyAvgProgress }, + overdueKRs: overdueList, + recentCommits: await getRecentCommits(), }, message: 'success', }); diff --git a/backend/src/services/okr.ts b/backend/src/services/okr.ts index 1422a9c..f3bcf0c 100644 --- a/backend/src/services/okr.ts +++ b/backend/src/services/okr.ts @@ -120,9 +120,37 @@ export async function createKeyResult(objectiveId: string, data: { createdAt: now, updatedAt: now, }); + + // 自动更新目标的时间范围(取所有任务的最早开始 ~ 最晚截止) + await recalcObjectiveDates(objectiveId); + return { id }; } +/** + * 根据所有 KR 的日期自动更新 Objective 的 startDate / endDate / period + */ +async function recalcObjectiveDates(objectiveId: string) { + const krs = await db.select().from(keyResults) + .where(eq(keyResults.objectiveId, objectiveId)); + + const starts = krs.map(kr => kr.startDate).filter(Boolean) as string[]; + const ends = krs.map(kr => kr.endDate).filter(Boolean) as string[]; + + if (starts.length === 0 && ends.length === 0) return; + + const earliest = starts.length > 0 ? starts.sort()[0] : null; + const latest = ends.length > 0 ? ends.sort().reverse()[0] : null; + const period = latest ? dateToPeriod(latest) : undefined; + + const updateData: Record = { updatedAt: new Date() }; + if (earliest) updateData.startDate = earliest; + if (latest) updateData.endDate = latest; + if (period) updateData.period = period; + + await db.update(objectives).set(updateData).where(eq(objectives.id, objectiveId)); +} + export async function updateKeyResultProgress(krId: string, currentValue: number) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, krId), @@ -194,4 +222,7 @@ export async function deleteKeyResult(id: string) { .set({ progress: objectiveProgress, updatedAt: new Date() }) .where(eq(objectives.id, kr.objectiveId)); } + + // 重算目标时间范围 + await recalcObjectiveDates(kr.objectiveId); } diff --git a/frontend/src/components/shared/DataCard.vue b/frontend/src/components/shared/DataCard.vue index e61c631..3977f7b 100644 --- a/frontend/src/components/shared/DataCard.vue +++ b/frontend/src/components/shared/DataCard.vue @@ -1,6 +1,6 @@