Files
admin-web/src/modules/sailing-calendar/api/platformSailingCalendar.ts
T

205 lines
13 KiB
TypeScript

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<SailingCalendarEntry['volumeSummary']> | 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<SailingCalendarResponse>, ApiEnvelope<SailingCalendarResponse>>(`${basePath}/calendar`, { params: query })
return calendar(unwrapApiResponse(response))
}
export async function getSailing(id: string) {
const response = await http.get<ApiEnvelope<SailingDetail>, ApiEnvelope<SailingDetail>>(`${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<SailingDetail>, ApiEnvelope<SailingDetail>>(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<SailingDetail>, ApiEnvelope<SailingDetail>>(`${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<SailingDetail>, ApiEnvelope<SailingDetail>>(`${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<SailingDetail>, ApiEnvelope<SailingDetail>>(`${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<SailingVolumeDetail>, ApiEnvelope<SailingVolumeDetail>>(`${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<SailingBookingAdvice>, ApiEnvelope<SailingBookingAdvice>>(`${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<unknown>, ApiEnvelope<unknown>>(`${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<SailingChannelResource[]>, ApiEnvelope<SailingChannelResource[]>>(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<SailingChannelResource>, ApiEnvelope<SailingChannelResource>>(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<SailingChannelResource>, ApiEnvelope<SailingChannelResource>>(`${channelPath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-channel') } })
return channel(unwrapApiResponse(response))
}
export async function listSailingTemplates() {
const response = await http.get<ApiEnvelope<SailingTemplateResource[]>, ApiEnvelope<SailingTemplateResource[]>>(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<SailingTemplateResource>, ApiEnvelope<SailingTemplateResource>>(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<SailingTemplateResource>, ApiEnvelope<SailingTemplateResource>>(`${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<SailingGenerationPreview>, ApiEnvelope<SailingGenerationPreview>>(`${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<SailingGenerationResult>, ApiEnvelope<SailingGenerationResult>>(`${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<SailingExceptionPage>, ApiEnvelope<SailingExceptionPage>>(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<SailingExceptionReassignResult>, ApiEnvelope<SailingExceptionReassignResult>>(`${basePath}/${encodeURIComponent(id)}/exception-reassign`, payload, { headers: { 'Idempotency-Key': idempotencyKey('sailing-exception-reassign') } })
return unwrapApiResponse(response)
}