import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { SailingAssignment, SailingBookingAdvice, SailingBookingLinkRequest, SailingCalendarEntry, SailingCalendarQuery, SailingCalendarResponse, SailingChannelResource, SailingChannelSaveRequest, SailingDetail, SailingExceptionPage, SailingExceptionQuery, SailingExceptionRecord, SailingExceptionReassignRequest, SailingExceptionReassignResult, SailingGenerationPreview, SailingGenerationResult, SailingSaveRequest, SailingTemplateResource, SailingTemplateSaveRequest, SailingVolumeDetail } from '../types' const basePath = '/platform/v1/sailings' function number(value: unknown, fallback = 0) { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : fallback } function entry(raw: SailingCalendarEntry): SailingCalendarEntry { return { ...raw, id: String(raw.id), sailingNo: String(raw.sailingNo ?? ''), warehouseId: String(raw.warehouseId ?? ''), channelCode: String(raw.channelCode ?? ''), channelName: String(raw.channelName ?? raw.channelCode ?? ''), templateId: raw.templateId == null ? null : String(raw.templateId), channelId: raw.channelId == null ? null : String(raw.channelId), etaAt: raw.etaAt ?? null, sourceType: raw.sourceType ?? null, plannedCapacityCbm: raw.plannedCapacityCbm == null ? null : number(raw.plannedCapacityCbm), volumeSummary: summary(raw.volumeSummary), version: number(raw.version), } } function summary(raw?: Partial | null) { return { actualReceivedCbm: number(raw?.actualReceivedCbm), catchableForecastCbm: number(raw?.catchableForecastCbm), riskUnmatchedCbm: number(raw?.riskUnmatchedCbm), bookedContainerCount: number(raw?.bookedContainerCount), recommendedContainerCount: number(raw?.recommendedContainerCount), pendingContainerCount: number(raw?.pendingContainerCount), lateBatchCount: number(raw?.lateBatchCount), overCapacity: Boolean(raw?.overCapacity), planningCapacityCbm: raw?.planningCapacityCbm == null ? null : number(raw.planningCapacityCbm), bookedPlanningCapacityCbm: raw?.bookedPlanningCapacityCbm == null ? null : number(raw.bookedPlanningCapacityCbm), catchableReceivedCbm: raw?.catchableReceivedCbm == null ? null : number(raw.catchableReceivedCbm), lateReceivedCbm: raw?.lateReceivedCbm == null ? null : number(raw.lateReceivedCbm), snapshotAt: raw?.snapshotAt ?? null, } } function assignment(raw: SailingAssignment): SailingAssignment { return { ...raw, id: String(raw.id), volumeCbm: number(raw.volumeCbm), assignmentVersion: raw.assignmentVersion == null ? null : number(raw.assignmentVersion), } } function history(raw: SailingDetail['histories'][number]): SailingDetail['histories'][number] { return { ...raw, id: String(raw.id) } } function calendar(raw: SailingCalendarResponse): SailingCalendarResponse { const source = raw || {} as SailingCalendarResponse const rawSummary = source.summary || {} as SailingCalendarResponse['summary'] return { ...source, records: (source.records || []).map(entry), summary: { ...summary(rawSummary), sailingCount: number(rawSummary.sailingCount) }, filterOptions: { warehouses: source.filterOptions?.warehouses || [], channels: source.filterOptions?.channels || [], departurePorts: source.filterOptions?.departurePorts || [], destinationPorts: source.filterOptions?.destinationPorts || [], }, } } export async function listSailingCalendar(query: SailingCalendarQuery) { const response = await http.get, ApiEnvelope>(`${basePath}/calendar`, { params: query }) return calendar(unwrapApiResponse(response)) } export async function getSailing(id: string) { const response = await http.get, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}`) const result = unwrapApiResponse(response) return { ...result, sailing: entry(result.sailing), histories: (result.histories || []).map(history) } } function idempotencyKey(prefix: string) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}` } export async function createSailing(payload: SailingSaveRequest) { const response = await http.post, ApiEnvelope>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing') } }) const result = unwrapApiResponse(response) return { ...result, sailing: entry(result.sailing), histories: (result.histories || []).map(history) } } export async function updateSailing(id: string, payload: SailingSaveRequest) { const response = await http.put, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing') } }) const result = unwrapApiResponse(response) return { ...result, sailing: entry(result.sailing), histories: (result.histories || []).map(history) } } export async function cancelSailing(id: string, expectedVersion: number, reason: string) { const response = await http.post, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}/cancel`, { expectedVersion, reason }, { headers: { 'Idempotency-Key': idempotencyKey('sailing-cancel') } }) const result = unwrapApiResponse(response) return { ...result, sailing: entry(result.sailing), histories: (result.histories || []).map(history) } } export async function copySailing(id: string, payload: { etdAt: string; cutoffAt: string }) { const response = await http.post, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}/copy`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-copy') } }) const result = unwrapApiResponse(response) return { ...result, sailing: entry(result.sailing), histories: (result.histories || []).map(history) } } export async function getSailingVolumeDetail(id: string) { const response = await http.get, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}/volume-summary`) const result = unwrapApiResponse(response) return { ...result, summary: summary(result.summary), assignments: (result.assignments || []).map(assignment) } } export async function getSailingBookingAdvice(id: string) { const response = await http.get, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}/booking-advice`) const result = unwrapApiResponse(response) return { ...result, actualReceivedCbm: number(result.actualReceivedCbm), catchableTotalCbm: number(result.catchableTotalCbm), planningCapacityCbm: result.planningCapacityCbm == null ? null : number(result.planningCapacityCbm), bookedContainerCount: number(result.bookedContainerCount), recommendedContainerCount: number(result.recommendedContainerCount), pendingContainerCount: number(result.pendingContainerCount), catchableReceivedCbm: result.catchableReceivedCbm == null ? null : number(result.catchableReceivedCbm), lateReceivedCbm: result.lateReceivedCbm == null ? null : number(result.lateReceivedCbm), scenarios: (result.scenarios || []).map((scenario) => ({ ...scenario, capacityCbm: number(scenario.capacityCbm), recommendedContainerCount: number(scenario.recommendedContainerCount), pendingContainerCount: number(scenario.pendingContainerCount) })), } } export async function createSailingBookingLink(id: string, payload: SailingBookingLinkRequest) { const response = await http.post, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}/booking-links`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-booking-link') } }) return unwrapApiResponse(response) } const channelPath = '/platform/v1/sailing-channels' const templatePath = '/platform/v1/sailing-templates' const exceptionPath = '/platform/v1/sailing-assignments/exceptions' function channel(raw: SailingChannelResource): SailingChannelResource { return { ...raw, id: String(raw.id), defaultCutoffDays: number(raw.defaultCutoffDays), version: number(raw.version) } } function template(raw: SailingTemplateResource): SailingTemplateResource { return { ...raw, id: String(raw.id), channelId: String(raw.channelId), warehouseId: String(raw.warehouseId), weekdays: (raw.weekdays || []).map((value) => number(value)), cutoffDays: number(raw.cutoffDays), version: number(raw.version) } } export async function listSailingChannels() { const response = await http.get, ApiEnvelope>(channelPath) const result = unwrapApiResponse(response) as SailingChannelResource[] | { records?: SailingChannelResource[] } | null return (Array.isArray(result) ? result : result?.records || []).map(channel) } export async function createSailingChannel(payload: SailingChannelSaveRequest) { const response = await http.post, ApiEnvelope>(channelPath, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-channel') } }) return channel(unwrapApiResponse(response)) } export async function updateSailingChannel(id: string, payload: SailingChannelSaveRequest) { const response = await http.put, ApiEnvelope>(`${channelPath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-channel') } }) return channel(unwrapApiResponse(response)) } export async function listSailingTemplates() { const response = await http.get, ApiEnvelope>(templatePath, { params: { page: 1, size: 200 } }) const result = unwrapApiResponse(response) as SailingTemplateResource[] | { records?: SailingTemplateResource[] } | null return (Array.isArray(result) ? result : result?.records || []).map(template) } export async function createSailingTemplate(payload: SailingTemplateSaveRequest) { const response = await http.post, ApiEnvelope>(templatePath, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-template') } }) return template(unwrapApiResponse(response)) } export async function updateSailingTemplate(id: string, payload: SailingTemplateSaveRequest) { const response = await http.put, ApiEnvelope>(`${templatePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-template') } }) return template(unwrapApiResponse(response)) } export async function previewSailingTemplateGeneration(id: string, payload: { from: string; to: string }) { const response = await http.post, ApiEnvelope>(`${templatePath}/${encodeURIComponent(id)}/generation-preview`, payload) return unwrapApiResponse(response) } export async function generateSailingTemplate(id: string, payload: { from: string; to: string }) { const response = await http.post, ApiEnvelope>(`${templatePath}/${encodeURIComponent(id)}/generate`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-template-generate') } }) return unwrapApiResponse(response) } function exceptionRecord(raw: SailingExceptionRecord): SailingExceptionRecord { return { ...raw, assignmentId: String(raw.assignmentId), volumeCbm: number(raw.volumeCbm), waitingDays: raw.waitingDays == null ? null : number(raw.waitingDays), assignmentVersion: raw.assignmentVersion == null ? null : number(raw.assignmentVersion), } } export async function listSailingExceptions(query: SailingExceptionQuery = {}) { const response = await http.get, ApiEnvelope>(exceptionPath, { params: query }) const raw = unwrapApiResponse(response) as SailingExceptionPage | SailingExceptionRecord[] | null if (Array.isArray(raw)) return { records: raw.map(exceptionRecord), page: 1, size: raw.length, total: raw.length, pages: raw.length ? 1 : 0 } return { ...(raw || { records: [], page: 1, size: 20, total: 0, pages: 0 }), records: (raw?.records || []).map(exceptionRecord) } } /** Restricted exception reassignment; the server validates cutoff, route and warehouse scope. */ export async function reassignSailingAssignment(id: string, payload: SailingExceptionReassignRequest) { const response = await http.post, ApiEnvelope>(`${basePath}/${encodeURIComponent(id)}/exception-reassign`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-exception-reassign') } }) return unwrapApiResponse(response) }