Files
admin-web/src/modules/container-loading/api/terminalDrayages.ts
T

148 lines
7.4 KiB
TypeScript
Raw Normal View History

import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type TerminalDrayageStatus = 'WAITING_FOR_ARRIVAL' | 'ARRIVED' | 'PICKUP_SCHEDULED' | 'PICKED_UP' | 'UNLOADED' | 'EMPTY_RETURNED'
2026-09-15 14:18:23 +08:00
export type TerminalDrayageAction = 'SELECT_FLEET' | 'SCHEDULE_PICKUP' | 'RECORD_EMPTY_RETURN' | 'SEND_PICKUP_EMAIL'
2026-09-09 13:54:43 +08:00
export interface TerminalDrayageEmail {
id: string
fleetName: string
recipient: string
kind: 'REQUEST_PICKUP' | 'CANCEL_PICKUP'
subject: string
body: string
status: 'PENDING' | 'SENDING' | 'SENT' | 'FAILED'
attempts: number
failureReason: string | null
createdAt: string
sentAt: string | null
}
export async function getTerminalDrayageEmails(containerId: string) {
type Mailbox = { deliveryEnabled: boolean; records: TerminalDrayageEmail[] }
const response = await http.get<ApiEnvelope<Mailbox>, ApiEnvelope<Mailbox>>(`${path}/${encodeURIComponent(containerId)}/emails`)
const result = unwrapApiResponse(response)
return { ...result, records: result.records.map((mail) => ({ ...mail, id: String(mail.id) })) }
}
export function sendTerminalDrayageEmail(containerId: string, expectedVersion: number) {
return command(containerId, 'pickup-email', { expectedVersion })
}
export async function retryTerminalDrayageEmail(containerId: string, mailId: string) {
const response = await http.post<ApiEnvelope<null>, ApiEnvelope<null>>(`${path}/${encodeURIComponent(containerId)}/emails/${encodeURIComponent(mailId)}/retry`)
unwrapApiResponse(response)
}
export type EmptyReturnWarningStatus = 'NOT_APPLICABLE' | 'PENDING' | 'OVERDUE' | 'RESOLVED'
export interface EmptyReturnWarning {
status: EmptyReturnWarningStatus
deadlineAt: string | null
overdueHours: number
overdueDays: number
}
export interface TerminalDrayageView {
containerBookingId: string
containerNo: string | null
oceanBookingId: string | null
status: TerminalDrayageStatus
version: number
fleetSupplierId: string | null
fleetSupplierName: string | null
pickupAppointmentAt: string | null
actualPickupAt: string | null
estimatedEmptyReturnAt: string | null
actualEmptyReturnAt: string | null
unloadingCompletedAt: string | null
emptyReturnWarning: EmptyReturnWarning
allowedActions: TerminalDrayageAction[]
updatedAt: string | null
}
export interface TerminalDrayageHistory {
action: TerminalDrayageAction
fromStatus: string
toStatus: string
fromVersion: number
toVersion: number
reason: string | null
operatorId: string
operatorName: string | null
createdAt: string
}
const path = '/platform/v1/terminal-drayages'
function operationId(action: string) { return `terminal-drayage-${action}-${Date.now()}-${Math.random().toString(36).slice(2)}` }
function id(value: unknown): string { return value == null ? '' : String(value) }
function optionalId(value: unknown): string | null { return value == null ? null : String(value) }
function normalize(raw: TerminalDrayageView): TerminalDrayageView {
return {
...raw,
containerBookingId: id(raw.containerBookingId),
oceanBookingId: optionalId(raw.oceanBookingId),
fleetSupplierId: optionalId(raw.fleetSupplierId),
version: Number(raw.version || 0),
allowedActions: raw.allowedActions || [],
emptyReturnWarning: {
...raw.emptyReturnWarning,
overdueHours: Number(raw.emptyReturnWarning?.overdueHours || 0),
overdueDays: Number(raw.emptyReturnWarning?.overdueDays || 0),
},
}
}
export async function listTerminalDrayages(containerBookingIds: string[]) {
const params = new URLSearchParams()
containerBookingIds.forEach((containerBookingId) => params.append('containerBookingIds', containerBookingId))
const response = await http.get<ApiEnvelope<TerminalDrayageView[]>, ApiEnvelope<TerminalDrayageView[]>>(path, { params })
return (unwrapApiResponse(response) || []).map(normalize)
}
export async function getTerminalDrayage(containerBookingId: string) {
const response = await http.get<ApiEnvelope<TerminalDrayageView>, ApiEnvelope<TerminalDrayageView>>(`${path}/${encodeURIComponent(containerBookingId)}`)
return normalize(unwrapApiResponse(response))
}
export async function getTerminalDrayageHistory(containerBookingId: string) {
const response = await http.get<ApiEnvelope<TerminalDrayageHistory[]>, ApiEnvelope<TerminalDrayageHistory[]>>(`${path}/${encodeURIComponent(containerBookingId)}/history`)
return (unwrapApiResponse(response) || []).map((item) => ({ ...item, operatorId: id(item.operatorId) }))
}
async function command(containerBookingId: string, action: string, body: object, idempotencyKey = operationId(action)) {
const response = await http.post<ApiEnvelope<TerminalDrayageView>, ApiEnvelope<TerminalDrayageView>>(
`${path}/${encodeURIComponent(containerBookingId)}/${action}`,
body,
{ headers: { 'Idempotency-Key': idempotencyKey } },
)
return normalize(unwrapApiResponse(response))
}
export function selectTerminalDrayageFleet(containerBookingId: string, body: { expectedVersion: number; fleetSupplierId: string; fleetSupplierName?: string; reason?: string }, idempotencyKey?: string) {
return command(containerBookingId, 'fleet', body, idempotencyKey)
}
export function scheduleTerminalDrayagePickup(containerBookingId: string, body: { expectedVersion: number; pickupAppointmentAt: string; reason?: string }, idempotencyKey?: string) {
return command(containerBookingId, 'pickup-appointment', body, idempotencyKey)
}
export function recordTerminalDrayageEmptyReturn(containerBookingId: string, body: { expectedVersion: number; actualAt: string; remark?: string }, idempotencyKey?: string) {
return command(containerBookingId, 'empty-return', body, idempotencyKey)
}
export interface DirectDelivery {
requirementKey: string; inboundCode: string | null; orderNo: string | null; destinationName: string | null
packageCount: number | null; version: number; actualAt: string | null; timezone: string | null
receivedBy: string | null; evidenceNo: string | null; remark: string | null
}
export interface DirectDeliveryCommand { expectedVersion: number; actualAt: string; timezone: string; receivedBy: string; evidenceNo: string; remark: string | null }
export async function getDirectDeliveries(containerBookingId: string) {
return unwrapApiResponse(await http.get<ApiEnvelope<DirectDelivery[]>, ApiEnvelope<DirectDelivery[]>>(`${path}/${encodeURIComponent(containerBookingId)}/direct-deliveries`))
}
export async function recordDirectDelivery(containerBookingId: string, requirementKey: string, body: DirectDeliveryCommand, idempotencyKey: string) {
if (!/^INBOUND:\d+$/.test(requirementKey)) throw new Error('服务端入仓来源标识无效,请重新读取清单')
const inboundId = requirementKey.slice('INBOUND:'.length)
return unwrapApiResponse(await http.post<ApiEnvelope<DirectDelivery>, ApiEnvelope<DirectDelivery>>(`${path}/${encodeURIComponent(containerBookingId)}/direct-deliveries/${encodeURIComponent(inboundId)}`, body, { headers: { 'Idempotency-Key': idempotencyKey } }))
}
export interface TerminalPickupCommand { standardNode: 'PICKED_UP'; observedAt: string; estimatedAt: null; actualAt: string; locationCode: string | null; locationName: string | null; reason: string }
export async function recordTerminalPickup(containerBookingId: string, body: TerminalPickupCommand, idempotencyKey: string) {
unwrapApiResponse(await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`/platform/v1/container-shipping-nodes/${encodeURIComponent(containerBookingId)}`, body, { headers: { 'Idempotency-Key': idempotencyKey } }))
}