15 KiB
Workspace Daily Report Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a right-side “今日日报” column to /workspace that summarizes the current logged-in user's worklog entries for today.
Architecture: Keep the feature as a derived workspace view. A pure helper combines TaskWorklog[] with existing WorkItem[], a focused DailyReportPanel renders the summary, and WorkspacePage wires the existing stores together without creating a new daily report entity.
Tech Stack: Next.js App Router, React client components, Zustand stores, TypeScript, Node node:test.
File Structure
- Create
apps/web/lib/workspace-daily-report.ts- Owns the pure daily report derivation logic.
- Depends only on
TaskWorklogandWorkItemtypes.
- Create
apps/web/lib/workspace-daily-report.test.ts- Covers filtering, totals, task context resolution, and deleted/missing task behavior.
- Create
apps/web/components/workspace/DailyReportPanel.tsx- Owns the right-side column UI for the already-derived report.
- No store access inside the component.
- Modify
apps/web/app/workspace/page.tsx- Fetches task worklogs.
- Builds the daily report with current user + today's date.
- Adds the fixed right-side column.
Task 1: Daily Report Derivation Helper
Files:
-
Create:
apps/web/lib/workspace-daily-report.test.ts -
Create:
apps/web/lib/workspace-daily-report.ts -
Step 1: Write the failing helper tests
Create apps/web/lib/workspace-daily-report.test.ts:
import test from 'node:test';
import assert from 'node:assert/strict';
import type { TaskWorklog } from './task-worklog';
import type { WorkItem } from './workspace-engine';
import { getWorkspaceDailyReport } from './workspace-daily-report';
const workItems: WorkItem[] = [
{
id: 'task-1',
type: 'devTask',
title: '实现登录接口',
status: 'in_progress',
completed: false,
productName: 'FTB',
projectName: '项目管理',
versionName: 'V1.0',
versionId: 'version-1',
raw: {} as any,
},
{
id: 'task-2',
type: 'bug',
title: '修复菜单错位',
status: 'fixing',
completed: false,
productName: 'FTB',
projectName: '项目管理',
versionName: 'V1.0',
versionId: 'version-1',
raw: {} as any,
},
];
const worklogs: TaskWorklog[] = [
{
id: 'wl-1',
taskId: 'task-1',
userId: '张三',
date: '2026-06-26',
hours: 1.5,
workContent: '完成登录接口联调',
createdAt: '2026-06-26T02:00:00.000Z',
},
{
id: 'wl-2',
taskId: 'task-2',
userId: '张三',
date: '2026-06-26',
hours: 0.5,
workContent: '定位菜单错位原因',
createdAt: '2026-06-26T03:00:00.000Z',
},
{
id: 'wl-other-user',
taskId: 'task-1',
userId: '李四',
date: '2026-06-26',
hours: 8,
workContent: '其他人的日报',
createdAt: '2026-06-26T04:00:00.000Z',
},
{
id: 'wl-other-date',
taskId: 'task-1',
userId: '张三',
date: '2026-06-25',
hours: 8,
workContent: '昨天的日报',
createdAt: '2026-06-25T04:00:00.000Z',
},
];
test('getWorkspaceDailyReport filters by current user and date', () => {
const report = getWorkspaceDailyReport({
worklogs,
workItems,
userId: '张三',
date: '2026-06-26',
});
assert.equal(report.totalHours, 2);
assert.equal(report.totalCount, 2);
assert.deepEqual(report.items.map((item) => item.id), ['wl-2', 'wl-1']);
});
test('getWorkspaceDailyReport resolves task title and workspace context', () => {
const report = getWorkspaceDailyReport({
worklogs,
workItems,
userId: '张三',
date: '2026-06-26',
});
assert.equal(report.items[0].taskTitle, '修复菜单错位');
assert.equal(report.items[0].productName, 'FTB');
assert.equal(report.items[0].projectName, '项目管理');
assert.equal(report.items[0].versionName, 'V1.0');
});
test('getWorkspaceDailyReport keeps logs whose task is no longer visible', () => {
const report = getWorkspaceDailyReport({
worklogs: [
{
id: 'wl-missing',
taskId: 'deleted-task',
userId: '张三',
date: '2026-06-26',
hours: 0.5,
workContent: '处理历史任务',
createdAt: '2026-06-26T01:00:00.000Z',
},
],
workItems: [],
userId: '张三',
date: '2026-06-26',
});
assert.equal(report.totalHours, 0.5);
assert.equal(report.totalCount, 1);
assert.equal(report.items[0].taskTitle, '未知任务');
assert.equal(report.items[0].workContent, '处理历史任务');
});
- Step 2: Run the helper tests and verify they fail
Run:
pnpm --filter=web test
Expected: TypeScript compilation fails because ./workspace-daily-report does not exist.
- Step 3: Implement the helper
Create apps/web/lib/workspace-daily-report.ts:
import type { TaskWorklog } from './task-worklog';
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 WorkspaceDailyReport {
date: string;
totalHours: number;
totalCount: number;
items: WorkspaceDailyReportItem[];
}
interface GetWorkspaceDailyReportInput {
worklogs: TaskWorklog[];
workItems: WorkItem[];
userId: string;
date: string;
}
export function getWorkspaceDailyReport({
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,
};
});
return {
date,
totalHours: items.reduce((sum, item) => sum + item.hours, 0),
totalCount: items.length,
items,
};
}
- Step 4: Run the helper tests and verify they pass
Run:
pnpm --filter=web test
Expected: PASS for the new workspace-daily-report tests.
- Step 5: Commit Task 1
Run:
git add apps/web/lib/workspace-daily-report.ts apps/web/lib/workspace-daily-report.test.ts
git commit -m "feat(workspace): 添加个人日报派生逻辑"
Expected: commit succeeds with only the helper and helper test staged.
Task 2: Daily Report Panel Component
Files:
-
Create:
apps/web/components/workspace/DailyReportPanel.tsx -
Step 1: Create the presentational component
Create apps/web/components/workspace/DailyReportPanel.tsx:
'use client';
import { CalendarDays, ClipboardList, Clock3 } from 'lucide-react';
import type { WorkspaceDailyReport } from '@/lib/workspace-daily-report';
import { formatWorkHours, formatWorkHoursShort } from '@/lib/work-hours';
interface Props {
report: WorkspaceDailyReport;
}
export function DailyReportPanel({ report }: Props) {
return (
<aside className="w-80 shrink-0 border-l border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
<div className="flex h-14 items-center justify-between border-b border-[var(--line)] px-4">
<div>
<h2 className="text-[14px] font-semibold text-[var(--ink)]">今日日报</h2>
<div className="mt-0.5 flex items-center gap-1 text-[11px] text-[var(--ink-muted)]">
<CalendarDays className="h-3 w-3" />
<span>{report.date}</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 border-b border-[var(--line)] p-3">
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg)] p-2">
<div className="flex items-center gap-1 text-[10px] text-[var(--ink-muted)]">
<Clock3 className="h-3 w-3" />
<span>今日合计</span>
</div>
<p className="mt-1 text-[16px] font-semibold text-[var(--ink)]">{formatWorkHours(report.totalHours)}</p>
</div>
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg)] p-2">
<div className="flex items-center gap-1 text-[10px] text-[var(--ink-muted)]">
<ClipboardList className="h-3 w-3" />
<span>已登记</span>
</div>
<p className="mt-1 text-[16px] font-semibold text-[var(--ink)]">{report.totalCount} 条</p>
</div>
</div>
<div className="flex-1 overflow-y-auto p-3">
{report.items.length === 0 ? (
<div className="rounded-lg border border-dashed border-[var(--line)] bg-[var(--bg)] p-4 text-center">
<p className="text-[13px] font-medium text-[var(--ink)]">今日暂无日报记录</p>
<p className="mt-1 text-[11px] leading-5 text-[var(--ink-muted)]">从任务详情里的工时记录登记今日工作内容。</p>
</div>
) : (
<div className="space-y-2">
{report.items.map((item) => (
<div key={item.id} className="rounded-lg border border-[var(--line)] bg-[var(--bg)] p-3">
<div className="flex items-start justify-between gap-2">
<p className="min-w-0 flex-1 text-[12px] font-medium leading-5 text-[var(--ink)]">{item.workContent}</p>
<span className="shrink-0 rounded bg-[var(--accent-soft)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--accent)]">
{formatWorkHoursShort(item.hours)}
</span>
</div>
<p className="mt-2 truncate text-[11px] text-[var(--ink-soft)]">{item.taskTitle}</p>
{(item.productName || item.projectName || item.versionName) && (
<p className="mt-1 truncate text-[10px] text-[var(--ink-muted)]">
{[item.productName, item.projectName, item.versionName].filter(Boolean).join(' / ')}
</p>
)}
</div>
))}
</div>
)}
</div>
</aside>
);
}
- Step 2: Run type-check for the new component
Run:
pnpm --filter=web type-check
Expected: PASS. If it fails because later integration imports are not present, fix only this component's imports and prop types.
- Step 3: Commit Task 2
Run:
git add apps/web/components/workspace/DailyReportPanel.tsx
git commit -m "feat(workspace): 添加今日日报侧栏组件"
Expected: commit succeeds with only the new component staged.
Task 3: Wire the Panel Into Workspace Page
Files:
-
Modify:
apps/web/app/workspace/page.tsx -
Step 1: Add imports
In apps/web/app/workspace/page.tsx, add these imports with the existing store and helper imports:
import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore';
import { DailyReportPanel } from '@/components/workspace/DailyReportPanel';
import { getWorkspaceDailyReport } from '@/lib/workspace-daily-report';
- Step 2: Read and fetch worklogs
Inside WorkspacePage, after the existing store hooks, add:
const { worklogs, fetchWorklogs } = useTaskWorklogStore();
After the existing fetch effects, add:
useEffect(() => { fetchWorklogs(); }, [fetchWorklogs]);
- Step 3: Derive today's report
After filteredItems is defined, add:
const today = useMemo(() => new Date().toISOString().slice(0, 10), []);
const dailyReport = useMemo(() =>
getWorkspaceDailyReport({
worklogs,
workItems,
userId: userName,
date: today,
}),
[worklogs, workItems, userName, today]
);
- Step 4: Add the right-side column
In the JSX returned by WorkspacePage, keep the existing main task list block and add the daily report panel immediately after it, before the detail drawers:
{/* 右侧:任务列表 */}
<div className="min-w-0 flex-1 flex flex-col overflow-hidden">
<header className="flex h-14 shrink-0 items-center border-b border-[var(--line)] bg-[var(--bg-card)] px-5">
<h2 className="text-[14px] font-semibold text-[var(--ink)]">{TABS.find((t) => t.key === activeTab)?.label}</h2>
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{filteredItems.length} 项</span>
{selectedVersionId && (
<span className="ml-3 text-[11px] text-[var(--accent)] bg-[var(--accent-soft)] px-2 py-0.5 rounded-full">
{versionMap.get(selectedVersionId)?.name}
</span>
)}
</header>
<div className="flex-1 overflow-y-auto p-5 bg-[var(--bg)]">
{filteredItems.length === 0 ? (
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
<p className="text-[13px] text-[var(--ink-muted)]">暂无相关任务</p>
</div>
) : (
<div className="space-y-2">
{filteredItems.map((item) => (
<WorkItemCard
key={item.id}
item={item}
onNavigate={() => item.versionId && router.push(`/versions/${item.versionId}`)}
onClick={() => setDrawerItem(item)}
/>
))}
</div>
)}
</div>
</div>
<DailyReportPanel report={dailyReport} />
The important class change is min-w-0 flex-1 on the task list container. This lets the center list shrink correctly after adding the fixed w-80 daily report column.
- Step 5: Run tests and type-check
Run:
pnpm --filter=web test
pnpm --filter=web type-check
Expected: both commands pass.
- Step 6: Commit Task 3
Run:
git add apps/web/app/workspace/page.tsx
git commit -m "feat(workspace): 在工作台接入今日日报列"
Expected: commit succeeds with only WorkspacePage staged.
Task 4: Final Verification
Files:
-
Verify:
apps/web/lib/workspace-daily-report.ts -
Verify:
apps/web/components/workspace/DailyReportPanel.tsx -
Verify:
apps/web/app/workspace/page.tsx -
Step 1: Run the full web test suite
Run:
pnpm --filter=web test
Expected: PASS.
- Step 2: Run TypeScript checking
Run:
pnpm --filter=web type-check
Expected: PASS.
- Step 3: Check the workspace page if the dev server is already running
Run:
curl http://localhost:3000/workspace
Expected: HTTP 200 HTML response. If the dev server is not running, record that this browser-level check was not available and do not start unrelated services unless the execution context asks for interactive verification.
- Step 4: Confirm git state
Run:
git status --short
Expected: only pre-existing unrelated files remain modified or untracked; the files from this plan are committed.