101 lines
16 KiB
TypeScript
101 lines
16 KiB
TypeScript
// Models after the dispatch API boundary: Long quantities are validated safe numbers.
|
|||
|
|
// dispatchWire.ts declares their HTTP string/number representation; IDs/versions remain lossless.
|
||
|
|
export type Version = string | number
|
||
|
|
export class DispatchInputError extends Error {}
|
||
|
|
export type DispatchStatus = 'ACTIVE' | 'CANCELLED'
|
||
|
|
export type DispatchStage = 'BEFORE_LOADING' | 'LOADING' | 'AFTER_LOADING' | 'DELIVERING' | 'DELIVERED'
|
||
|
|
export type DispatchAction = 'CREATE' | 'EDIT' | 'ADD_CARGO' | 'REMOVE_CARGO' | 'CANCEL' | 'SUBMIT_REVIEW' | 'DISPATCH_REVIEW' | 'LOADING_REVIEW' | 'RETURN_TO_REVIEW' | 'PICK' | 'LOADING_COMPLETE' | 'ARRANGE_DELIVERY' | 'DEPART'
|
||
|
|
export type ReviewStatus = 'NOT_SUBMITTED' | 'PENDING' | 'APPROVED' | 'REJECTED'
|
||
|
|
export type BeforeLoadingSection = 'VEHICLE_INFO' | 'DISPATCH_REVIEW' | 'LOADING_REVIEW'
|
||
|
|
export interface Gate { allowed: boolean; blockCode: string | null; blockReason: string | null }
|
||
|
|
export type DispatchActions = Partial<Record<DispatchAction, Gate>>
|
||
|
|
export interface Counts { itemCount: number; palletCount: number; packageCount: number; pickedPackageCount: number; shippedPackageCount: number }
|
||
|
|
export interface Capacity { palletSpaces: number | null; maxLoadKg: number | null; maxVolumeCbm: number | null }
|
||
|
|
export interface DriverEvidence { licenseNo: string | null; licenseExpiresOn: string | null; fileIds: string[]; expectedTaskEndAt: string | null }
|
||
|
|
export interface Vehicle { mode: 'MASTER' | 'EXTERNAL'; vehicleId: string | null; plate: string | null; carrierName: string | null; capacity: Capacity | null; evidenceFileIds: string[] }
|
||
|
|
export interface Driver { mode: 'MASTER' | 'EXTERNAL'; driverId: string | null; name: string | null; phone: string | null; evidence: DriverEvidence | null }
|
||
|
|
export interface VehicleSnapshot extends Vehicle { vehicleStatus: string | null }
|
||
|
|
export interface Resources { vehicle: VehicleSnapshot | null; driver: Driver | null }
|
||
|
|
export interface Destination { key: string; type: string; warehouseId: string | null; warehouseCode: string | null; warehouseName: string | null; address: string | null; postalCode: string | null; contactName: string | null; contactPhone: string | null }
|
||
|
|
export interface Review { status: ReviewStatus; reason: string | null; reviewerId: string | null; reviewedAt: string | null; reviewRound: Version; basisHash: string | null; invalidatedReason: string | null }
|
||
|
|
export interface Reviews { dispatchReview: Review; loadingReview: Review }
|
||
|
|
export interface DispatchBatchReference { batchKey: string; batchNo: string | null; customerId: string; sources: Array<{ sourceKey: string; destinationKey: string; packageCount: number }>; recordVersion: Version }
|
||
|
|
export interface DispatchBatchAllocation { planPalletId: string; batchKey: string; customerId: string; sourceKey: string; destinationKey: string; packageCount: number; recordVersion: Version }
|
||
|
|
export interface DispatchQuery { page: number; size: number; dispatchNo?: string; vehiclePlate?: string; warehouseId?: string; customerId?: string; fbaAppointmentNo?: string; status?: DispatchStatus | 'ALL'; stage?: DispatchStage; beforeLoadingSection?: BeforeLoadingSection; reviewStatus?: ReviewStatus }
|
||
|
|
export interface ReviewCounts { notSubmitted: number; pending: number; approved: number; rejected: number }
|
||
|
|
export interface DispatchStatistics { matchedTotal: number; activeTotal: number; cancelledTotal: number; stageCounts: Record<DispatchStage, number>; beforeLoadingReviewCounts: { vehicleInfoTotal: number; dispatchReview: ReviewCounts; loadingReview: ReviewCounts } }
|
||
|
|
export interface DispatchListRow { dispatchNo: string; status: DispatchStatus; stage: DispatchStage; recordVersion: Version; reviewState: string; reviews: Reviews; resources: Resources; warehouseId: string; warehouseCode: string; warehouseName: string; plannedDepartureAt: string | null; departedAt: string | null; counts: Counts; pickingReconciled: boolean; arranged: boolean; actions: DispatchActions }
|
||
|
|
export interface DispatchPage { records: DispatchListRow[]; total: number; page: number; size: number; pages: number; statistics: DispatchStatistics; snapshotAt: string }
|
||
|
|
export interface OutboundReference { customerId: string; customerName: string; outboundNo: string; status: string; packageCount: number; palletCount: number }
|
||
|
|
export interface DeliveryReference { groupKey: string; customerId: string; customerName: string; outboundNo: string; deliveryNo: string; destination: Destination; status: string; packageCount: number; palletCount: number }
|
||
|
|
export interface VehicleAppointment { referenceNo: string; status: string; appointmentAt: string | null }
|
||
|
|
export interface FbaAppointment { batchKey: string; batchNo: string | null; appointmentNo: string | null; destinationKey: string; destinationWarehouseCode: string | null; status: string; confirmedAt: string | null; blockReason: string | null }
|
||
|
|
export interface OrderDetail { customerId: string; customerName: string; orderNo: string; inboundCode: string | null; grossWeightKg: number | null; palletCount: number | null; destinationFbaWarehouse: string | null; volumeCbm: number | null; containerNo: string | null; sourceKey: string }
|
||
|
|
export interface DispatchDetail { dispatchNo: string; status: DispatchStatus; stage: DispatchStage; recordVersion: Version; reviewState: string; reviews: Reviews; counts: Counts; pickingReconciled: boolean; actions: DispatchActions; vehicleInfo: Resources; dispatchInfo: { route: string | null; plannedDepartureAt: string | null; departedAt: string | null; estimatedArrivalAt: string | null; fbaAppointments: FbaAppointment[]; vehicleAppointment: VehicleAppointment | null; appointmentBatches: DispatchBatchReference[] }; documents: { dispatchNo: string; outbounds: OutboundReference[]; deliveries: DeliveryReference[]; createdAt: string; updatedAt: string; remark: string | null }; orderDetails: OrderDetail[] }
|
||
|
|
export interface CargoSelection { sourceType: 'INBOUND_PLAN' | 'RETURN_RECEIPT' | 'RELABEL'; planPalletId: string | null; returnReceiptId: string | null; relabelNo: string | null; warehousePalletId: string | null; sourceVersion: string }
|
||
|
|
export interface CreateDispatch { idempotencyKey: string; warehouseId: string; vehicle: Vehicle | null; driver: Driver | null; plannedDepartureAt: string | null; estimatedArrivalAt: string | null; remark: string | null; cargo: CargoSelection[]; batchAllocations: DispatchBatchAllocation[] }
|
||
|
|
export interface VersionCommand { recordVersion: Version; idempotencyKey: string }
|
||
|
|
export interface UpdateDispatch extends VersionCommand { vehicle: Vehicle | null; driver: Driver | null; plannedDepartureAt: string | null; estimatedArrivalAt: string | null; vehicleAppointment?: { referenceNo: string; recordVersion: Version } | null; remark: string | null }
|
||
|
|
export interface AddCargo extends VersionCommand { cargo: CargoSelection[]; batchAllocations: DispatchBatchAllocation[] }
|
||
|
|
export interface ReasonCommand extends VersionCommand { reason: string }
|
||
|
|
export interface ReviewCommand extends VersionCommand { decision: 'APPROVED' | 'REJECTED'; reason: string | null }
|
||
|
|
export interface LoadingMeasurement { warehousePalletId: string; palletSpaces: number; weightKg: number; volumeCbm: number; deliveryIndex: number }
|
||
|
|
export interface ConfirmedLoad { manifestHash: string; pallets: LoadingMeasurement[]; operatorId: string; confirmedAt: string }
|
||
|
|
export interface LoadingComplete extends VersionCommand { manifestHash: string; measurements: LoadingMeasurement[]; remark: string | null }
|
||
|
|
export interface LoadingPreview { dispatchNo: string; recordVersion: Version; status: DispatchStatus; stage: DispatchStage; manifestHash: string; pickingReconciled: boolean; resources: Resources; pallets: Array<{ warehousePalletId: string; palletNo: string; scheduledPackages: number; pickedPackages: number; palletSpaces: number | null; weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; missingFields: string[]; evidence: Array<{ field: string; sourceType: string; sourceBusinessNo: string | null; observedAt: string | null; value: number | null; actual: boolean }> }>; confirmedLoad: ConfirmedLoad | null; loadingComplete: Gate; missingFields: string[] }
|
||
|
|
export interface Candidate { selection: CargoSelection; sourceKey: string; sourceVersion: string; customerId: string; customerName: string; warehouseId: string; warehouseCode: string; warehouseName: string; orderNo: string | null; inboundCode: string | null; planNo: string | null; palletNo: string | null; containerNo: string | null; destination: Destination; availablePackages: number; suggestedWeightKg: number | null; suggestedVolumeCbm: number | null; measurementSource: string | null; appointmentBatches: DispatchBatchReference[]; blockReasons: string[] }
|
||
|
|
export interface CandidateQuery { warehouseId?: string; planIds?: string[]; orderNo?: string; destinationWarehouseCode?: string; sourceStage?: string; draftToken?: string }
|
||
|
|
export interface DispatchCargoItem { dispatchItemId: string; customerId: string; customerName: string; sourceKey: string; sourceVersion: string; sourceType: CargoSelection['sourceType']; planPalletId: string | null; returnReceiptId: string | null; relabelNo: string | null; warehousePalletId: string | null; palletNo: string | null; orderNo: string | null; inboundCode: string | null; destination: Destination; scheduledPackages: number; pickedPackages: number; remove: Gate }
|
||
|
|
export interface DispatchItems { dispatchNo: string; recordVersion: Version; status: DispatchStatus; stage: DispatchStage; items: DispatchCargoItem[] }
|
||
|
|
export interface DispatchRouteStop { index: number; name: string; address: string | null; destinationKey: string | null }
|
||
|
|
export interface DispatchRoute { dispatchNo: string; recordVersion: Version; stops: DispatchRouteStop[]; edit: Gate }
|
||
|
|
export interface DispatchRouteCommand extends VersionCommand { destinationKeys: string[]; reason: string }
|
||
|
|
export type DispatchEvidenceKind = 'VEHICLE' | 'DRIVER'
|
||
|
|
export interface DispatchEvidenceFile { fileId: string; fileName: string; contentType: string; sizeBytes: number }
|
||
|
|
export interface CandidateList { candidates: Candidate[]; snapshotAt: string; complete: boolean }
|
||
|
|
export interface DraftContainer { draftContainerId: string; sequenceNo: number; capacity: number; sourceKeys: string[] }
|
||
|
|
export interface DispatchDraft { draftToken: string; recordVersion: Version; status: string; warehouseId: string; containers: DraftContainer[]; sources: Candidate[]; expiresAt: string }
|
||
|
|
export interface DraftSelection extends VersionCommand { draftToken: string; draftContainerId: string; source: CargoSelection }
|
||
|
|
export interface ConvertDraft extends VersionCommand { draftToken: string; groups: Array<{ draftContainerId: string; dispatch: CreateDispatch }> }
|
||
|
|
export interface ConvertedDraft { draftToken: string; vehicles: Array<{ draftContainerId: string; dispatch: CommandResult }>; replayed: boolean }
|
||
|
|
export interface CoverageSelection { financeFeeId: string; sourceVersion: number; sourceHash: string; services: string[] }
|
||
|
|
export interface FeeItem { feeType: string; feeName: string; direction: string; currency: string; amount: number | null; supplierId?: string | null; supplierName?: string | null; supplierCategory?: string | null; settlementMode?: string | null; paidAt?: string | null; paymentMethod?: string | null; paymentReferenceNo?: string | null; paymentVoucherFileIds?: string[]; chargeableQuantity?: number | null; quantityUnit?: string | null; unitPrice?: number | null; periodStart?: string | null; periodEnd?: string | null; remark?: string | null }
|
||
|
|
export interface ArrangeGroup { groupKey: string; groupCargoHash: string; plannedDeliveryAt: string; deliveryMethod: string; waybillNo: string | null; carrierName: string | null; pickupMethod: string; loadingType: string; vehicleType: string | null; vehicleSize: string | null; appointmentAccount: string | null; sellerName: string | null; remark: string | null; externalFreightHash: string; feeCoverageCargoHash: string; feeCoverage: CoverageSelection[]; fees: FeeItem[] }
|
||
|
|
export interface ArrangeDelivery extends VersionCommand { cargoHash: string; loadingCompletionHash: string; groups: ArrangeGroup[] }
|
||
|
|
export interface ArrangementResult { dispatchNo: string; recordVersion: Version; arrangementKey: string; cargoHash: string; arrangedAt: string; outbounds: OutboundReference[]; deliveries: DeliveryReference[] }
|
||
|
|
export interface CargoLine { dispatchItemId: string; customerId: string; customerName: string; orderId: string | null; orderNo: string | null; sourceKey: string; sourceStage: string; sourceRegion: string | null; sourceInboundId: string | null; inboundCode: string | null; planPalletId: string | null; warehousePalletId: string | null; palletNo: string | null; pickId: string | null; attributionId: string | null; inventoryPositionId: string | null; receiptId: string | null; returnReceiptId: string | null; returnCartonId: string | null; cartonMark: string | null; relabelNo: string | null; destination: Destination; packages: number; productQuantity: number; grossWeightKg: number | null; volumeCbm: number | null; batchAllocations: DispatchBatchAllocation[] }
|
||
|
|
export interface PreviewGroup { groupKey: string; customerId: string; customerName: string; destination: Destination; groupCargoHash: string; cargo: CargoLine[]; externalFreightHash: string; externalQuotes: Array<{ customerId: string; groupKey: string; bookingNo: string; inboundCode: string; currency: string; amount: number; sourceHash: string }>; feeCoverageCargoHash: string; feeSources: Array<{ financeFeeId: string; customerId: string; currency: string; amount: number; sourceVersion: number; sourceHash: string; status: string; services: string[]; sourceKeys: string[] }>; requiredServices: string[]; blockReasons: string[] }
|
||
|
|
export interface ArrangementPreview { dispatchNo: string; recordVersion: Version; status: DispatchStatus; stage: DispatchStage; cargoHash: string; loadingCompletionHash: string; arrange: Gate; groups: PreviewGroup[]; arrangement: ArrangementResult | null }
|
||
|
|
interface CommandBase { dispatchNo: string; idempotencyKey: string; recordVersion: Version; status: DispatchStatus; stage: DispatchStage; occurredAt: string; effect: 'APPLIED' | 'EXISTING'; counts: Counts; replayed: boolean; alreadyArranged: boolean }
|
||
|
|
export type CommandResult = CommandBase & (
|
||
|
|
{ action: 'PICK'; result: { dispatchItemId: string; positionId: string | null; returnCartonId: string | null; packages: number; pickedPackages: number; pickingReconciled: boolean } }
|
||
|
|
| { action: 'LOADING_COMPLETE'; result: { loadingCompletionHash: string; loadingCompletedAt: string; confirmedLoad: ConfirmedLoad } }
|
||
|
|
| { action: 'DEPART'; result: { departedAt: string; shippedPackages: number; outbounds: OutboundReference[]; deliveries: DeliveryReference[] } }
|
||
|
|
| { action: 'ARRANGE_DELIVERY'; result: ArrangementResult }
|
||
|
|
| { action: Exclude<DispatchAction, 'PICK' | 'LOADING_COMPLETE' | 'DEPART' | 'ARRANGE_DELIVERY'>; result: { dispatchNo: string; changedItemIds: string[] } }
|
||
|
|
)
|
||
|
|
|
||
|
|
export const dispatchStages: Array<{ value: DispatchStage; label: string }> = [
|
||
|
|
{ value: 'BEFORE_LOADING', label: '装车前' }, { value: 'LOADING', label: '装车中' },
|
||
|
|
{ value: 'AFTER_LOADING', label: '装车后' }, { value: 'DELIVERING', label: '派送中' }, { value: 'DELIVERED', label: '派送完成' },
|
||
|
|
]
|
||
|
|
export const stageLabel = (stage: DispatchStage) => dispatchStages.find(row => row.value === stage)?.label || '—'
|
||
|
|
export const reviewLabels: Record<ReviewStatus, string> = { NOT_SUBMITTED: '未提交', PENDING: '待审核', APPROVED: '已通过', REJECTED: '已驳回' }
|
||
|
|
export function requireVersion(value: unknown): Version {
|
||
|
|
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return value
|
||
|
|
if (typeof value === 'string' && /^\d+$/.test(value)) return value
|
||
|
|
throw new DispatchInputError('操作版本缺失或无效,请刷新单据')
|
||
|
|
}
|
||
|
|
|
||
|
|
export function requireBatchVersions(values: readonly { batchKey: string; recordVersion: Version }[]): void {
|
||
|
|
const versions = new Map<string, string>()
|
||
|
|
for (const value of values) {
|
||
|
|
let version: Version
|
||
|
|
try { version = requireVersion(value.recordVersion) }
|
||
|
|
catch { throw new DispatchInputError('预约批次版本缺失或无效,请重新读取候选') }
|
||
|
|
const canonical = String(version).replace(/^0+(?=\d)/, '')
|
||
|
|
if (versions.has(value.batchKey) && versions.get(value.batchKey) !== canonical) throw new DispatchInputError('同一预约批次的版本不一致,请重新读取候选')
|
||
|
|
versions.set(value.batchKey, canonical)
|
||
|
|
}
|
||
|
|
}
|