Files
ftb-project-management/apps/web/lib/v22-api.ts

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';
}