import { api } from './api'; import type { Priority } from './derive'; import type { Requirement, RequirementStatus, SourceType } from './requirement'; export interface RootProject { id: string; name: string; description: string; createdAt: string; } export interface RootVersion { id: string; productId?: string; projectId?: string | null; name: string; status?: string; releaseDate: string | null; createdAt: string; currentStage?: string | null; startDate?: string | null; expectedReleaseDate?: string | null; members?: unknown[]; progress?: unknown[]; priority?: string; links?: unknown; } export interface RootProduct { id: string; name: string; description: string; createdAt: string; updatedAt: string; projects: RootProject[]; versions: RootVersion[]; _count?: { requirements: number; projects: number; versions?: number }; } export async function createProductRoot(data: { name: string; description?: string }): Promise { const product = await api.post>('/products', data); return normalizeProductRoot({ ...product, projects: [], versions: [] }); } export async function updateProductRoot( productId: string, data: { name?: string; description?: string }, ): Promise> { return api.patch>(`/products/${productId}`, data); } export async function deleteProductRoot(productId: string): Promise { await api.delete(`/products/${productId}`); } export async function createProjectByProductId( productId: string, data: { name: string; description?: string }, ): Promise { return api.post(`/products/${productId}/projects`, data); } export async function updateProjectByProductId( productId: string, projectId: string, data: { name?: string; description?: string }, ): Promise { return api.patch(`/products/${productId}/projects/${projectId}`, data); } export async function deleteProjectByProductId(productId: string, projectId: string): Promise { await api.delete(`/products/${productId}/projects/${projectId}`); } export async function createVersionByProductId( productId: string, data: { name: string; status?: string; projectId?: string }, ): Promise { return normalizeVersionRoot(await api.post(`/products/${productId}/versions`, data)); } export async function updateVersionByProductId( productId: string, versionId: string, data: Record, ): Promise { return normalizeVersionRoot(await api.patch(`/products/${productId}/versions/${versionId}`, data)); } export async function deleteVersionByProductId(productId: string, versionId: string): Promise { await api.delete(`/products/${productId}/versions/${versionId}`); } interface DomainRequirementRow { id: string; productId: string; projectId?: string | null; versionId?: string | null; code: string; title: string; description?: string | null; status?: string | null; priority?: string | number | 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?: string | Date | null; } export async function createRequirementByProductId( productId: string, data: Partial, ): Promise { return normalizeRequirement(await api.post( `/products/${productId}/requirements`, toRequirementPayload(data), )); } export async function updateRequirementByProductId( productId: string, requirementId: string, data: Partial, ): Promise { return normalizeRequirement(await api.patch( `/products/${productId}/requirements/${requirementId}`, toRequirementPayload(data), )); } export async function updateRequirementStatusByProductId( productId: string, requirementId: string, status: RequirementStatus, ): Promise { return normalizeRequirement(await api.patch( `/products/${productId}/requirements/${requirementId}/status`, { status }, )); } export async function deleteRequirementByProductId(productId: string, requirementId: string): Promise { await api.delete(`/products/${productId}/requirements/${requirementId}`); } function normalizeProductRoot(product: RootProduct): RootProduct { return { ...product, projects: product.projects ?? [], versions: (product.versions ?? []).map(normalizeVersionRoot), _count: product._count ?? { requirements: 0, projects: product.projects?.length ?? 0, versions: product.versions?.length ?? 0 }, }; } function normalizeVersionRoot(version: RootVersion): RootVersion { return { ...version, status: version.status ?? 'planned', releaseDate: version.releaseDate ?? null, members: Array.isArray(version.members) ? version.members : [], progress: Array.isArray(version.progress) ? version.progress : [], links: version.links && typeof version.links === 'object' ? version.links : {}, }; } function toRequirementPayload(data: Partial) { return { ...(data.code !== undefined && { code: data.code }), ...(data.title !== undefined && { title: data.title }), ...(data.description !== undefined && { description: data.description }), ...(data.projectId !== undefined && { projectId: data.projectId }), ...(data.versionId !== undefined && { versionId: data.versionId ?? null }), ...(data.typeId !== undefined && { type: data.typeId }), ...(data.sourceType !== undefined && { sourceType: data.sourceType }), ...(data.sourceTarget !== undefined && { sourceTarget: data.sourceTarget }), ...(data.platforms !== undefined && { platform: data.platforms.join(',') }), ...(data.priority !== undefined && { priority: priorityToNumber(data.priority) }), }; } function normalizeRequirement(row: DomainRequirementRow): 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: isoString(row.createdAt), }; } function toSourceType(value: string | null | undefined): SourceType { const allowed = new Set(['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management']); return allowed.has(value as SourceType) ? value as SourceType : 'internal'; } function toRequirementStatus(value: string | null | undefined): RequirementStatus { const allowed = new Set(['pending_review', 'adopted', 'rejected', 'planned', 'developing', 'testing', 'released', 'closed']); return allowed.has(value as RequirementStatus) ? value as RequirementStatus : 'pending_review'; } function toPriority(value: string | number | null | undefined): Priority { if (typeof value === 'string' && /^P[0-4]$/.test(value)) return value as Priority; const parsed = Number(value ?? 2); const normalized = Number.isFinite(parsed) ? Math.max(0, Math.min(4, Math.floor(parsed))) : 2; return `P${normalized}` as Priority; } function priorityToNumber(value: Priority | undefined): number | undefined { if (!value) return undefined; const match = /^P([0-4])$/.exec(value); return match ? Number(match[1]) : undefined; } function splitCsv(value: string | null | undefined): string[] { if (!value) return []; return value.split(',').map((item) => item.trim()).filter(Boolean); } function isoString(value: string | Date | null | undefined): string { if (!value) return new Date().toISOString(); if (value instanceof Date) return value.toISOString(); const time = new Date(value).getTime(); return Number.isFinite(time) ? new Date(time).toISOString() : new Date().toISOString(); }