2026-09-07 23:58:18 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-07 23:58:18 +08:00
|
|
|
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) }))
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 17:11:03 +08:00
|
|
|
async function command(containerBookingId: string, action: string, body: object, idempotencyKey = operationId(action)) {
|
2026-09-07 23:58:18 +08:00
|
|
|
const response = await http.post<ApiEnvelope<TerminalDrayageView>, ApiEnvelope<TerminalDrayageView>>(
|
|
|
|
|
`${path}/${encodeURIComponent(containerBookingId)}/${action}`,
|
|
|
|
|
body,
|
2026-10-15 17:11:03 +08:00
|
|
|
{ headers: { 'Idempotency-Key': idempotencyKey } },
|
2026-09-07 23:58:18 +08:00
|
|
|
)
|
|
|
|
|
return normalize(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 17:11:03 +08:00
|
|
|
export function selectTerminalDrayageFleet(containerBookingId: string, body: { expectedVersion: number; fleetSupplierId: string; fleetSupplierName?: string; reason?: string }, idempotencyKey?: string) {
|
|
|
|
|
return command(containerBookingId, 'fleet', body, idempotencyKey)
|
2026-09-07 23:58:18 +08:00
|
|
|
}
|
2026-10-15 17:11:03 +08:00
|
|
|
export function scheduleTerminalDrayagePickup(containerBookingId: string, body: { expectedVersion: number; pickupAppointmentAt: string; reason?: string }, idempotencyKey?: string) {
|
|
|
|
|
return command(containerBookingId, 'pickup-appointment', body, idempotencyKey)
|
2026-09-07 23:58:18 +08:00
|
|
|
}
|
2026-10-15 17:11:03 +08:00
|
|
|
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 } }))
|
2026-09-07 23:58:18 +08:00
|
|
|
}
|