Files
ftb-project-management/apps/web/components/workspace/DailyReportPanel.tsx
Script Generator 04520bd8c5 feat(版本): 记录计划需求进展活动
关键改动:

- 记录产品/UI 计划需求覆盖进展,并写入工作活动与实体日志

- 日报和小宝预警补充需求进展活动证据

- 调整版本列表与需求覆盖面板展示,并补充测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-06-30 15:35:36 +08:00

146 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { AlertTriangle, CalendarDays, ClipboardList, Clock3 } from 'lucide-react';
import { WORK_ACTIVITY_CATEGORY_LABEL, type WorkActivityCategory } from '@/lib/work-activity';
import type { WorkspaceDailyReport } from '@/lib/workspace-daily-report';
import { formatDateTimeShort } from '@/lib/format';
import { formatWorkHours, formatWorkHoursShort } from '@/lib/work-hours';
interface Props {
report: WorkspaceDailyReport;
}
export function DailyReportPanel({ report }: Props) {
const groupOrder: WorkActivityCategory[] = ['delivery', 'progress', 'creation', 'risk', 'note'];
const hasActivities = groupOrder.some((key) => report.groups[key].length > 0);
const hasLegacyWorklogs = report.items.length > 0;
const hasNeedsProgress = report.needsProgressItems.length > 0;
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">
{!hasActivities && !hasLegacyWorklogs && !hasNeedsProgress ? (
<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>
</div>
) : (
<div className="space-y-4">
{groupOrder.map((key) => {
const items = report.groups[key];
if (items.length === 0) return null;
return (
<section key={key}>
<div className="mb-2 flex items-center justify-between">
<h3 className="text-[11px] font-semibold text-[var(--ink-soft)]">{WORK_ACTIVITY_CATEGORY_LABEL[key]}</h3>
<span className="text-[10px] text-[var(--ink-muted)]">{items.length} </span>
</div>
<div className="space-y-2">
{items.map((item) => {
const completedContent = typeof item.metadata?.completedContent === 'string' ? item.metadata.completedContent : '';
const remainingContent = typeof item.metadata?.remainingContent === 'string' ? item.metadata.remainingContent : '';
return (
<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 break-words text-[12px] font-medium leading-5 text-[var(--ink)]">{item.summary}</p>
<span className="shrink-0 text-[10px] text-[var(--ink-muted)]">{formatDateTimeShort(item.occurredAt)}</span>
</div>
{(completedContent || remainingContent) && (
<div className="mt-2 space-y-1 border-l-2 border-amber-200 pl-2 text-[11px] leading-5 text-[var(--ink-soft)]">
{completedContent && <div className="break-words">{completedContent}</div>}
{remainingContent && <div className="break-words text-amber-700">{remainingContent}</div>}
</div>
)}
{item.context && (
<p className="mt-1 truncate text-[10px] text-[var(--ink-muted)]" title={item.context}>{item.context}</p>
)}
</div>
);
})}
</div>
</section>
);
})}
{hasLegacyWorklogs && (
<section>
<div className="mb-2 flex items-center justify-between">
<h3 className="text-[11px] font-semibold text-[var(--ink-soft)]"></h3>
<span className="text-[10px] text-[var(--ink-muted)]">{report.items.length} </span>
</div>
<div className="space-y-2">
{report.items.map((item) => {
const contextLabel = [item.productName, item.projectName, item.versionName].filter(Boolean).join(' / ');
return (
<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 break-words 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)]" title={item.taskTitle}>{item.taskTitle}</p>
{contextLabel && (
<p className="mt-1 truncate text-[10px] text-[var(--ink-muted)]" title={contextLabel}>
{contextLabel}
</p>
)}
</div>
);
})}
</div>
</section>
)}
{hasNeedsProgress && (
<section>
<div className="mb-2 flex items-center gap-1.5">
<AlertTriangle className="h-3 w-3 text-orange-500" />
<h3 className="text-[11px] font-semibold text-orange-700"></h3>
</div>
<div className="space-y-2">
{report.needsProgressItems.map((item) => (
<div key={item.id} className="rounded-lg border border-orange-200 bg-orange-50 p-3">
<p className="break-words text-[12px] font-medium leading-5 text-orange-800">{item.title}</p>
{item.context && <p className="mt-1 truncate text-[10px] text-orange-700/70" title={item.context}>{item.context}</p>}
</div>
))}
</div>
</section>
)}
</div>
)}
</div>
</aside>
);
}