import { eq } from 'drizzle-orm'; import { v4 as uuid } from 'uuid'; import { db } from '../db/index'; import { objectives, keyResults, users, projects, krLogs } from '../db/schema'; import { desc } from 'drizzle-orm'; import { AppError } from '../middleware/error-handler'; export async function getOKRByPeriod(period?: string) { const allObjectives = period ? await db.select().from(objectives).where(eq(objectives.period, period)) : await db.select().from(objectives); const result = []; for (const obj of allObjectives) { const krs = await db.select().from(keyResults) .where(eq(keyResults.objectiveId, obj.id)); const owner = obj.ownerId ? await db.query.users.findFirst({ where: eq(users.id, obj.ownerId) }) : null; const project = obj.projectId ? await db.query.projects.findFirst({ where: eq(projects.id, obj.projectId) }) : null; result.push({ id: obj.id, title: obj.title, ownerName: owner?.displayName || '未指定', projectName: project?.name || '未关联项目', period: obj.period, startDate: obj.startDate || null, endDate: obj.endDate || null, progress: obj.progress || 0, keyResults: await Promise.all(krs.map(async kr => { // 查是否有延期记录 const postponeLog = await db.select().from(krLogs) .where(eq(krLogs.krId, kr.id)) .orderBy(desc(krLogs.createdAt)) .limit(5); const wasPostponed = postponeLog.some(l => l.action === 'postponed'); const lastPostponeReason = postponeLog.find(l => l.action === 'postponed')?.detail || null; return { id: kr.id, title: kr.title, targetValue: kr.targetValue, currentValue: kr.currentValue || 0, unit: kr.unit || '', weight: kr.weight || 1, status: kr.status || 'active', wasPostponed, lastPostponeReason, startDate: kr.startDate || null, endDate: kr.endDate || null, progress: kr.targetValue > 0 ? Math.round(((kr.currentValue || 0) / kr.targetValue) * 100) : 0, }; })), }); } return { objectives: result }; } /** * 从日期自动推算所属季度,例如 2026-04-15 → "2026-Q2" */ function dateToPeriod(dateStr: string): string { const d = new Date(dateStr); const year = d.getFullYear(); const q = Math.ceil((d.getMonth() + 1) / 3); return `${year}-Q${q}`; } export async function createObjective(data: { title: string; ownerId: string; projectId: string; startDate: string; endDate: string; period?: string; }) { const id = uuid(); const now = new Date(); const period = data.period || dateToPeriod(data.endDate); await db.insert(objectives).values({ id, title: data.title, ownerId: data.ownerId, projectId: data.projectId, period, startDate: data.startDate, endDate: data.endDate, progress: 0, createdAt: now, updatedAt: now, }); return { id }; } export async function createKeyResult(objectiveId: string, data: { title: string; targetValue: number; unit: string; weight: number; startDate?: string; endDate?: string; linkedPlaneCycleId?: string; linkedPlaneModuleId?: string; }) { const objective = await db.query.objectives.findFirst({ where: eq(objectives.id, objectiveId), }); if (!objective) { throw new AppError(40403, 'Objective not found', 404); } const id = uuid(); const now = new Date(); await db.insert(keyResults).values({ id, objectiveId, title: data.title, targetValue: data.targetValue, currentValue: 0, unit: data.unit, weight: data.weight, startDate: data.startDate || null, endDate: data.endDate || null, linkedPlaneCycleId: data.linkedPlaneCycleId || null, linkedPlaneModuleId: data.linkedPlaneModuleId || null, 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), }); if (!kr) { throw new AppError(40404, 'Key result not found', 404); } await db.update(keyResults) .set({ currentValue, updatedAt: new Date() }) .where(eq(keyResults.id, krId)); // Recalculate objective progress (weighted average) const allKRs = await db.select().from(keyResults) .where(eq(keyResults.objectiveId, kr.objectiveId)); const totalWeight = allKRs.reduce((sum, k) => sum + (k.weight || 1), 0); const weightedProgress = allKRs.reduce((sum, k) => { const val = k.id === krId ? currentValue : (k.currentValue || 0); const progress = k.targetValue > 0 ? (val / k.targetValue) * 100 : 0; return sum + progress * (k.weight || 1); }, 0); const objectiveProgress = totalWeight > 0 ? Math.round(weightedProgress / totalWeight) : 0; await db.update(objectives) .set({ progress: objectiveProgress, updatedAt: new Date() }) .where(eq(objectives.id, kr.objectiveId)); return { id: krId, progress: kr.targetValue > 0 ? Math.round((currentValue / kr.targetValue) * 100) : 0, objectiveProgress, }; } export async function deleteObjective(id: string) { // Delete all KRs first await db.delete(keyResults).where(eq(keyResults.objectiveId, id)); await db.delete(objectives).where(eq(objectives.id, id)); } export async function deleteKeyResult(id: string) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, id), }); if (!kr) { throw new AppError(40404, 'Key result not found', 404); } await db.delete(keyResults).where(eq(keyResults.id, id)); // Recalculate objective progress const remainingKRs = await db.select().from(keyResults) .where(eq(keyResults.objectiveId, kr.objectiveId)); if (remainingKRs.length === 0) { await db.update(objectives) .set({ progress: 0, updatedAt: new Date() }) .where(eq(objectives.id, kr.objectiveId)); } else { const totalWeight = remainingKRs.reduce((sum, k) => sum + (k.weight || 1), 0); const weightedProgress = remainingKRs.reduce((sum, k) => { const progress = k.targetValue > 0 ? ((k.currentValue || 0) / k.targetValue) * 100 : 0; return sum + progress * (k.weight || 1); }, 0); const objectiveProgress = totalWeight > 0 ? Math.round(weightedProgress / totalWeight) : 0; await db.update(objectives) .set({ progress: objectiveProgress, updatedAt: new Date() }) .where(eq(objectives.id, kr.objectiveId)); } // 重算目标时间范围 await recalcObjectiveDates(kr.objectiveId); } // ── 操作日志辅助 ── async function addKRLog(krId: string, action: string, detail: string | null, operatorId: string, operatorName: string) { await db.insert(krLogs).values({ id: uuid(), krId, action, detail, operatorId, operatorName, createdAt: new Date(), }); } async function recalcObjectiveProgress(objectiveId: string) { const allKRs = await db.select().from(keyResults) .where(eq(keyResults.objectiveId, objectiveId)); // 只算 active 和已完成的 KR,暂停和取消的不计入 const activeKRs = allKRs.filter(kr => kr.status !== 'paused' && kr.status !== 'cancelled'); const totalWeight = activeKRs.reduce((sum, k) => sum + (k.weight || 1), 0); const weightedProgress = activeKRs.reduce((sum, k) => { const progress = k.targetValue > 0 ? ((k.currentValue || 0) / k.targetValue) * 100 : 0; return sum + progress * (k.weight || 1); }, 0); const objProgress = totalWeight > 0 ? Math.round(weightedProgress / totalWeight) : 0; await db.update(objectives).set({ progress: objProgress, updatedAt: new Date() }) .where(eq(objectives.id, objectiveId)); } // ── 延期 ── export async function postponeKR(krId: string, newEndDate: string, reason: string, operatorId: string, operatorName: string) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, krId) }); if (!kr) throw new AppError(40404, 'Key result not found', 404); const oldEndDate = kr.endDate || '未设置'; await db.update(keyResults).set({ endDate: newEndDate, updatedAt: new Date() }) .where(eq(keyResults.id, krId)); await addKRLog(krId, 'postponed', `截止日期 ${oldEndDate} → ${newEndDate},原因:${reason}`, operatorId, operatorName); await recalcObjectiveDates(kr.objectiveId); return { id: krId, endDate: newEndDate }; } // ── 暂停 ── export async function pauseKR(krId: string, reason: string, operatorId: string, operatorName: string) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, krId) }); if (!kr) throw new AppError(40404, 'Key result not found', 404); await db.update(keyResults).set({ status: 'paused', updatedAt: new Date() }) .where(eq(keyResults.id, krId)); await addKRLog(krId, 'paused', `原因:${reason}`, operatorId, operatorName); await recalcObjectiveProgress(kr.objectiveId); return { id: krId, status: 'paused' }; } // ── 恢复 ── export async function resumeKR(krId: string, newEndDate: string, operatorId: string, operatorName: string) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, krId) }); if (!kr) throw new AppError(40404, 'Key result not found', 404); const oldEndDate = kr.endDate || '未设置'; await db.update(keyResults).set({ status: 'active', endDate: newEndDate, updatedAt: new Date() }) .where(eq(keyResults.id, krId)); await addKRLog(krId, 'resumed', `恢复为进行中,截止日期 ${oldEndDate} → ${newEndDate}`, operatorId, operatorName); await recalcObjectiveProgress(kr.objectiveId); await recalcObjectiveDates(kr.objectiveId); return { id: krId, status: 'active', endDate: newEndDate }; } // ── 取消 ── export async function cancelKR(krId: string, reason: string, operatorId: string, operatorName: string) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, krId) }); if (!kr) throw new AppError(40404, 'Key result not found', 404); await db.update(keyResults).set({ status: 'cancelled', updatedAt: new Date() }) .where(eq(keyResults.id, krId)); await addKRLog(krId, 'cancelled', `原因:${reason}`, operatorId, operatorName); await recalcObjectiveProgress(kr.objectiveId); return { id: krId, status: 'cancelled' }; } // ── 获取操作日志 ── export async function getKRLogs(krId: string) { return await db.select().from(krLogs) .where(eq(krLogs.krId, krId)) .orderBy(desc(krLogs.createdAt)); }