import type { TaskWorklog } from './task-worklog'; import type { WorkActivity, WorkActivityCategory } from './work-activity'; import type { WorkItem } from './workspace-engine'; export interface WorkspaceDailyReportItem { id: string; taskId: string; taskTitle: string; workContent: string; hours: number; createdAt: string; productName?: string; projectName?: string; versionName?: string; } export interface WorkspaceDailyReportActivityItem { id: string; sourceId: string; sourceType: WorkActivity['sourceType']; action: WorkActivity['action']; category: WorkActivityCategory; title: string; summary: string; occurredAt: string; context?: string; metadata?: WorkActivity['metadata']; } export interface WorkspaceDailyReportNeedsProgressItem { id: string; title: string; status: string; type: WorkItem['type']; context?: string; } export type WorkspaceDailyReportGroups = Record; export interface WorkspaceDailyReport { date: string; totalHours: number; totalCount: number; items: WorkspaceDailyReportItem[]; groups: WorkspaceDailyReportGroups; needsProgressItems: WorkspaceDailyReportNeedsProgressItem[]; } interface GetWorkspaceDailyReportInput { activities?: WorkActivity[]; worklogs: TaskWorklog[]; workItems: WorkItem[]; userId: string; date: string; } export function getWorkspaceDailyReport({ activities = [], worklogs, workItems, userId, date, }: GetWorkspaceDailyReportInput): WorkspaceDailyReport { const workItemMap = new Map(workItems.map((item) => [item.id, item])); const items = worklogs .filter((log) => log.userId === userId && log.date === date) .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) .map((log) => { const item = workItemMap.get(log.taskId); return { id: log.id, taskId: log.taskId, taskTitle: item?.title ?? '未知任务', workContent: log.workContent, hours: log.hours, createdAt: log.createdAt, productName: item?.productName, projectName: item?.projectName, versionName: item?.versionName, }; }); const dailyActivities = activities .filter((activity) => activity.actorId === userId && activity.date === date) .sort((a, b) => b.occurredAt.localeCompare(a.occurredAt)); const groups = makeEmptyGroups(); for (const activity of dailyActivities) { const item = workItemMap.get(activity.sourceId); groups[activity.category].push({ id: activity.id, sourceId: activity.sourceId, sourceType: activity.sourceType, action: activity.action, category: activity.category, title: activity.title, summary: activity.summary, occurredAt: activity.occurredAt, context: getContext(item), metadata: activity.metadata, }); } const touchedSourceIds = new Set([ ...dailyActivities.map((activity) => activity.sourceId), ...items.map((item) => item.taskId), ]); const needsProgressItems = workItems .filter((item) => shouldRequireProgress(item, date, touchedSourceIds)) .map((item) => ({ id: item.id, title: item.title, status: item.status, type: item.type, context: getContext(item), })); return { date, totalHours: items.reduce((sum, item) => sum + item.hours, 0), totalCount: items.length + dailyActivities.length, items, groups, needsProgressItems, }; } function makeEmptyGroups(): WorkspaceDailyReportGroups { return { delivery: [], progress: [], creation: [], risk: [], note: [], }; } function getContext(item?: WorkItem): string | undefined { if (!item) return undefined; return [item.productName, item.projectName, item.versionName].filter(Boolean).join(' / '); } function shouldRequireProgress(item: WorkItem, date: string, touchedSourceIds: Set): boolean { if (item.completed) return false; if (touchedSourceIds.has(item.id)) return false; if (!isInProgressStatus(item.status)) return false; const startedAt = getStartedAt(item); if (!startedAt) return false; return startedAt.slice(0, 10) < date; } function isInProgressStatus(status: string): boolean { return ['in_progress', 'testing', 'running', 'fixing', 'verifying'].includes(status); } function getStartedAt(item: WorkItem): string | undefined { const extra = item.extra ?? {}; if (typeof extra.actualStartAt === 'string') return extra.actualStartAt; if (typeof extra.startedAt === 'string') return extra.startedAt; if (typeof extra.startTime === 'string') return extra.startTime; return undefined; }