'use client'; import { useState } from 'react'; import { X, AlertTriangle, Link2, ChevronRight, Bug as BugIcon, Trash2, ArrowRightLeft } from 'lucide-react'; import { TestCaseStatusBadge } from './TestCaseStatusBadge'; import { BugStatusBadge } from '@/components/bug/BugStatusBadge'; import { ActivityLogPanel } from '@/components/ActivityLogPanel'; import { CommentPanel } from '@/components/comment/CommentPanel'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useBugStore } from '@/stores/useBugStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { FilterSelect } from '@/components/FilterSelect'; import { WorkDateTimePicker } from '@/components/WorkDateTimePicker'; import { CategoryChip } from '@/components/dev-task/CategoryChip'; import { TC_ALLOWED_TRANSITIONS, TEST_CASE_STATUS_LABEL, canStartTestCase, getTestCaseActualHours, needsTestCaseClaim } from '@/lib/test-case'; import { calcWorkHours, formatWorkHours, isoToLocal, localToISO } from '@/lib/work-hours'; import { formatDateTime } from '@/lib/format'; import { BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; import type { TestCaseStatus } from '@/lib/test-case'; interface Props { testCaseId: string; onClose: () => void; onCreateBug?: (testCaseId: string) => void; contextLabel?: string; readOnly?: boolean; } function defaultPlanStartLocal(): string { const d = new Date(); d.setHours(9, 0, 0, 0); return isoToLocal(d.toISOString()); } function defaultPlanEndLocal(): string { const d = new Date(); d.setHours(10, 0, 0, 0); return isoToLocal(d.toISOString()); } export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, contextLabel, readOnly = false }: Props) { const { testCases, changeStatus, deleteTestCase, updateTestCase } = useTestCaseStore(); const { bugs } = useBugStore(); const { requirements } = useRequirementStore(); const { members } = useMemberStore(); const { categories } = useTaskCategoryStore(); const user = useAuthStore((s) => s.user); const [showTransfer, setShowTransfer] = useState(false); const [transferTo, setTransferTo] = useState(''); const [showPlanInput, setShowPlanInput] = useState(false); const [planStartLocal, setPlanStartLocal] = useState(''); const [planEndLocal, setPlanEndLocal] = useState(''); const tc = testCases.find((c) => c.id === testCaseId); if (!tc) return null; const requirement = requirements.find((r) => r.id === tc.requirementId); const category = categories.find((c) => c.id === tc.categoryId); const relatedBugs = bugs.filter((b) => b.testCaseId === tc.id); const nextStatuses = TC_ALLOWED_TRANSITIONS[tc.status]; const executorEstimateHours = typeof tc.estimateHours === 'number' && tc.estimateHours > 0 ? tc.estimateHours : undefined; const aiEstimateHours = typeof tc.aiEstimateHours === 'number' && tc.aiEstimateHours > 0 ? tc.aiEstimateHours : undefined; const actualHours = getTestCaseActualHours(tc); const planDisplay = tc.plannedTestAt && tc.plannedEndAt ? `${formatDateTime(tc.plannedTestAt)} → ${formatDateTime(tc.plannedEndAt)}` : tc.plannedTestAt ? formatDateTime(tc.plannedTestAt) : '-'; const currentUserName = user?.name || ''; const needsClaim = needsTestCaseClaim(tc); const startReady = canStartTestCase(tc); const visibleNextStatuses = nextStatuses.filter((status) => status !== 'running' || startReady); const planStartISO = localToISO(planStartLocal); const planEndISO = localToISO(planEndLocal); const planStartBeforeEnd = Boolean(planStartISO && planEndISO && planEndISO > planStartISO); const planEstimateHours = planStartBeforeEnd ? calcWorkHours(planStartISO, planEndISO) : 0; const openPlanInput = () => { if (readOnly) return; setPlanStartLocal(tc.plannedTestAt ? isoToLocal(tc.plannedTestAt) : defaultPlanStartLocal()); setPlanEndLocal(tc.plannedEndAt ? isoToLocal(tc.plannedEndAt) : defaultPlanEndLocal()); setShowPlanInput(true); }; const handleSavePlan = () => { if (readOnly) return; const assigneeId = tc.assigneeId || currentUserName; if (!assigneeId) { alert('领取前需要先登录或选择负责人'); return; } if (!planStartBeforeEnd || planEstimateHours <= 0 || !planStartISO || !planEndISO) return; updateTestCase(tc.id, { assigneeId, plannedTestAt: planStartISO, plannedEndAt: planEndISO, estimateHours: planEstimateHours, }); setShowPlanInput(false); }; const [failReason, setFailReason] = useState(''); const [blockReason, setBlockReason] = useState(''); const [showFailInput, setShowFailInput] = useState(false); const [showBlockInput, setShowBlockInput] = useState(false); const handleTransition = (to: TestCaseStatus) => { if (readOnly) return; if (to === 'running' && !startReady) { openPlanInput(); return; } if (to === 'failed') { setShowFailInput(true); return; } if (to === 'blocked') { setShowBlockInput(true); return; } changeStatus(tc.id, to); }; const confirmFail = () => { if (readOnly) return; changeStatus(tc.id, 'failed', { failReason: failReason.trim() || undefined }); setShowFailInput(false); setFailReason(''); }; const confirmBlock = () => { if (readOnly) return; changeStatus(tc.id, 'blocked', { blockReason: blockReason.trim() || undefined }); setShowBlockInput(false); setBlockReason(''); }; return (
{tc.description}
暂无关联 Bug
) : (