123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
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;
|
||
}
|