'use client'; import { useState, useMemo, useEffect } from 'react'; import { Plus, Code2, Trash2 } from 'lucide-react'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { DevTaskRow } from './DevTaskRow'; import { DevTaskCreateModal } from './DevTaskCreateModal'; import { DevTaskDetailDrawer } from './DevTaskDetailDrawer'; import { calcGroupProgress, DEV_TASK_STATUS_LABEL, aggregateDevTaskHours, devTaskIntervals } from '@/lib/dev-task'; import { calcTwoMetrics } from '@/lib/work-hours'; import { Pagination, usePagination } from '@/components/Pagination'; import { FilterSelect } from '@/components/FilterSelect'; import { SearchInput, matchTitleOrNo } from '@/components/SearchInput'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { orderByRequirementForGrouping } from '@/lib/requirement-grouping'; import { buildEffortSummaryMetrics, sumPositiveHours } from '@/lib/effort-summary'; import type { DevTask, DevTaskStatus } from '@/lib/dev-task'; import type { Requirement } from '@/lib/requirement'; interface Props { versionId: string; requirementIds: string[]; versionDeadline?: string; readOnly?: boolean; versionTasks?: DevTask[]; versionRequirements?: Requirement[]; } const NO_REQUIREMENT_GROUP_PREFIX = '__no_requirement__'; function getDevTaskGroupKey(task: DevTask): string { return task.requirementId || `${NO_REQUIREMENT_GROUP_PREFIX}${task.requirementName || '未命名需求'}`; } function getNoRequirementGroupName(groupKey: string): string { return groupKey.slice(NO_REQUIREMENT_GROUP_PREFIX.length) || '未命名需求'; } export function DevTaskTab({ versionId, requirementIds, versionDeadline, readOnly = false, versionTasks: scopedVersionTasks, versionRequirements }: Props) { const { tasks, fetchTasks, deleteTask } = useDevTaskStore(); const { categories, fetchCategories } = useTaskCategoryStore(); const { requirements } = useRequirementStore(); const { members } = useMemberStore(); const user = useAuthStore((s) => s.user); useEffect(() => { if (!scopedVersionTasks) fetchTasks({ versionId }); }, [fetchTasks, scopedVersionTasks, versionId]); useEffect(() => { fetchCategories(); }, [fetchCategories]); const reqIdSet = useMemo(() => new Set(requirementIds), [requirementIds]); const fallbackVersionTasks = useMemo( () => tasks.filter((t) => t.versionId === versionId || (!t.versionId && reqIdSet.has(t.requirementId))), [tasks, reqIdSet, versionId], ); const versionTasks = scopedVersionTasks ?? fallbackVersionTasks; const [filterAssignee, setFilterAssignee] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [filterBlocked, setFilterBlocked] = useState(''); const [filterCategory, setFilterCategory] = useState(''); const [keyword, setKeyword] = useState(''); const debouncedKeyword = useDebouncedValue(keyword, 300); const filteredTasks = useMemo(() => { let result = versionTasks; if (filterAssignee) result = result.filter((t) => t.assigneeId === filterAssignee); if (filterStatus) result = result.filter((t) => t.status === filterStatus); if (filterBlocked === 'yes') result = result.filter((t) => t.isBlocked); if (filterBlocked === 'no') result = result.filter((t) => !t.isBlocked); if (filterCategory) result = result.filter((t) => t.categoryId === filterCategory); if (debouncedKeyword) result = result.filter((t) => matchTitleOrNo({ title: t.title, no: t.taskNo }, debouncedKeyword)); return result; }, [versionTasks, filterAssignee, filterStatus, filterBlocked, filterCategory, debouncedKeyword]); const orderedTasks = useMemo( () => orderByRequirementForGrouping(filteredTasks, requirementIds, (task) => task.requirementId), [filteredTasks, requirementIds], ); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(orderedTasks, 20); const progress = calcGroupProgress(versionTasks); const { actual: actualHours } = aggregateDevTaskHours(versionTasks); const { manhours } = useMemo(() => calcTwoMetrics(devTaskIntervals(versionTasks)), [versionTasks]); const effortMetrics = useMemo( () => buildEffortSummaryMetrics({ aiEstimateHours: sumPositiveHours(versionTasks, (task) => task.aiEstimateHours), estimateHours: sumPositiveHours(versionTasks, (task) => task.estimateHours), actualHours, manhours, }), [versionTasks, actualHours, manhours], ); const blockedCount = versionTasks.filter((t) => t.isBlocked).length; const [showCreate, setShowCreate] = useState(false); const [selectedTaskId, setSelectedTaskId] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); const toggleSelect = (id: string) => { if (readOnly) return; const next = new Set(selectedIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedIds(next); }; const handleBatchDelete = () => { if (readOnly) return; if (selectedIds.size === 0) return; if (!confirm(`确定删除选中的 ${selectedIds.size} 个任务?`)) return; selectedIds.forEach((id) => deleteTask(id)); setSelectedIds(new Set()); }; const groupedByReq = useMemo(() => { const map = new Map(); for (const t of paged) { const key = getDevTaskGroupKey(t); const list = map.get(key) || []; list.push(t); map.set(key, list); } return map; }, [paged]); const categoryMap = useMemo(() => new Map(categories.map((c) => [c.id, c])), [categories]); const categoryLabelWidthEm = useMemo( () => Math.max(4, ...categories.map((category) => category.name.length)) + 1, [categories], ); const visibleRequirements = versionRequirements ?? requirements; const requirementMap = useMemo(() => new Map(visibleRequirements.map((r) => [r.id, r])), [visibleRequirements]); const allTaskIds = useMemo(() => versionTasks.map((t) => t.id), [versionTasks]); const assignees = useMemo(() => Array.from(new Set(versionTasks.map((t) => t.assigneeId))), [versionTasks]); const hasFilter = !!(filterAssignee || filterStatus || filterBlocked || filterCategory || debouncedKeyword); return (
{progress}%
{versionTasks.length}任务 {effortMetrics.map((metric) => ( {metric.label} {metric.value} ))} {blockedCount > 0 && {blockedCount}阻塞} { setKeyword(v); setPage(1); }} placeholder="搜索标题/编号" /> { setFilterAssignee(value === 'all' ? '' : value); setPage(1); }} options={[ ...(user?.name ? [{ value: user.name, label: '我的' }] : []), ...assignees.filter((a) => a !== user?.name).map((a) => ({ value: a, label: a })), ]} allLabel="负责人" /> { setFilterStatus(value === 'all' ? '' : value); setPage(1); }} options={(Object.entries(DEV_TASK_STATUS_LABEL) as [DevTaskStatus, string][]).map(([value, label]) => ({ value, label }))} allLabel="状态" /> { setFilterBlocked(value === 'all' ? '' : value); setPage(1); }} options={[ { value: 'yes', label: '阻塞中' }, { value: 'no', label: '正常' }, ]} allLabel="阻塞" /> { setFilterCategory(value === 'all' ? '' : value); setPage(1); }} options={categories.map((category) => ({ value: category.id, label: category.name }))} allLabel="类型" /> {hasFilter && }
{selectedIds.size > 0 && !readOnly && ( )} {!readOnly && ( )}
{filteredTasks.length === 0 ? (

{hasFilter ? '没有匹配的任务' : '暂无开发任务'}

{!hasFilter && !readOnly && }
) : ( Array.from(groupedByReq.entries()).map(([reqId, reqTasks]) => { const isNoRequirementGroup = reqId.startsWith(NO_REQUIREMENT_GROUP_PREFIX); const req = isNoRequirementGroup ? undefined : requirementMap.get(reqId); const noRequirementName = isNoRequirementGroup ? getNoRequirementGroupName(reqId) : ''; const reqProgress = calcGroupProgress(reqTasks); return (
{!readOnly && (
selectedIds.has(t.id))} onChange={() => { const ids = reqTasks.map((t) => t.id); const allSelected = ids.every((id) => selectedIds.has(id)); const next = new Set(selectedIds); if (allSelected) ids.forEach((id) => next.delete(id)); else ids.forEach((id) => next.add(id)); setSelectedIds(next); }} className="h-3.5 w-3.5 rounded border-[var(--line)]" />
)}
{isNoRequirementGroup ? '无需求ID' : req?.code} {isNoRequirementGroup ? noRequirementName : req?.title} {reqProgress}%
{reqTasks.map((t) => (
{!readOnly && (
toggleSelect(t.id)} className="h-3.5 w-3.5 rounded border-[var(--line)]" onClick={(e) => e.stopPropagation()} />
)}
setSelectedTaskId(t.id)} />
))}
); }) )} {total > 20 && } {showCreate && !readOnly && ( setShowCreate(false)} /> )} {selectedTaskId && setSelectedTaskId(null)} />}
); }