import type { Priority } from './derive'; import { calcActualElapsedHours, type TimeInterval } from './work-hours'; export type BugStatus = 'open' | 'fixing' | 'fixed' | 'verifying' | 'closed' | 'rejected'; export type BugSeverity = 'critical' | 'major' | 'minor' | 'trivial'; export interface BugLog { id: string; action: 'create' | 'status_change' | 'transfer' | 'resolve'; fromValue?: string; toValue?: string; operator: string; remark?: string; createdAt: string; } export interface Bug { id: string; bugNo: string; versionId: string; testCaseId: string; requirementId?: string; title: string; description: string; severity: BugSeverity; priority: Priority; reportedBy: string; assigneeId: string; status: BugStatus; images?: string[]; logs?: BugLog[]; resolvedAt?: string; closedAt?: string; resolution?: string; estimateHours?: number; aiEstimateHours?: number; plannedFixAt?: string; createdAt: string; updatedAt: string; } export const BUG_STATUS_LABEL: Record = { open: '待修复', fixing: '修复中', fixed: '已修复', verifying: '验证中', closed: '已关闭', rejected: '已拒绝', }; export const BUG_STATUS_COLOR: Record = { open: 'bg-red-50 text-red-600', fixing: 'bg-blue-50 text-blue-600', fixed: 'bg-indigo-50 text-indigo-600', verifying: 'bg-purple-50 text-purple-600', closed: 'bg-emerald-50 text-emerald-600', rejected: 'bg-zinc-100 text-zinc-500', }; export const BUG_SEVERITY_LABEL: Record = { critical: '致命', major: '严重', minor: '一般', trivial: '轻微', }; export const BUG_SEVERITY_COLOR: Record = { critical: 'bg-red-100 text-red-700', major: 'bg-orange-50 text-orange-700', minor: 'bg-yellow-50 text-yellow-700', trivial: 'bg-zinc-100 text-zinc-600', }; export const BUG_ALLOWED_TRANSITIONS: Record = { open: ['fixing', 'rejected'], fixing: ['fixed'], fixed: ['verifying'], verifying: ['closed', 'open'], closed: [], rejected: [], }; export function canBugTransition(from: BugStatus, to: BugStatus): boolean { return BUG_ALLOWED_TRANSITIONS[from].includes(to); } export function generateBugNo(existingBugs: Bug[]): string { const maxNum = existingBugs.reduce((max, b) => { const num = parseInt(b.bugNo.replace('BUG-', ''), 10); return isNaN(num) ? max : Math.max(max, num); }, 0); return `BUG-${String(maxNum + 1).padStart(3, '0')}`; } export function getBugActualHours(bug: Bug, now: Date = new Date()): number { const start = bug.createdAt; if (!start) return 0; const end = bug.closedAt ?? bug.resolvedAt ?? (bug.status === 'closed' || bug.status === 'rejected' ? bug.updatedAt : now.toISOString()); return calcActualElapsedHours(start, end); } export function aggregateBugActualHours(bugs: Bug[], now: Date = new Date()): number { let sum = 0; for (const b of bugs) sum += getBugActualHours(b, now); return Math.round(sum * 2) / 2; } /** * 抽取每条 Bug 的 [createdAt, closedAt ?? resolvedAt ?? (终态 updatedAt) ?? now] 时间区间 * 用于双口径耗时统计(calcTwoMetrics) */ export function bugIntervals(bugs: Bug[], now: Date = new Date()): TimeInterval[] { const out: TimeInterval[] = []; const nowIso = now.toISOString(); for (const b of bugs) { if (!b.createdAt) continue; const isTerminal = b.status === 'closed' || b.status === 'rejected'; const end = b.closedAt ?? b.resolvedAt ?? (isTerminal ? b.updatedAt : nowIso); out.push({ start: b.createdAt, end }); } return out; }