Files
ftb-project-management/apps/web/lib/bug.ts
2026-06-29 14:18:58 +08:00

123 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<BugStatus, string> = {
open: '待修复',
fixing: '修复中',
fixed: '已修复',
verifying: '验证中',
closed: '已关闭',
rejected: '已拒绝',
};
export const BUG_STATUS_COLOR: Record<BugStatus, string> = {
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<BugSeverity, string> = {
critical: '致命',
major: '严重',
minor: '一般',
trivial: '轻微',
};
export const BUG_SEVERITY_COLOR: Record<BugSeverity, string> = {
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<BugStatus, BugStatus[]> = {
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;
}