'use client'; import { useState, useMemo, useEffect } from 'react'; import { Bug as BugIcon, Filter } from 'lucide-react'; import { useBugStore } from '@/stores/useBugStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { BugRow } from './BugRow'; import { BugDetailDrawer } from './BugDetailDrawer'; import { Pagination, usePagination } from '@/components/Pagination'; import { FilterSelect } from '@/components/FilterSelect'; import { SearchInput, matchTitleOrNo } from '@/components/SearchInput'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, aggregateBugActualHours, bugIntervals } from '@/lib/bug'; import { calcTwoMetrics } from '@/lib/work-hours'; import { isMemberReference, resolveMemberDisplayName } from '@/lib/member-system'; import { buildEffortSummaryMetrics, sumPositiveHours } from '@/lib/effort-summary'; import type { Bug, BugStatus, BugSeverity } from '@/lib/bug'; import type { TestCase } from '@/lib/test-case'; interface Props { versionId: string; requirementIds: string[]; readOnly?: boolean; versionBugs?: Bug[]; versionTestCases?: TestCase[]; } export function BugTab({ versionId, requirementIds, readOnly = false, versionBugs: scopedVersionBugs, versionTestCases }: Props) { const { bugs, fetchBugs } = useBugStore(); const { testCases, fetchTestCases } = useTestCaseStore(); const { requirements } = useRequirementStore(); const user = useAuthStore((s) => s.user); const members = useMemberStore((s) => s.members); useEffect(() => { if (!scopedVersionBugs) fetchBugs(); }, [fetchBugs, scopedVersionBugs]); useEffect(() => { if (!versionTestCases) fetchTestCases(); }, [fetchTestCases, versionTestCases]); const fallbackVersionBugs = useMemo( () => bugs.filter((b) => b.versionId === versionId), [bugs, versionId], ); const versionBugs = scopedVersionBugs ?? fallbackVersionBugs; const openCount = versionBugs.filter((b) => b.status === 'open').length; const fixingCount = versionBugs.filter((b) => b.status === 'fixing').length; const fixedCount = versionBugs.filter((b) => b.status === 'fixed' || b.status === 'verifying').length; const closedCount = versionBugs.filter((b) => b.status === 'closed' || b.status === 'rejected').length; const criticalCount = versionBugs.filter((b) => b.severity === 'critical' && b.status !== 'closed' && b.status !== 'rejected').length; const resolveRate = versionBugs.length > 0 ? Math.round(((fixedCount + closedCount) / versionBugs.length) * 100) : 0; const bugActualHours = useMemo(() => aggregateBugActualHours(versionBugs), [versionBugs]); const { manhours: bugManhours } = useMemo(() => calcTwoMetrics(bugIntervals(versionBugs)), [versionBugs]); const effortMetrics = useMemo( () => buildEffortSummaryMetrics({ aiEstimateHours: sumPositiveHours(versionBugs, (bug) => bug.aiEstimateHours), estimateHours: sumPositiveHours(versionBugs, (bug) => bug.estimateHours), actualHours: bugActualHours, manhours: bugManhours, }), [versionBugs, bugActualHours, bugManhours], ); const [filterAssignee, setFilterAssignee] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [filterSeverity, setFilterSeverity] = useState(''); const [keyword, setKeyword] = useState(''); const debouncedKeyword = useDebouncedValue(keyword, 300); const [selectedBugId, setSelectedBugId] = useState(null); const visibleTestCases = versionTestCases ?? testCases; const testCaseMap = useMemo(() => new Map(visibleTestCases.map((c) => [c.id, c])), [visibleTestCases]); const currentMember = useMemo(() => members.find((m) => m.id === user?.id), [members, user?.id]); const currentUserName = currentMember?.name ?? user?.name; const assignees = useMemo(() => { const names = new Set(versionBugs.map((b) => resolveMemberDisplayName(b.assigneeId, members))); return Array.from(names); }, [versionBugs, members]); const filteredBugs = useMemo(() => { let result = versionBugs; if (filterAssignee === '__me') { if (currentMember) { result = result.filter((b) => isMemberReference(b.assigneeId, currentMember)); } else { result = []; } } else if (filterAssignee) { result = result.filter((b) => resolveMemberDisplayName(b.assigneeId, members) === filterAssignee); } if (filterStatus) result = result.filter((b) => b.status === filterStatus); if (filterSeverity) result = result.filter((b) => b.severity === filterSeverity); if (debouncedKeyword) result = result.filter((b) => matchTitleOrNo({ title: b.title, no: b.bugNo }, debouncedKeyword)); return result; }, [versionBugs, filterAssignee, filterStatus, filterSeverity, debouncedKeyword, currentMember, members]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filteredBugs, 20); const hasFilter = !!(filterAssignee || filterStatus || filterSeverity || debouncedKeyword); return (
Bug {versionBugs.length} 个
待修复 {openCount} · 修复中 {fixingCount} · 已修复 {fixedCount} · 已关闭 {closedCount} {criticalCount > 0 && ( {criticalCount} 致命 )} 解决率 {resolveRate}% {effortMetrics.map((metric) => ( {metric.label} {metric.value} ))}
{ setKeyword(v); setPage(1); }} placeholder="搜索标题/编号" /> { setFilterAssignee(value === 'all' ? '' : value); setPage(1); }} options={[ ...(currentMember ? [{ value: '__me', label: '我的Bug' }] : []), ...assignees.filter((assignee) => assignee !== currentUserName).map((assignee) => ({ value: assignee, label: assignee })), ]} allLabel="全部修复人" /> { setFilterStatus(value === 'all' ? '' : value); setPage(1); }} options={(Object.entries(BUG_STATUS_LABEL) as [BugStatus, string][]).map(([value, label]) => ({ value, label }))} allLabel="全部状态" /> { setFilterSeverity(value === 'all' ? '' : value); setPage(1); }} options={(Object.entries(BUG_SEVERITY_LABEL) as [BugSeverity, string][]).map(([value, label]) => ({ value, label }))} allLabel="全部严重程度" /> {hasFilter && } {filteredBugs.length} 条
{filteredBugs.length === 0 ? (

{hasFilter ? '没有匹配的 Bug' : '暂无 Bug,很好'}

) : (
{paged.map((bug) => { const tc = testCaseMap.get(bug.testCaseId); return setSelectedBugId(bug.id)} />; })}
)} {total > 20 && } {selectedBugId && setSelectedBugId(null)} />}
); }