Files
admin-web/src/modules/container-loading/api/terminalDrayages.ts
T
2026-09-09 13:54:43 +08:00

132 lines
5.6 KiB
TypeScript

import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type TerminalDrayageStatus = 'WAITING_FOR_ARRIVAL' | 'ARRIVED' | 'PICKUP_SCHEDULED' | 'PICKED_UP' | 'UNLOADED' | 'EMPTY_RETURNED'
export type TerminalDrayageAction = 'SELECT_FLEET' | 'SCHEDULE_PICKUP' | 'RECORD_PICKUP' | 'RECORD_EMPTY_RETURN' | 'SEND_PICKUP_EMAIL'
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) {
const response = await http.post<ApiEnvelope<TerminalDrayageView>, ApiEnvelope<TerminalDrayageView>>(
`${path}/${encodeURIComponent(containerBookingId)}/${action}`,
body,
{ headers: { 'Idempotency-Key': operationId(action) } },
)
return normalize(unwrapApiResponse(response))
}
export function selectTerminalDrayageFleet(containerBookingId: string, body: { expectedVersion: number; fleetSupplierId: string; fleetSupplierName?: string; reason?: string }) {
return command(containerBookingId, 'fleet', body)
}
export function scheduleTerminalDrayagePickup(containerBookingId: string, body: { expectedVersion: number; pickupAppointmentAt: string; reason?: string }) {
return command(containerBookingId, 'pickup-appointment', body)
}
export function recordTerminalDrayagePickup(containerBookingId: string, body: { expectedVersion: number; actualAt: string; remark?: string }) {
return command(containerBookingId, 'pickup', body)
}
export function recordTerminalDrayageEmptyReturn(containerBookingId: string, body: { expectedVersion: number; actualAt: string; remark?: string }) {
return command(containerBookingId, 'empty-return', body)
}