feat(版本): 完善研发计划与预警已读

关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 16:48:58 +08:00
parent 04520bd8c5
commit 3d3d56697a
41 changed files with 1701 additions and 121 deletions

View File

@@ -120,12 +120,13 @@ export function getWorkspaceDailyReport({
...items.map((item) => item.taskId),
]);
const worklogTaskIds = new Set(items.map((item) => item.taskId));
const activityHours = Array.from(new Set(reportActivities.map((activity) => activity.sourceId)))
.filter((sourceId) => !worklogTaskIds.has(sourceId))
.reduce((sum, sourceId) => {
const item = workItemMap.get(sourceId);
return sum + calcWorkItemHoursForDate(item, date, now);
}, 0);
const activityHours = calcActivityHoursForDate(
Array.from(new Set(reportActivities.map((activity) => activity.sourceId))),
workItemMap,
worklogTaskIds,
date,
now,
);
const needsProgressItems = workItems
.filter((item) => shouldRequireProgress(item, date, touchedSourceIds))
@@ -297,25 +298,64 @@ function getTestCaseTerminalFallback(
return undefined;
}
function calcWorkItemHoursForDate(item: WorkItem | undefined, date: string, now: Date): number {
function calcActivityHoursForDate(
sourceIds: string[],
workItemMap: Map<string, WorkItem>,
excludedSourceIds: Set<string>,
date: string,
now: Date,
): number {
const intervals = sourceIds
.filter((sourceId) => !excludedSourceIds.has(sourceId))
.map((sourceId) => getWorkItemIntervalForDate(workItemMap.get(sourceId), date, now))
.filter(isTimeInterval);
return calcMergedIntervalHours(intervals);
}
function getWorkItemIntervalForDate(item: WorkItem | undefined, date: string, now: Date): { start: number; end: number } | undefined {
const interval = getActualInterval(item, now);
if (!interval) return 0;
if (!interval) return undefined;
const day = getLocalDateBounds(date);
if (!day) return 0;
if (!day) return undefined;
const start = new Date(interval.start).getTime();
const end = new Date(interval.end).getTime();
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return 0;
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return undefined;
const overlapStart = Math.max(start, day.start.getTime());
const overlapEnd = Math.min(end, day.end.getTime());
if (overlapEnd <= overlapStart) return 0;
if (overlapEnd <= overlapStart) return undefined;
return calcActualElapsedHours(
new Date(overlapStart).toISOString(),
new Date(overlapEnd).toISOString(),
);
return { start: overlapStart, end: overlapEnd };
}
function isTimeInterval(interval: { start: number; end: number } | undefined): interval is { start: number; end: number } {
return Boolean(interval);
}
function calcMergedIntervalHours(intervals: Array<{ start: number; end: number }>): number {
if (intervals.length === 0) return 0;
const sorted = [...intervals].sort((a, b) => a.start - b.start);
const merged: Array<{ start: number; end: number }> = [];
let current = { ...sorted[0] };
for (const interval of sorted.slice(1)) {
if (interval.start <= current.end) {
current.end = Math.max(current.end, interval.end);
} else {
merged.push(current);
current = { ...interval };
}
}
merged.push(current);
return merged.reduce((sum, interval) => sum + calcActualElapsedHours(
new Date(interval.start).toISOString(),
new Date(interval.end).toISOString(),
), 0);
}
function getActualInterval(item: WorkItem | undefined, now: Date): { start: string; end: string } | undefined {