import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import { normalizeBooking, type WireBooking } from './tripBookings' import type { PageResult } from './overseasMasterData' import type { TripPreviousDispatch, FleetTrip, TripStatus, TripDetail, TripDraft, TripPlan, TripAssignment, TripBindingCommand, TripBinding, TripCargo, TripLoad, TripMeasurement, TripConfirmedLoad, TripLoading, TripLoadConfirmation, TripOutcome, TripCompletionInspection, TripComplete, TripResourceRelease, TripHistory, TripReviewState, TripReviewCommand, TripCapacityPlanning, } from '../tripTypes' const base = '/platform/v1/last-leg/fleet/trips' const path = (tripNo: string) => `${base}/${encodeURIComponent(tripNo)}` type Numeric = number | string type WireTrip = Omit & { recordVersion: Numeric; maxPalletSpaces: Numeric | null; maxLoadKg: Numeric | null; maxVolumeCbm: Numeric | null } type WireReview = Omit & { recordVersion: Numeric | null } type WireBinding = Omit & { packageCount: Numeric; review: WireReview } type WirePreviousDispatch = Omit & { recordVersion: Numeric } type WireDetail = Omit & { trip: WireTrip; dispatches: WireBinding[]; loadingReview: WireReview; previousDispatches: WirePreviousDispatch[] } type WirePage = { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric } type WireCargo = Omit & { packages: Numeric } type WireLoad = { palletSpaces: Numeric; weightKg: Numeric | null; volumeCbm: Numeric | null } type WireMeasurement = Omit & WireLoad & { deliveryIndex: Numeric } type WireConfirmed = Omit & { cargo: WireCargo[]; pallets: WireMeasurement[] segments: { fromIndex: Numeric; toIndex: Numeric; occupied: WireLoad; remaining: WireLoad }[] } type WireLoading = Omit & { recordVersion: Numeric; cargo: WireCargo[]; confirmedLoad: WireConfirmed | null } type WirePlanning = Omit & { recordVersion: Numeric; cargo: WireCargo[]; capacityPlan: WireConfirmed | null; availableSegments: WireConfirmed['segments'] } type WireOutcome = Omit & { packages: Numeric; signedPackages: Numeric; returnedPackages: Numeric; remainingPackages: Numeric } type WireInspection = Omit & { recordVersion: Numeric; outcomes: WireOutcome[] } type WireHistory = Omit & { recordVersion: Numeric snapshot: { externalBookings: WireBooking[]; capacityPlan: WireConfirmed | null; trip: WireTrip; dispatches: WireBinding[]; confirmedLoad: WireConfirmed | null; loadingReview: WireReview; completion: (Omit, 'outcomes'> & { outcomes: WireOutcome[] }) | null } } function number(value: Numeric): number { if (typeof value !== 'number' && typeof value !== 'string') throw new Error('车次接口缺少有效数值字段') if (typeof value === 'string' && !value.trim()) throw new Error('车次接口返回了空的数值字段') const result = Number(value) if (!Number.isFinite(result) || result < 0) throw new Error('车次接口返回了无效数值') return result } function integer(value: Numeric): number { const result = number(value) if (!Number.isSafeInteger(result)) throw new Error('车次接口返回了无效分页、数量或版本') return result } const trip = (value: WireTrip): FleetTrip => ({ ...value, recordVersion: integer(value.recordVersion), maxPalletSpaces: value.maxPalletSpaces == null ? null : integer(value.maxPalletSpaces), maxLoadKg: value.maxLoadKg == null ? null : number(value.maxLoadKg), maxVolumeCbm: value.maxVolumeCbm == null ? null : number(value.maxVolumeCbm) }) const review = (value: WireReview): TripReviewState => ({ ...value, recordVersion: value.recordVersion == null ? null : integer(value.recordVersion) }) const batchReferences = (values: TripBinding['appointmentBatches']) => values.map(reference => ({ ...reference, sources: reference.sources.map(source => ({ ...source, packageCount: integer(source.packageCount) })) })) const binding = (value: WireBinding): TripBinding => ({ ...value, packageCount: integer(value.packageCount), appointmentBatches: batchReferences(value.appointmentBatches), review: review(value.review) }) export const normalizeTripDetail = (value: WireDetail): TripDetail => ({ ...value, trip: trip(value.trip), dispatches: value.dispatches.map(binding), previousDispatches: value.previousDispatches.map(row => ({ ...row, recordVersion: integer(row.recordVersion), appointmentBatches: batchReferences(row.appointmentBatches) })), loadingReview: review(value.loadingReview) }) const cargo = (value: WireCargo): TripCargo => ({ ...value, packages: integer(value.packages) }) const outcome = (value: WireOutcome): TripOutcome => ({ ...value, packages: integer(value.packages), signedPackages: integer(value.signedPackages), returnedPackages: integer(value.returnedPackages), remainingPackages: integer(value.remainingPackages) }) const load = (value: WireLoad): TripLoad => ({ palletSpaces: integer(value.palletSpaces), weightKg: value.weightKg == null ? null : number(value.weightKg), volumeCbm: value.volumeCbm == null ? null : number(value.volumeCbm) }) function confirmed(value: WireConfirmed | null): TripConfirmedLoad | null { return value == null ? null : { ...value, cargo: value.cargo.map(cargo), pallets: value.pallets.map(row => ({ ...row, ...load(row), deliveryIndex: integer(row.deliveryIndex) })), segments: value.segments.map(row => ({ fromIndex: integer(row.fromIndex), toIndex: integer(row.toIndex), occupied: load(row.occupied), remaining: load(row.remaining) })) } } const loading = (value: WireLoading): TripLoading => ({ ...value, recordVersion: integer(value.recordVersion), cargo: value.cargo.map(cargo), confirmedLoad: confirmed(value.confirmedLoad) }) function page(value: WirePage, map: (row: W) => T): PageResult { return { records: value.records.map(map), page: integer(value.page), size: integer(value.size), total: integer(value.total), pages: integer(value.pages) } } export interface TripQuery { page: number; size: number; keyword?: string; status?: TripStatus; warehouseId?: string } export async function listTrips(query: TripQuery): Promise> { return page(unwrapApiResponse(await http.get>, ApiEnvelope>>(base, { params: query })), trip) } export async function getTrip(tripNo: string): Promise { return normalizeTripDetail(unwrapApiResponse(await http.get, ApiEnvelope>(path(tripNo)))) } async function postDetail(url: string, body: TripDraft | TripAssignment | TripBindingCommand | TripComplete | TripResourceRelease | TripReviewCommand | { recordVersion: number; reason: string }): Promise { return normalizeTripDetail(unwrapApiResponse(await http.post, ApiEnvelope>(url, body))) } export const createTrip = (body: TripDraft) => postDetail(base, body) export const assignTrip = (tripNo: string, body: TripAssignment) => postDetail(`${path(tripNo)}/assignment`, body) export interface TripResourceOccupancy { resourceId: string; occupiedTripNo: string | null; available: boolean } /** 当前下拉候选批量查询实际占用,长 ID 始终按字符串传递。 */ export async function getTripResourceOccupancy(tripNo: string, kind: 'VEHICLE' | 'DRIVER', resourceIds: string[]): Promise { if (!resourceIds.length) return [] return unwrapApiResponse(await http.get, ApiEnvelope>(`${path(tripNo)}/resource-occupancy`, { params: { kind, resourceIds: resourceIds.join(',') } })) } export const reviewTripDispatch = (tripNo: string, dispatchNo: string, body: TripReviewCommand) => postDetail(`${path(tripNo)}/dispatches/${encodeURIComponent(dispatchNo)}/review`, body) export const reviewTripLoading = (tripNo: string, body: TripReviewCommand) => postDetail(`${path(tripNo)}/loading-review`, body) export const bindTripDispatch = (tripNo: string, body: TripBindingCommand) => postDetail(`${path(tripNo)}/dispatches/bind`, body) export const unbindTripDispatch = (tripNo: string, body: TripBindingCommand) => postDetail(`${path(tripNo)}/dispatches/unbind`, body) export const cancelTrip = (tripNo: string, body: { recordVersion: number; reason: string }) => postDetail(`${path(tripNo)}/cancel`, body) export const completeTrip = (tripNo: string, body: TripComplete) => postDetail(`${path(tripNo)}/complete`, body) export const releaseTripResource = (tripNo: string, body: TripResourceRelease) => postDetail(`${path(tripNo)}/resources/release`, body) export async function updateTripPlan(tripNo: string, body: TripPlan): Promise { return normalizeTripDetail(unwrapApiResponse(await http.put, ApiEnvelope>(`${path(tripNo)}/plan`, body))) } const planning = (value: WirePlanning): TripCapacityPlanning => ({ ...value, recordVersion: integer(value.recordVersion), cargo: value.cargo.map(cargo), capacityPlan: confirmed(value.capacityPlan), planValid: value.planValid === true, availableSegments: value.availableSegments.map(row => ({ fromIndex: integer(row.fromIndex), toIndex: integer(row.toIndex), occupied: load(row.occupied), remaining: load(row.remaining) })) }) export async function getTripCapacityPlan(tripNo: string): Promise { return planning(unwrapApiResponse(await http.get, ApiEnvelope>(`${path(tripNo)}/capacity-plan`))) } export async function confirmTripCapacityPlan(tripNo: string, body: TripLoadConfirmation): Promise { return planning(unwrapApiResponse(await http.post, ApiEnvelope>(`${path(tripNo)}/capacity-plan`, body))) } export async function getTripLoading(tripNo: string): Promise { return loading(unwrapApiResponse(await http.get, ApiEnvelope>(`${path(tripNo)}/loading`))) } export async function confirmTripLoading(tripNo: string, body: TripLoadConfirmation): Promise { return loading(unwrapApiResponse(await http.post, ApiEnvelope>(`${path(tripNo)}/loading`, body))) } export async function getTripCompletion(tripNo: string): Promise { const value = unwrapApiResponse(await http.get, ApiEnvelope>(`${path(tripNo)}/completion`)) return { ...value, canComplete: value.canComplete === true, recordVersion: integer(value.recordVersion), outcomes: value.outcomes.map(outcome) } } export async function listTripHistory(tripNo: string, currentPage = 1, size = 20): Promise> { const value = unwrapApiResponse(await http.get>, ApiEnvelope>>(`${path(tripNo)}/history`, { params: { page: currentPage, size } })) return page(value, row => ({ ...row, recordVersion: integer(row.recordVersion), snapshot: { trip: trip(row.snapshot.trip), dispatches: row.snapshot.dispatches.map(binding), confirmedLoad: confirmed(row.snapshot.confirmedLoad), externalBookings: row.snapshot.externalBookings.map(normalizeBooking), loadingReview: review(row.snapshot.loadingReview), capacityPlan: confirmed(row.snapshot.capacityPlan), completion: row.snapshot.completion == null ? null : { ...row.snapshot.completion, outcomes: row.snapshot.completion.outcomes.map(outcome) }, } })) } export interface DispatchCandidate { dispatchNo: string; customerId: string; warehouseId: string; packageCount: number; route: string | null } export async function listTripDispatchCandidates(tripNo: string, keyword?: string) { const value = unwrapApiResponse(await http.get>, ApiEnvelope>>(`${path(tripNo)}/dispatch-candidates`, { params: { keyword, page: 1, size: 100 } })) return { ...value, total: integer(value.total), records: value.records.map(row => ({ ...row, packageCount: integer(row.packageCount) })) } } export interface TripPreparation { tripNo: string | null; recordVersion: number | null; plan: TripDraft | null; vehicleId: string | null; driverId: string | null; dispatchNos: string[]; idempotencyKey: string } export async function prepareTrip(body: TripPreparation): Promise { return normalizeTripDetail(unwrapApiResponse(await http.post, ApiEnvelope>(`${base}/prepare`, body))) }