167 lines
4.6 KiB
TypeScript
167 lines
4.6 KiB
TypeScript
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<WorkActivityCategory, WorkspaceDailyReportActivityItem[]>;
|
|
|
|
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<string>): 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;
|
|
}
|