'use client'; import { useMemo, useState } from 'react'; import { Plus, Pencil, Trash2, X, Check, ExternalLink, FileUp, Link2, Play, ArrowRightLeft } from 'lucide-react'; import type { ProductPlanKind, ProductPlanReviewFailureType, ProductPlanReviewResult, VersionPlan, PlanTask } from '@/lib/version-plan'; import { calcPlanDuration, formatDuration, calcTotalDuration, calcPlanProgress, sortPlansNewestFirst, PRODUCT_PLAN_KIND_LABEL, PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS, PRODUCT_PLAN_REVIEW_RESULT_LABEL, } from '@/lib/version-plan'; import { formatDateTime } from '@/lib/format'; import { FieldError } from '@/components/FieldError'; import { WorkDateTimePicker } from '@/components/WorkDateTimePicker'; import { AiDecomposeButton } from './AiDecomposeButton'; import { PlanLogTimeline, PlanRequirementCoveragePanel } from './PlanRequirementCoveragePanel'; import type { VersionWithContext } from '@/lib/derive'; import type { Requirement } from '@/lib/requirement'; import { mergeSelectedRequirementOptions } from '@/lib/requirement-selector'; import { canEditPlanRequirementCoverage, canTogglePlanChecklist, getPlanCompletionState } from '@/lib/version-plan-workflow'; import type { PlanResultPayload } from '@/lib/version-plan-workflow'; interface Props { plans: VersionPlan[]; versionId: string; version?: VersionWithContext; versionDeadline?: string; currentUserName: string; planType: 'research' | 'product' | 'ui'; versionMembers: { role: string; name: string }[]; linkedRequirements?: Requirement[]; allRequirements?: Requirement[]; onCreate: (data: Omit) => void; onUpdate: (id: string, data: Partial) => void; onComplete: (id: string, result: PlanResultPayload) => { ok: boolean; message?: string } | void; onDelete: (id: string) => void; } const TYPE_LABEL = { research: '调研', product: '产品方案', ui: 'UI设计' }; const STATUS_STYLE = { pending: 'bg-zinc-100 text-zinc-600', in_progress: 'bg-blue-50 text-blue-600 border-blue-200', completed: 'bg-green-50 text-green-700 border-green-200', }; const STATUS_LABEL = { pending: '未开始', in_progress: '进行中', completed: '已完成' }; function getProductPlanKind(plan: VersionPlan): ProductPlanKind { return plan.productPlanKind ?? 'design'; } function getSubmitActionLabel(plan: VersionPlan): string { if (plan.type === 'product') { return getProductPlanKind(plan) === 'review' ? '提交评审结论' : '提交原型地址'; } return '提交成果'; } function getFailureLabels(types?: ProductPlanReviewFailureType[]): string[] { if (!types?.length) return []; return types .map((type) => PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS.find((option) => option.value === type)?.label) .filter(Boolean) as string[]; } export function PlanTab({ plans, versionId, version, versionDeadline, currentUserName, planType, versionMembers, linkedRequirements, allRequirements, onCreate, onUpdate, onComplete, onDelete }: Props) { const [showCreateModal, setShowCreateModal] = useState(false); const [editingPlan, setEditingPlan] = useState(null); const [completingPlan, setCompletingPlan] = useState(null); const [transferPlanId, setTransferPlanId] = useState(null); const [transferTo, setTransferTo] = useState(''); const typePlans = sortPlansNewestFirst(plans.filter((p) => p.versionId === versionId && p.type === planType)); const totalDuration = calcTotalDuration(typePlans); return (
{typePlans.length} 个{TYPE_LABEL[planType]}计划 已耗时:{totalDuration} {versionDeadline && 版本截止:{versionDeadline}}
{typePlans.length === 0 ? (
暂无{TYPE_LABEL[planType]}计划
) : (
{typePlans.map((plan) => { const now = new Date().toISOString(); // 自动开始:如果到了计划开始日期且状态还是 pending,视为已开始 const autoStarted = plan.status === 'pending' && plan.startTime && new Date(plan.startTime) <= new Date(); const effectiveStatus = autoStarted ? 'in_progress' : plan.status; const effectiveStartAt = plan.actualStartAt || (autoStarted ? plan.startTime : null); const completionState = getPlanCompletionState(plan); const canToggle = canTogglePlanChecklist(plan); const canEditCoverage = canEditPlanRequirementCoverage(plan); const requirementOptions = mergeSelectedRequirementOptions(linkedRequirements ?? [], allRequirements ?? [], plan.linkedRequirementIds ?? []); // 耗时用实际时间戳计算 const dur = plan.status === 'completed' && plan.completedAt && plan.actualStartAt ? calcPlanDuration(plan.actualStartAt, plan.completedAt) : effectiveStatus === 'in_progress' && effectiveStartAt ? calcPlanDuration(effectiveStartAt, now) : { days: 0, hours: 0 }; const durText = dur.days > 0 || dur.hours > 0 ? formatDuration(dur.days, dur.hours) : '-'; // 如果自动开始了,触发 store 更新(副作用) if (autoStarted && !plan.actualStartAt) { onUpdate(plan.id, { status: 'in_progress' }); } return (
{plan.title} {STATUS_LABEL[effectiveStatus]} {plan.type === 'product' && ( {PRODUCT_PLAN_KIND_LABEL[getProductPlanKind(plan)]} )} {plan.type === 'product' && plan.reviewResult && ( {PRODUCT_PLAN_REVIEW_RESULT_LABEL[plan.reviewResult]} )}
负责人
{plan.owner}
计划时间
{formatDateTime(plan.startTime)} → {formatDateTime(plan.endTime)}
已耗时
{durText}
{plan.actualStartAt && (
实际开始
{formatDateTime(plan.actualStartAt)}
)} {plan.completedAt && (
完成时间
{formatDateTime(plan.completedAt)}
)}
{plan.overdueReason && (
超期原因:{plan.overdueReason}
)} {plan.remark && (
备注
{plan.remark}
)} {plan.status === 'completed' && plan.resultUrl && (
{plan.resultType === 'link' ? : } {plan.resultTitle || plan.resultFileName || '查看成果'} {planType === 'product' && version && ( )}
)} {plan.status === 'completed' && plan.type === 'product' && getProductPlanKind(plan) === 'review' && plan.reviewResult && (
{PRODUCT_PLAN_REVIEW_RESULT_LABEL[plan.reviewResult]}
{plan.reviewResult === 'failed' && (
{getFailureLabels(plan.reviewFailureTypes).length > 0 &&
类型:{getFailureLabels(plan.reviewFailureTypes).join('、')}
} {plan.reviewFailureReason &&
原因:{plan.reviewFailureReason}
}
)}
)} {/* 子任务 */} {plan.type === 'research' && plan.tasks && plan.tasks.length > 0 && (
{calcPlanProgress(plan.tasks)}%
{plan.tasks.map((task) => (
{task.title} {task.status === 'completed' ? '已完成' : '未完成'}
))}
)} {plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0 && requirementOptions.length > 0 && ( requirementOptions.find((r) => r.id === rid)).filter(Boolean) as Requirement[]} canEdit={canEditCoverage} currentUserName={currentUserName} onUpdate={onUpdate} /> )} {plan.status === 'in_progress' && !completionState.canSubmitResult && (

还不能提交成果:{completionState.missingReasons.join('、')}

)}
{plan.status === 'pending' && !autoStarted && ( )} {plan.status !== 'completed' && ( <> )}
{transferPlanId === plan.id && (
转交给:
)} {plan.status !== 'completed' && completionState.canSubmitResult && (
已满足{getSubmitActionLabel(plan)}条件
)}
{plan.type !== 'research' && ( )}
); })}
)} {(showCreateModal || editingPlan) && ( { setShowCreateModal(false); setEditingPlan(null); }} onSubmit={(data) => { if (editingPlan) onUpdate(editingPlan.id, data); else onCreate(data as any); setShowCreateModal(false); setEditingPlan(null); }} /> )} {completingPlan && ( setCompletingPlan(null)} onSubmit={(result) => { const response = onComplete(completingPlan.id, result); if (response && typeof response === 'object' && 'ok' in response && !response.ok) { alert(response.message || '计划未满足完成条件'); return; } setCompletingPlan(null); }} /> )}
); } function PlanFormModal({ initial, planType, versionId, versionDeadline, currentUserName, linkedRequirements, allRequirements, onClose, onSubmit }: { initial: VersionPlan | null; planType: 'research' | 'product' | 'ui'; versionId: string; versionDeadline?: string; currentUserName: string; linkedRequirements?: Requirement[]; allRequirements?: Requirement[]; onClose: () => void; onSubmit: (data: any) => void; }) { const now = new Date().toISOString().slice(0, 16); const [title, setTitle] = useState(initial?.title ?? ''); const [owner] = useState(initial?.owner ?? currentUserName); const [startTime, setStartTime] = useState(initial?.startTime?.slice(0, 16) ?? now); const [endTime, setEndTime] = useState(initial?.endTime?.slice(0, 16) ?? ''); const [remark, setRemark] = useState(initial?.remark ?? ''); const [productPlanKind, setProductPlanKind] = useState(initial?.productPlanKind ?? 'design'); const [tasks, setTasks] = useState(initial?.tasks ?? []); const [newTaskTitle, setNewTaskTitle] = useState(''); const [overdueReason, setOverdueReason] = useState(initial?.overdueReason ?? ''); const [selectedReqs, setSelectedReqs] = useState>(new Set(initial?.linkedRequirementIds ?? [])); const [endTimeError, setEndTimeError] = useState(''); const requirementOptions = useMemo( () => mergeSelectedRequirementOptions(linkedRequirements ?? [], allRequirements ?? [], Array.from(selectedReqs)), [linkedRequirements, allRequirements, selectedReqs], ); const isOverdue = !!(versionDeadline && endTime && new Date(endTime) > new Date(versionDeadline)); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title.trim() || !endTime) return; if (startTime && endTime && new Date(endTime).getTime() <= new Date(startTime).getTime()) { setEndTimeError('结束日期必须晚于开始日期'); return; } setEndTimeError(''); if (isOverdue && !overdueReason.trim()) return; if (planType === 'research' && tasks.length === 0) return; onSubmit({ versionId, type: planType, title: title.trim(), owner: owner.trim() || currentUserName, startTime, endTime, status: initial?.status ?? 'pending', productPlanKind: planType === 'product' ? productPlanKind : undefined, linkedRequirementIds: requirementOptions.length > 0 ? Array.from(selectedReqs) : undefined, tasks: planType === 'research' && tasks.length > 0 ? tasks : undefined, remark: remark.trim() || undefined, overdueReason: isOverdue ? overdueReason.trim() : undefined, addedBy: currentUserName, }); }; return (
e.stopPropagation()}>

{initial ? '编辑' : '新建'}{TYPE_LABEL[planType]}计划

setTitle(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
{planType === 'product' && (
{(['design', 'review'] as ProductPlanKind[]).map((kind) => ( ))}
)}
{ setEndTime(next); setEndTimeError(''); }} placeholder="选择计划截止时间" defaultHour={18} popoverAlign="right" />
{versionDeadline && (
版本截止日期:{versionDeadline}
)} {isOverdue && (
⚠ 结束日期超出版本截止日期,请说明原因