'use client'; import { useState } from 'react'; import { X, Check, Link2, FileUp, ExternalLink, Play, ArrowRightLeft } from 'lucide-react'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { FilterSelect } from '@/components/FilterSelect'; import { calcPlanProgress, getRequirementCoverageSummary, PRODUCT_PLAN_KIND_LABEL, PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS, PRODUCT_PLAN_REVIEW_RESULT_LABEL, } from '@/lib/version-plan'; import { formatDateTime } from '@/lib/format'; import type { PlanTask, ProductPlanKind, ProductPlanReviewFailureType, ProductPlanReviewResult, VersionPlan } from '@/lib/version-plan'; import { canEditPlanRequirementCoverage, canTogglePlanChecklist, getPlanCompletionState } from '@/lib/version-plan-workflow'; import type { PlanResultPayload } from '@/lib/version-plan-workflow'; import { PlanLogTimeline, PlanRequirementCoveragePanel } from './PlanRequirementCoveragePanel'; interface Props { planId: string; onClose: () => void; contextLabel?: string; } const STATUS_STYLE: Record = { pending: 'bg-zinc-100 text-zinc-600', in_progress: 'bg-blue-50 text-blue-600', completed: 'bg-emerald-50 text-emerald-600' }; const STATUS_LABEL: Record = { pending: '未开始', in_progress: '进行中', completed: '已完成' }; const TYPE_LABEL: Record = { research: '调研', product: '产品方案', ui: 'UI设计' }; 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 PlanDetailDrawer({ planId, onClose, contextLabel }: Props) { const { plans, updatePlan, completePlan } = useVersionPlanStore(); const { requirements } = useRequirementStore(); const { members } = useMemberStore(); const user = useAuthStore((s) => s.user); const [showTransfer, setShowTransfer] = useState(false); const [transferTo, setTransferTo] = useState(''); const [resultType, setResultType] = useState<'link' | 'file'>('link'); const [resultTitle, setResultTitle] = useState(''); const [resultUrl, setResultUrl] = useState(''); const [fileName, setFileName] = useState(''); const [fileData, setFileData] = useState(''); const [prototypeReviewConfirmed, setPrototypeReviewConfirmed] = useState(false); const [reviewResult, setReviewResult] = useState('passed'); const [reviewFailureTypes, setReviewFailureTypes] = useState>(new Set()); const [reviewFailureReason, setReviewFailureReason] = useState(''); const [showComplete, setShowComplete] = useState(false); const plan = plans.find((p) => p.id === planId); if (!plan) return null; const completionState = getPlanCompletionState(plan); const isResearch = plan.type === 'research'; const progress = isResearch ? calcPlanProgress(plan.tasks) : getRequirementCoverageSummary(plan).percent; const linkedReqs = (plan.linkedRequirementIds || []).map((id) => requirements.find((r) => r.id === id)).filter(Boolean) as { id: string; code: string; title: string }[]; const canToggle = canTogglePlanChecklist(plan); const canEditCoverage = canEditPlanRequirementCoverage(plan); const currentUserName = user?.name ?? plan.owner; const productPlanKind = plan.type === 'product' ? getProductPlanKind(plan) : undefined; const isProductDesignPlan = productPlanKind === 'design'; const isProductReviewPlan = productPlanKind === 'review'; const handleToggleTask = (task: PlanTask) => { if (!canToggle) return; const nextStatus = task.status === 'completed' ? 'pending' : 'completed'; const updatedTasks = (plan.tasks || []).map((t) => t.id === task.id ? { ...t, status: nextStatus as PlanTask['status'] } : t); updatePlan(plan.id, { tasks: updatedTasks }); }; const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setFileName(file.name); const reader = new FileReader(); reader.onload = () => setFileData(reader.result as string); reader.readAsDataURL(file); }; const toggleFailureType = (type: ProductPlanReviewFailureType) => { const next = new Set(reviewFailureTypes); if (next.has(type)) next.delete(type); else next.add(type); setReviewFailureTypes(next); }; const handleSubmitResult = () => { let payload: PlanResultPayload | null = null; if (isProductReviewPlan) { payload = { productPlanKind: 'review', reviewResult, reviewFailureTypes: reviewResult === 'failed' ? Array.from(reviewFailureTypes) : undefined, reviewFailureReason: reviewResult === 'failed' ? reviewFailureReason.trim() : undefined, resultTitle: PRODUCT_PLAN_REVIEW_RESULT_LABEL[reviewResult], }; } else if (isProductDesignPlan) { const title = resultTitle.trim(); const url = resultUrl.trim(); if (!url || !title || !prototypeReviewConfirmed) return; payload = { productPlanKind: 'design', resultType: 'link', resultTitle: title, resultUrl: url, prototypeReviewConfirmed, }; } else { const url = resultType === 'link' ? resultUrl.trim() : fileData; const title = resultTitle.trim(); if (!url || !title) return; payload = { resultType, resultTitle: title, resultUrl: url, resultFileName: fileName || undefined, resultFileData: resultType === 'file' ? fileData : undefined, }; } const response = completePlan(plan.id, payload); if (response && typeof response === 'object' && 'ok' in response && !response.ok) { alert(response.message || '计划未满足完成条件'); return; } setShowComplete(false); }; const handleTransfer = () => { if (!transferTo) return; updatePlan(plan.id, { owner: transferTo }); setShowTransfer(false); setTransferTo(''); }; return (
e.stopPropagation()}> {/* Context */} {contextLabel && (
{contextLabel}
)} {/* Header */}
{TYPE_LABEL[plan.type]} {plan.type === 'product' && ( {PRODUCT_PLAN_KIND_LABEL[getProductPlanKind(plan)]} )} {STATUS_LABEL[plan.status]} {plan.type === 'product' && plan.reviewResult && ( {PRODUCT_PLAN_REVIEW_RESULT_LABEL[plan.reviewResult]} )}
{/* Body */}

{plan.title}

{/* Info */}
负责人
{plan.owner}
进度
{progress}%
计划时间
{formatDateTime(plan.startTime)} → {formatDateTime(plan.endTime)}
{plan.actualStartAt && (
实际开始
{formatDateTime(plan.actualStartAt)}
)} {plan.completedAt && (
完成时间
{formatDateTime(plan.completedAt)}
)}
{/* Progress Bar */}
{/* Research Tasks */} {isResearch && plan.tasks && plan.tasks.length > 0 && (
任务清单
{plan.tasks.map((task) => (
{task.title}
))}
)} {linkedReqs.length > 0 && (
{plan.status === 'in_progress' && !completionState.canSubmitResult && (

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

)}
)} {!isResearch && ( )} {/* Result */} {plan.status === 'completed' && plan.resultUrl && (
成果
)} {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.remark && (
备注
{plan.remark}
)} {/* Transfer Section */} {showTransfer && (
转交给
setTransferTo(value === 'all' ? '' : value)} options={members.filter((m) => m.name !== plan.owner).map((m) => ({ value: m.name, label: m.name }))} allLabel="选择人员" />
)} {/* Complete with result */} {showComplete && (
{getSubmitActionLabel(plan)}
{isProductReviewPlan ? ( <>
{(['passed', 'failed'] as ProductPlanReviewResult[]).map((result) => ( ))}
{reviewResult === 'failed' && ( <>
{PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS.map((option) => ( ))}