484 lines
15 KiB
TypeScript
484 lines
15 KiB
TypeScript
import { api } from './api';
|
|
import type { Bug, BugSeverity, BugStatus } from './bug';
|
|
import type { DevTask, DevTaskStatus, Reference } from './dev-task';
|
|
import type { Priority } from './derive';
|
|
import type { Requirement, RequirementStatus, SourceType } from './requirement';
|
|
import type { TestCase, TestCaseStatus } from './test-case';
|
|
import type { VersionDataScope } from './version-data-scope';
|
|
import type {
|
|
PlanTask,
|
|
VersionPlan,
|
|
VersionPlanLog,
|
|
VersionPlanRequirementCoverage,
|
|
} from './version-plan';
|
|
|
|
type V22DateValue = string | Date | null | undefined;
|
|
|
|
interface V22VersionRow {
|
|
id: string;
|
|
productId: string;
|
|
projectId?: string | null;
|
|
name: string;
|
|
releaseDate?: V22DateValue;
|
|
createdAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}
|
|
|
|
interface V22RequirementRow {
|
|
id: string;
|
|
productId: string;
|
|
projectId?: string | null;
|
|
versionId?: string | null;
|
|
code: string;
|
|
title: string;
|
|
description?: string | null;
|
|
status?: string | null;
|
|
priority?: number | string | null;
|
|
type?: string | null;
|
|
sourceType?: string | null;
|
|
sourceTarget?: string | null;
|
|
platform?: string | null;
|
|
creatorId?: string | null;
|
|
creatorName?: string | null;
|
|
creator?: { id?: string | null; name?: string | null } | null;
|
|
createdAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}
|
|
|
|
interface V22VersionPlanRow {
|
|
id: string;
|
|
versionId: string;
|
|
type: string;
|
|
title: string;
|
|
status?: string | null;
|
|
ownerId?: string | null;
|
|
expectedStartAt?: V22DateValue;
|
|
expectedEndAt?: V22DateValue;
|
|
actualStartAt?: V22DateValue;
|
|
completedAt?: V22DateValue;
|
|
resultUrl?: string | null;
|
|
requirementCoverage?: unknown;
|
|
logs?: unknown;
|
|
createdAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}
|
|
|
|
interface V22DevTaskRow {
|
|
id: string;
|
|
versionId: string;
|
|
requirementId?: string | null;
|
|
categoryId?: string | null;
|
|
code: string;
|
|
title: string;
|
|
description?: string | null;
|
|
status?: string | null;
|
|
priority?: number | string | null;
|
|
assigneeId?: string | null;
|
|
creatorId?: string | null;
|
|
isBlocked?: boolean | null;
|
|
blockReason?: string | null;
|
|
expectedStartAt?: V22DateValue;
|
|
expectedEndAt?: V22DateValue;
|
|
startDate?: V22DateValue;
|
|
completedAt?: V22DateValue;
|
|
estimateHours?: number | null;
|
|
aiEstimateHours?: number | null;
|
|
references?: unknown;
|
|
aiDraft?: boolean | null;
|
|
aiDraftAt?: V22DateValue;
|
|
createdAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}
|
|
|
|
interface V22TestCaseRow {
|
|
id: string;
|
|
versionId: string;
|
|
requirementId?: string | null;
|
|
categoryId?: string | null;
|
|
code: string;
|
|
title: string;
|
|
description?: string | null;
|
|
status?: string | null;
|
|
roundNo?: number | null;
|
|
priority?: number | string | null;
|
|
assigneeId?: string | null;
|
|
creatorId?: string | null;
|
|
plannedTestAt?: V22DateValue;
|
|
plannedEndAt?: V22DateValue;
|
|
startedAt?: V22DateValue;
|
|
completedAt?: V22DateValue;
|
|
estimateHours?: number | null;
|
|
aiEstimateHours?: number | null;
|
|
references?: unknown;
|
|
aiDraft?: boolean | null;
|
|
aiDraftAt?: V22DateValue;
|
|
createdAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}
|
|
|
|
interface V22BugRow {
|
|
id: string;
|
|
versionId: string;
|
|
testCaseId?: string | null;
|
|
code: string;
|
|
title: string;
|
|
description?: string | null;
|
|
status?: string | null;
|
|
severity?: string | null;
|
|
priority?: number | string | null;
|
|
assigneeId?: string | null;
|
|
reporterId?: string | null;
|
|
plannedFixAt?: V22DateValue;
|
|
resolvedAt?: V22DateValue;
|
|
closedAt?: V22DateValue;
|
|
resolution?: string | null;
|
|
createdAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}
|
|
|
|
interface V22VersionDetailResponse {
|
|
version: V22VersionRow;
|
|
requirements: V22RequirementRow[];
|
|
versionPlans: V22VersionPlanRow[];
|
|
devTasks: V22DevTaskRow[];
|
|
testCases: V22TestCaseRow[];
|
|
bugs: V22BugRow[];
|
|
}
|
|
|
|
export interface V22VersionDetailData {
|
|
version: V22VersionRow;
|
|
scope: VersionDataScope;
|
|
}
|
|
|
|
export interface V22RequirementsQuery {
|
|
productId: string;
|
|
projectId?: string;
|
|
versionId?: string;
|
|
status?: string;
|
|
priority?: string;
|
|
type?: string;
|
|
q?: string;
|
|
sort?: string;
|
|
cursor?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
export interface V22RequirementsPage {
|
|
items: Requirement[];
|
|
nextCursor?: string;
|
|
}
|
|
|
|
export interface V22WorkspaceData {
|
|
versionPlans: VersionPlan[];
|
|
devTasks: DevTask[];
|
|
testCases: TestCase[];
|
|
bugs: Bug[];
|
|
}
|
|
|
|
export interface V22XiaobaoWarningQuery {
|
|
userId?: string;
|
|
manager?: boolean;
|
|
}
|
|
|
|
export interface V22XiaobaoWarningSummary {
|
|
versionId: string;
|
|
riskLevel: string;
|
|
riskScore: number;
|
|
confidence: number;
|
|
forecastReleaseDate?: string;
|
|
riskSignature: string;
|
|
summary: unknown;
|
|
dirty?: boolean;
|
|
recomputedAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export async function loadV22VersionDetailData(versionId: string): Promise<V22VersionDetailData> {
|
|
const response = await api.get<V22VersionDetailResponse>(
|
|
`/v2.2/versions/${encodeURIComponent(versionId)}/detail-data`,
|
|
);
|
|
return {
|
|
version: response.version,
|
|
scope: buildScope(response),
|
|
};
|
|
}
|
|
|
|
export async function loadV22RequirementsPage(query: V22RequirementsQuery): Promise<V22RequirementsPage> {
|
|
const response = await api.get<{ items: V22RequirementRow[]; nextCursor?: string }>(
|
|
`/v2.2/requirements?${queryString(query)}`,
|
|
);
|
|
return {
|
|
items: response.items.map(mapRequirement),
|
|
nextCursor: response.nextCursor,
|
|
};
|
|
}
|
|
|
|
export async function loadV22WorkspaceData(userId: string): Promise<V22WorkspaceData> {
|
|
const response = await api.get<{
|
|
versionPlans: V22VersionPlanRow[];
|
|
devTasks: V22DevTaskRow[];
|
|
testCases: V22TestCaseRow[];
|
|
bugs: V22BugRow[];
|
|
}>(`/v2.2/workspace?${queryString({ userId })}`);
|
|
return {
|
|
versionPlans: response.versionPlans.map(mapVersionPlan),
|
|
devTasks: response.devTasks.map(mapDevTask),
|
|
testCases: response.testCases.map(mapTestCase),
|
|
bugs: response.bugs.map(mapBug),
|
|
};
|
|
}
|
|
|
|
export async function loadV22XiaobaoWarnings(
|
|
query: V22XiaobaoWarningQuery,
|
|
): Promise<V22XiaobaoWarningSummary[]> {
|
|
const response = await api.get<Array<Omit<V22XiaobaoWarningSummary, 'forecastReleaseDate' | 'recomputedAt' | 'updatedAt'> & {
|
|
forecastReleaseDate?: V22DateValue;
|
|
recomputedAt?: V22DateValue;
|
|
updatedAt?: V22DateValue;
|
|
}>>(`/v2.2/xiaobao-warning?${queryString({
|
|
userId: query.userId,
|
|
manager: query.manager ? 'true' : undefined,
|
|
})}`);
|
|
return response.map((row) => ({
|
|
...row,
|
|
forecastReleaseDate: optionalIso(row.forecastReleaseDate),
|
|
recomputedAt: optionalIso(row.recomputedAt),
|
|
updatedAt: optionalIso(row.updatedAt),
|
|
}));
|
|
}
|
|
|
|
function buildScope(response: V22VersionDetailResponse): VersionDataScope {
|
|
const requirements = response.requirements.map(mapRequirement);
|
|
const requirementIds = requirements.map((requirement) => requirement.id);
|
|
const requirementIdSet = new Set(requirementIds);
|
|
const plans = response.versionPlans.map(mapVersionPlan);
|
|
const plansByType: VersionDataScope['plansByType'] = { research: [], product: [], ui: [] };
|
|
for (const plan of plans) {
|
|
plansByType[plan.type].push(plan);
|
|
}
|
|
|
|
return {
|
|
requirements,
|
|
requirementIds,
|
|
requirementIdSet,
|
|
plans,
|
|
plansByType,
|
|
devTasks: response.devTasks.map(mapDevTask),
|
|
testCases: response.testCases.map(mapTestCase),
|
|
bugs: response.bugs.map(mapBug),
|
|
overtimeRecords: [],
|
|
};
|
|
}
|
|
|
|
function mapRequirement(row: V22RequirementRow): Requirement {
|
|
return {
|
|
id: row.id,
|
|
code: row.code,
|
|
title: row.title,
|
|
description: row.description ?? '',
|
|
productId: row.productId,
|
|
projectId: row.projectId ?? '',
|
|
versionId: row.versionId ?? undefined,
|
|
sourceType: toSourceType(row.sourceType),
|
|
sourceTarget: row.sourceTarget ?? '',
|
|
platforms: splitCsv(row.platform),
|
|
typeId: row.type ?? '',
|
|
status: toRequirementStatus(row.status),
|
|
priority: toPriority(row.priority),
|
|
effort: 'M',
|
|
creator: row.creator?.name ?? row.creatorName ?? row.creatorId ?? '',
|
|
createdAt: requiredIso(row.createdAt),
|
|
};
|
|
}
|
|
|
|
function mapVersionPlan(row: V22VersionPlanRow): VersionPlan {
|
|
const coverage = asArray<VersionPlanRequirementCoverage>(row.requirementCoverage);
|
|
return {
|
|
id: row.id,
|
|
versionId: row.versionId,
|
|
type: toPlanType(row.type),
|
|
title: row.title,
|
|
owner: row.ownerId ?? '',
|
|
startTime: requiredIso(row.expectedStartAt),
|
|
endTime: requiredIso(row.expectedEndAt),
|
|
status: toPlanStatus(row.status),
|
|
tasks: [],
|
|
completedRequirementIds: coverage
|
|
.filter((item) => item?.status === 'completed')
|
|
.map((item) => item.requirementId)
|
|
.filter(Boolean),
|
|
linkedRequirementIds: coverage.map((item) => item?.requirementId).filter(Boolean),
|
|
requirementCoverage: coverage,
|
|
logs: asArray<VersionPlanLog>(row.logs),
|
|
resultUrl: row.resultUrl ?? undefined,
|
|
actualStartAt: optionalIso(row.actualStartAt),
|
|
createdAt: requiredIso(row.createdAt),
|
|
completedAt: optionalIso(row.completedAt),
|
|
addedBy: row.ownerId ?? '',
|
|
};
|
|
}
|
|
|
|
function mapDevTask(row: V22DevTaskRow): DevTask {
|
|
return {
|
|
id: row.id,
|
|
taskNo: row.code,
|
|
versionId: row.versionId,
|
|
requirementId: row.requirementId ?? '',
|
|
title: row.title,
|
|
description: row.description ?? '',
|
|
categoryId: row.categoryId ?? '',
|
|
assigneeId: row.assigneeId ?? '',
|
|
priority: toPriority(row.priority),
|
|
expectedStartAt: requiredIso(row.expectedStartAt),
|
|
expectedEndAt: requiredIso(row.expectedEndAt),
|
|
estimateHours: row.estimateHours ?? undefined,
|
|
aiEstimateHours: row.aiEstimateHours ?? undefined,
|
|
actualStartAt: optionalIso(row.startDate),
|
|
actualEndAt: optionalIso(row.completedAt),
|
|
status: toDevTaskStatus(row.status),
|
|
isBlocked: row.isBlocked ?? false,
|
|
blockReason: row.blockReason ?? undefined,
|
|
references: asArray<Reference>(row.references),
|
|
aiDraft: row.aiDraft ?? false,
|
|
aiDraftAt: optionalIso(row.aiDraftAt),
|
|
createdBy: row.creatorId ?? '',
|
|
createdAt: requiredIso(row.createdAt),
|
|
updatedAt: requiredIso(row.updatedAt),
|
|
};
|
|
}
|
|
|
|
function mapTestCase(row: V22TestCaseRow): TestCase {
|
|
return {
|
|
id: row.id,
|
|
caseNo: row.code,
|
|
versionId: row.versionId,
|
|
requirementId: row.requirementId ?? undefined,
|
|
roundNo: row.roundNo ?? 1,
|
|
title: row.title,
|
|
description: row.description ?? '',
|
|
categoryId: row.categoryId ?? '',
|
|
priority: toPriority(row.priority),
|
|
assigneeId: row.assigneeId ?? undefined,
|
|
status: toTestCaseStatus(row.status),
|
|
estimateHours: row.estimateHours ?? undefined,
|
|
aiEstimateHours: row.aiEstimateHours ?? undefined,
|
|
plannedTestAt: optionalIso(row.plannedTestAt),
|
|
plannedEndAt: optionalIso(row.plannedEndAt),
|
|
startedAt: optionalIso(row.startedAt),
|
|
completedAt: optionalIso(row.completedAt),
|
|
references: asArray<Reference>(row.references),
|
|
aiDraft: row.aiDraft ?? false,
|
|
aiDraftAt: optionalIso(row.aiDraftAt),
|
|
createdBy: row.creatorId ?? '',
|
|
createdAt: requiredIso(row.createdAt),
|
|
updatedAt: requiredIso(row.updatedAt),
|
|
};
|
|
}
|
|
|
|
function mapBug(row: V22BugRow): Bug {
|
|
return {
|
|
id: row.id,
|
|
bugNo: row.code,
|
|
versionId: row.versionId,
|
|
testCaseId: row.testCaseId ?? '',
|
|
title: row.title,
|
|
description: row.description ?? '',
|
|
severity: toBugSeverity(row.severity),
|
|
priority: toPriority(row.priority),
|
|
reportedBy: row.reporterId ?? '',
|
|
assigneeId: row.assigneeId ?? '',
|
|
status: toBugStatus(row.status),
|
|
resolvedAt: optionalIso(row.resolvedAt),
|
|
closedAt: optionalIso(row.closedAt),
|
|
resolution: row.resolution ?? undefined,
|
|
plannedFixAt: optionalIso(row.plannedFixAt),
|
|
createdAt: requiredIso(row.createdAt),
|
|
updatedAt: requiredIso(row.updatedAt),
|
|
};
|
|
}
|
|
|
|
function queryString<T extends object>(values: T): string {
|
|
const params = new URLSearchParams();
|
|
Object.entries(values).forEach(([key, value]) => {
|
|
if (value === undefined || value === '') return;
|
|
params.set(key, String(value));
|
|
});
|
|
return params.toString();
|
|
}
|
|
|
|
function requiredIso(value: V22DateValue): string {
|
|
return optionalIso(value) ?? '';
|
|
}
|
|
|
|
function optionalIso(value: V22DateValue): string | undefined {
|
|
if (!value) return undefined;
|
|
if (value instanceof Date) return value.toISOString();
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return undefined;
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return `${trimmed}T00:00:00.000Z`;
|
|
const parsed = new Date(trimmed);
|
|
if (Number.isFinite(parsed.getTime())) return parsed.toISOString();
|
|
return trimmed;
|
|
}
|
|
|
|
function splitCsv(value?: string | null): string[] {
|
|
if (!value) return [];
|
|
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
|
|
function asArray<T>(value: unknown): T[] {
|
|
return Array.isArray(value) ? value as T[] : [];
|
|
}
|
|
|
|
function toPriority(value: number | string | null | undefined): Priority {
|
|
if (typeof value === 'string') {
|
|
const normalized = value.trim().toUpperCase();
|
|
if (/^P[0-4]$/.test(normalized)) return normalized as Priority;
|
|
const parsed = Number(normalized);
|
|
if (Number.isFinite(parsed)) return toPriority(parsed);
|
|
}
|
|
const rank = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 2;
|
|
return `P${Math.max(0, Math.min(4, rank))}` as Priority;
|
|
}
|
|
|
|
function toSourceType(value?: string | null): SourceType {
|
|
const allowed: SourceType[] = ['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management'];
|
|
return allowed.includes(value as SourceType) ? value as SourceType : 'internal';
|
|
}
|
|
|
|
function toRequirementStatus(value?: string | null): RequirementStatus {
|
|
const allowed: RequirementStatus[] = ['pending_review', 'adopted', 'rejected', 'planned', 'developing', 'testing', 'released', 'closed'];
|
|
return allowed.includes(value as RequirementStatus) ? value as RequirementStatus : 'pending_review';
|
|
}
|
|
|
|
function toPlanType(value: string): VersionPlan['type'] {
|
|
return value === 'research' || value === 'ui' ? value : 'product';
|
|
}
|
|
|
|
function toPlanStatus(value?: string | null): VersionPlan['status'] {
|
|
if (value === 'in_progress' || value === 'completed') return value;
|
|
return 'pending';
|
|
}
|
|
|
|
function toDevTaskStatus(value?: string | null): DevTaskStatus {
|
|
if (value === 'in_progress' || value === 'testing' || value === 'submitted') return value;
|
|
return 'todo';
|
|
}
|
|
|
|
function toTestCaseStatus(value?: string | null): TestCaseStatus {
|
|
if (value === 'running' || value === 'passed' || value === 'failed' || value === 'blocked') return value;
|
|
return 'pending';
|
|
}
|
|
|
|
function toBugStatus(value?: string | null): BugStatus {
|
|
if (value === 'fixing' || value === 'fixed' || value === 'verifying' || value === 'closed' || value === 'rejected') {
|
|
return value;
|
|
}
|
|
return 'open';
|
|
}
|
|
|
|
function toBugSeverity(value?: string | null): BugSeverity {
|
|
if (value === 'critical' || value === 'major' || value === 'trivial') return value;
|
|
return 'minor';
|
|
}
|