feat(appointment): complete customer receiving and trip continuation workflows
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiResponseError, http } from '@/shared/api/http'
|
||||
import {
|
||||
listTrips, getTrip, createTrip, updateTripPlan, assignTrip, bindTripDispatch, unbindTripDispatch, cancelTrip,
|
||||
prepareTrip, listTrips, getTrip, createTrip, updateTripPlan, assignTrip, bindTripDispatch, unbindTripDispatch, cancelTrip,
|
||||
getTripLoading, confirmTripLoading, getTripCompletion, completeTrip, releaseTripResource, listTripHistory, reviewTripDispatch, reviewTripLoading, getTripCapacityPlan, confirmTripCapacityPlan,
|
||||
} from './trips'
|
||||
|
||||
@@ -14,7 +14,7 @@ const trip = { tripNo: 'TR/TEST', status: 'PLANNED', warehouseId: longId, wareho
|
||||
plannedDepartureAt: '2026-10-02T10:00:00Z', expectedDriverEndAt: '2026-10-03T10:00:00Z', departedAt: null, completedAt: null,
|
||||
stops: [{ name: '起运仓' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }], remark: null, updatedAt: '2026-10-02T08:00:00Z', vehicleRelease: null, driverRelease: null }
|
||||
const reviewState = { status: 'PENDING', reason: null, reviewerId: null, reviewedAt: null, recordVersion: null }
|
||||
const detail = { trip, driverQualification: null, loadingReview: reviewState, dispatches: [{ dispatchNo: 'DP-1', customerId: longId, packageCount: '12', boundBy: longId, boundAt: trip.updatedAt, review: reviewState }] }
|
||||
const detail = { trip, driverQualification: null, previousDispatches: [], appointmentHistoryUnavailableReason: null, loadingReview: reviewState, dispatches: [{ appointmentBatches: [], dispatchNo: 'DP-1', customerId: longId, packageCount: '12', boundBy: longId, boundAt: trip.updatedAt, review: reviewState }] }
|
||||
const cargo = { dispatchNo: 'DP-1', customerId: longId, dispatchItemId: longId, warehousePalletId: longId, palletNo: 'PALLET-1', packages: '12', destinationKey: 'FBA:ONT8', destinationLabel: 'ONT8' }
|
||||
const load = { palletSpaces: '1', weightKg: '100.125', volumeCbm: '1.005' }
|
||||
const confirmed = { manifestHash: 'a'.repeat(64), cargo: [cargo], pallets: [{ ...load, warehousePalletId: longId, deliveryIndex: '1' }],
|
||||
@@ -113,3 +113,28 @@ describe('fleet trip API contract', () => {
|
||||
await expect(cancelTrip(trip.tripNo, { recordVersion: 0, reason: '取消' })).rejects.toBe(conflict)
|
||||
})
|
||||
})
|
||||
|
||||
it('prepares the same trip from two real customer dispatches without issuing cargo creation or separate bind commands', async () => {
|
||||
vi.mocked(http.post).mockResolvedValueOnce(envelope({ ...detail, dispatches: [{ ...detail.dispatches[0]!, dispatchNo: 'DS-60', customerId: 'customer-b', packageCount: '60' }, { ...detail.dispatches[0]!, dispatchNo: 'DS-40', customerId: 'customer-d', packageCount: '40' }] }))
|
||||
const request = { tripNo: trip.tripNo, recordVersion: 4, plan: null, vehicleId: null, driverId: null, dispatchNos: ['DS-60', 'DS-40'], idempotencyKey: 'same-share-key' }
|
||||
const value = await prepareTrip(request)
|
||||
expect(http.post).toHaveBeenCalledExactlyOnceWith(`${base}/prepare`, request)
|
||||
expect(value.dispatches.map(row => [row.dispatchNo, row.customerId, row.packageCount])).toEqual([['DS-60', 'customer-b', 60], ['DS-40', 'customer-d', 40]])
|
||||
expect(http.get).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
it('preserves current and previous stable batch identities and normalizes only their numeric fields', async () => {
|
||||
const reference = { batchKey: 'FBA_APPOINTMENT:9223372036854775801', batchNo: null, customerId: longId, sources: [{ sourceKey: 'ORDER:9223372036854775801', destinationKey: 'FBA:ONT8', packageCount: '40' }] }
|
||||
vi.mocked(http.get).mockResolvedValueOnce(envelope({ ...detail, dispatches: [{ ...detail.dispatches[0]!, appointmentBatches: [reference] }], previousDispatches: [{ dispatchNo: 'DS-REMOVED', customerId: longId, recordVersion: '7', recordedAt: trip.updatedAt, appointmentBatches: [reference] }], appointmentHistoryUnavailableReason: '缺少部分历史来源' }))
|
||||
const result = await getTrip(trip.tripNo)
|
||||
expect(result.dispatches[0]!.appointmentBatches).toEqual([{ ...reference, sources: [{ ...reference.sources[0], packageCount: 40 }] }])
|
||||
expect(result.previousDispatches[0]).toMatchObject({ dispatchNo: 'DS-REMOVED', customerId: longId, recordVersion: 7, appointmentBatches: result.dispatches[0]!.appointmentBatches })
|
||||
expect(result.appointmentHistoryUnavailableReason).toBe('缺少部分历史来源')
|
||||
})
|
||||
it('rejects missing stable-reference arrays instead of silently falling back to legacy appointment numbers', async () => {
|
||||
for (const invalid of [{ ...detail, previousDispatches: undefined }, { ...detail, dispatches: [{ ...detail.dispatches[0]!, appointmentBatches: undefined, fbaAppointmentNo: 'AP-OLD' }] }]) {
|
||||
vi.mocked(http.get).mockResolvedValueOnce(envelope(invalid))
|
||||
await expect(getTrip(trip.tripNo)).rejects.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import { normalizeBooking, type WireBooking } from './tripBookings'
|
||||
import type { PageResult } from './overseasMasterData'
|
||||
import type {
|
||||
FleetTrip, TripStatus, TripDetail, TripDraft, TripPlan, TripAssignment, TripBindingCommand, TripBinding,
|
||||
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'
|
||||
@@ -15,7 +15,8 @@ type WireTrip = Omit<FleetTrip, 'recordVersion' | 'maxPalletSpaces' | 'maxLoadKg
|
||||
}
|
||||
type WireReview = Omit<TripReviewState, 'recordVersion'> & { recordVersion: Numeric | null }
|
||||
type WireBinding = Omit<TripBinding, 'packageCount' | 'review'> & { packageCount: Numeric; review: WireReview }
|
||||
type WireDetail = Omit<TripDetail, 'trip' | 'dispatches' | 'loadingReview'> & { trip: WireTrip; dispatches: WireBinding[]; loadingReview: WireReview }
|
||||
type WirePreviousDispatch = Omit<TripPreviousDispatch, 'recordVersion'> & { recordVersion: Numeric }
|
||||
type WireDetail = Omit<TripDetail, 'trip' | 'dispatches' | 'loadingReview' | 'previousDispatches'> & { trip: WireTrip; dispatches: WireBinding[]; loadingReview: WireReview; previousDispatches: WirePreviousDispatch[] }
|
||||
type WirePage<T> = { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric }
|
||||
type WireCargo = Omit<TripCargo, 'packages'> & { packages: Numeric }
|
||||
type WireLoad = { [K in keyof TripLoad]: Numeric }
|
||||
@@ -49,8 +50,9 @@ const trip = (value: WireTrip): FleetTrip => ({ ...value, recordVersion: integer
|
||||
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 binding = (value: WireBinding): TripBinding => ({ ...value, packageCount: integer(value.packageCount), review: review(value.review) })
|
||||
const detail = (value: WireDetail): TripDetail => ({ ...value, trip: trip(value.trip), dispatches: value.dispatches.map(binding), loadingReview: review(value.loadingReview) })
|
||||
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) })
|
||||
const detail = (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) })
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock('../api/tripHandovers', async original => ({ ...(await original<typeof a
|
||||
vi.mock('../api/overseasMasterData', () => ({ listVehicles: vi.fn() }))
|
||||
vi.mock('../api/drivers', () => ({ listDrivers: vi.fn() }))
|
||||
const date = '2026-10-03T01:00:00Z'
|
||||
const detail: TripDetail = { trip: { tripNo: 'TR-ONE', status: 'IN_TRANSIT', warehouseId: '1', warehouseCode: 'WH', warehouseName: '海外仓', vehicleId: '11', vehiclePlate: 'OLD-PLATE', driverId: '21', driverName: '原司机', driverPhone: 'phone', maxPalletSpaces: 2, maxLoadKg: 200, maxVolumeCbm: 4, plannedDepartureAt: date, expectedDriverEndAt: '2026-10-04T01:00:00Z', departedAt: date, completedAt: null, stops: [], remark: null, recordVersion: 7, updatedAt: date, vehicleRelease: null, driverRelease: null }, driverQualification: null, dispatches: [], loadingReview: { status: 'APPROVED', reason: null, reviewerId: null, reviewedAt: date, recordVersion: 7 } }
|
||||
const detail: TripDetail = { trip: { tripNo: 'TR-ONE', status: 'IN_TRANSIT', warehouseId: '1', warehouseCode: 'WH', warehouseName: '海外仓', vehicleId: '11', vehiclePlate: 'OLD-PLATE', driverId: '21', driverName: '原司机', driverPhone: 'phone', maxPalletSpaces: 2, maxLoadKg: 200, maxVolumeCbm: 4, plannedDepartureAt: date, expectedDriverEndAt: '2026-10-04T01:00:00Z', departedAt: date, completedAt: null, stops: [], remark: null, recordVersion: 7, updatedAt: date, vehicleRelease: null, driverRelease: null }, driverQualification: null, previousDispatches: [], appointmentHistoryUnavailableReason: null, dispatches: [], loadingReview: { status: 'APPROVED', reason: null, reviewerId: null, reviewedAt: date, recordVersion: 7 } }
|
||||
const previous = { vehicleId: '11', vehiclePlate: 'OLD-PLATE', driverId: '21', driverName: '原司机', driverPhone: 'phone' }
|
||||
const replacement = { vehicleId: '12', vehiclePlate: 'NEW-PLATE', driverId: '22', driverName: '接替司机', driverPhone: 'phone' }
|
||||
const load = { palletSpaces: 1, weightKg: 100, volumeCbm: 2 }
|
||||
|
||||
@@ -26,7 +26,7 @@ it('loads same-warehouse trips on demand and retries one immutable atomic prepar
|
||||
select.vm.$emit('focus'); await flushPromises()
|
||||
expect(listTrips).toHaveBeenCalledWith(expect.objectContaining({ warehouseId: '9007199254740993', status: 'PLANNED' }))
|
||||
select.vm.$emit('update:modelValue', trip.tripNo); await flushPromises()
|
||||
vi.mocked(prepareTrip).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ trip, dispatches: [], driverQualification: null, loadingReview: { status: 'PENDING', reason: null, reviewerId: null, reviewedAt: null, recordVersion: null } } as TripDetail)
|
||||
vi.mocked(prepareTrip).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ trip, dispatches: [], driverQualification: null, previousDispatches: [], appointmentHistoryUnavailableReason: null, loadingReview: { status: 'PENDING', reason: null, reviewerId: null, reviewedAt: null, recordVersion: null } } as TripDetail)
|
||||
const dialog=wrapper.findComponent({ name: 'AppDialog' })
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
const original=vi.mocked(prepareTrip).mock.calls[0]![0]
|
||||
|
||||
@@ -8,3 +8,5 @@ export { routeLocation, type RouteLocation } from './routeLocations'
|
||||
export { default as TripPreparationDialog } from './components/TripPreparationDialog.vue'
|
||||
export { default as TripDispatchSelector } from './components/TripDispatchSelector.vue'
|
||||
export { listTripDispatchCandidates, prepareTrip, type TripPreparation, type DispatchCandidate } from './api/trips'
|
||||
|
||||
export type { TripPreviousDispatch } from './tripTypes'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { appointmentReturnTarget } from '@/modules/appointment-calendar'
|
||||
import { appointmentReturnTarget, TripAppointmentPanel } from '@/modules/appointment-calendar'
|
||||
import { inquiryReturnTarget } from '../inquiryNavigation'
|
||||
import TripDispatchSelector from '../components/TripDispatchSelector.vue'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
@@ -32,6 +32,8 @@ const route = useRoute(), router = useRouter(), access = useAccessPreviewStore()
|
||||
const detail = ref<TripDetail | null>(null), loading = ref(false), error = ref<ApiErrorDetails | null>(null), operationError = ref<ApiErrorDetails | null>(null)
|
||||
const activeTab = ref('overview'), editorOpen = ref(false), busy = ref(''), dispatchNo = ref('')
|
||||
const offerSettingsOpen = ref(false)
|
||||
const appointmentsOpen = ref(false)
|
||||
const appointmentsPanel = ref<InstanceType<typeof TripAppointmentPanel> | null>(null)
|
||||
const inspection = ref<TripCompletionInspection | null>(null), inspectionLoading = ref(false)
|
||||
const history = ref<TripHistory[]>([]), historyPage = ref(1), historyTotal = ref(0), historyLoading = ref(false)
|
||||
const resourceKind = ref<'VEHICLE' | 'DRIVER'>('VEHICLE'), actualEndAt = ref(new Date().toISOString()), endLocation = ref('')
|
||||
@@ -65,9 +67,11 @@ function accept(value: TripDetail) {
|
||||
if (value.trip.vehicleRelease && resourceKind.value === 'VEHICLE') resourceKind.value = 'DRIVER'
|
||||
}
|
||||
async function load() {
|
||||
if (appointmentsPanel.value && !await appointmentsPanel.value.canLeave()) return
|
||||
const current = ++generation; inspectionSequence++; historySequence++
|
||||
loading.value = true; error.value = null; detail.value = null; operationError.value = null; activeTab.value = route.query.tab === 'loading' ? 'loading' : 'overview'
|
||||
editorOpen.value = false; busy.value = ''; dispatchNo.value = ''; inspection.value = null; history.value = []; historyPage.value = 1
|
||||
appointmentsOpen.value = false
|
||||
completionRequest.value = null; releaseRequest.value = null; endLocation.value = ''
|
||||
try { const result = await getTrip(String(route.params.tripNo)); if (current === generation) accept(result) }
|
||||
catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '车次详情加载失败') }
|
||||
@@ -202,6 +206,10 @@ onBeforeUnmount(() => { generation++; inspectionSequence++; historySequence++ })
|
||||
<DataTable :data="detail.dispatches" :columns="bindingsColumns" row-key="dispatchNo" empty-text="尚未关联排车单"><template #actions="{ row }"><div class="density-actions"><el-button v-if="access.can('platform:overseas-warehouse:dispatch:list')" class="density-action" @click="router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } })">详情</el-button><el-button v-if="editable" class="density-action" data-tone="danger" :disabled="!!busy" @click="unbind(row.dispatchNo)">解除关联</el-button><el-button v-if="planned && access.can(permission.tripDispatchReview)" class="density-action" :disabled="!!busy || row.review.status === 'APPROVED'" @click="review('APPROVED', row.dispatchNo)">审核通过</el-button><el-button v-if="planned && access.can(permission.tripDispatchReview)" class="density-action" data-tone="danger" :disabled="!!busy" @click="review('REJECTED', row.dispatchNo)">打回</el-button></div></template><template #review="{ row }"><span>{{ tripReviewLabels[row.review.status as keyof typeof tripReviewLabels] }}</span><p v-if="row.review.reason">{{ row.review.reason }}</p><span v-if="row.review.reviewedAt">{{ time(row.review.reviewedAt) }}</span></template><template #boundAt="{ row }">{{ time(row.boundAt) }}</template></DataTable>
|
||||
</FormSection>
|
||||
</template>
|
||||
<FormSection v-show="activeTab === 'overview'" v-if="access.can('platform:appointment:list')" class="density-form-section" title="客户预约与后送去向">
|
||||
<el-button v-if="!appointmentsOpen" @click="appointmentsOpen = true">核对客户预约与后送去向</el-button>
|
||||
<TripAppointmentPanel v-if="appointmentsOpen" ref="appointmentsPanel" :key="detail.trip.tripNo" :dispatches="detail.dispatches" :previous-dispatches="detail.previousDispatches" :history-unavailable-reason="detail.appointmentHistoryUnavailableReason" :can-write="planned && !busy" @updated="refreshLinked" @work="key => router.push({ path: '/appointment-calendar', query: { workItem: key } })" @external="path => router.push(path)" />
|
||||
</FormSection>
|
||||
<FormSection v-if="activeTab === 'bookings'" class="density-form-section" title="本车外接预订"><TripBookingPanel :key="detail.trip.tripNo" :trip="detail.trip" @changed="detail.trip.recordVersion = $event" @linked="refreshLinked" /></FormSection>
|
||||
<FormSection v-if="activeTab === 'handovers'" class="density-form-section" title="在途车辆与司机交接"><TripHandoverPanel :key="detail.trip.tripNo" :detail="detail" @saved="refreshLinked" /></FormSection>
|
||||
<FormSection v-if="activeTab === 'costs'" class="density-form-section" title="本趟直接成本"><TripCostPanel :key="detail.trip.tripNo" :trip-no="detail.trip.tripNo" /></FormSection>
|
||||
|
||||
@@ -22,7 +22,7 @@ const trip: FleetTrip = { tripNo: 'TR-TEST', status: 'PLANNED', warehouseId: '92
|
||||
plannedDepartureAt: '2026-10-03T00:00:00Z', expectedDriverEndAt: '2026-10-04T00:00:00Z', departedAt: null, completedAt: null,
|
||||
stops: [{ name: '起点' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }], remark: null, recordVersion: 3, updatedAt: '2026-10-02T00:00:00Z', vehicleRelease: null, driverRelease: null }
|
||||
const reviewState = { status: 'PENDING' as const, reason: null, reviewerId: null, reviewedAt: null, recordVersion: null }
|
||||
const detail: TripDetail = { trip, driverQualification: null, dispatches: [], loadingReview: reviewState }
|
||||
const detail: TripDetail = { trip, driverQualification: null, previousDispatches: [], appointmentHistoryUnavailableReason: null, dispatches: [], loadingReview: reviewState }
|
||||
const layout = { template: '<div><slot name="header"/><slot/></div>' }
|
||||
const input = { name: 'ElInput', props: ['modelValue'], emits: ['update:modelValue'], template: '<input :value="modelValue" @input="$emit(\'update:modelValue\',$event.target.value)" />' }
|
||||
const dialog = { name: 'AppDialog', props: ['modelValue'], emits: ['confirm'], template: '<div v-if="modelValue"><slot/><button @click="$emit(\'confirm\')">保存计划</button></div>' }
|
||||
@@ -53,6 +53,18 @@ it('returns from a trip to the exact appointment work context and preserves it w
|
||||
const page = mount(TripDetailView); await flushPromises(); await button(page, '返回预约工作').trigger('click')
|
||||
expect(state.push).toHaveBeenLastCalledWith(tripQuery.appointmentReturn)
|
||||
})
|
||||
it('loads customer appointment cards only on request and navigates using a resolved work key', async () => {
|
||||
const previousDispatches = [{ dispatchNo: 'DS-REMOVED', customerId: 'A', recordVersion: 2, recordedAt: trip.updatedAt, appointmentBatches: [] }]
|
||||
vi.mocked(api.getTrip).mockResolvedValue({ ...structuredClone(detail), previousDispatches, appointmentHistoryUnavailableReason: '历史证据不完整' })
|
||||
const page = mount(TripDetailView); await flushPromises()
|
||||
expect(page.findComponent({ name: 'TripAppointmentPanel' }).exists()).toBe(false)
|
||||
await button(page, '核对客户预约与后送去向').trigger('click'); await flushPromises()
|
||||
const panel = page.getComponent({ name: 'TripAppointmentPanel' })
|
||||
expect(panel.props()).toMatchObject({ previousDispatches, historyUnavailableReason: '历史证据不完整' })
|
||||
expect(panel.props('dispatches')).toEqual([])
|
||||
panel.vm.$emit('work', 'real-child-work'); await flushPromises()
|
||||
expect(state.push).toHaveBeenLastCalledWith({ path: '/appointment-calendar', query: { workItem: 'real-child-work' } })
|
||||
})
|
||||
|
||||
it('shows mixed signature and return quantities from one lazy completion read', async () => {
|
||||
vi.mocked(api.getTrip).mockResolvedValue({ ...structuredClone(detail), trip: { ...trip, status: 'IN_TRANSIT' } })
|
||||
@@ -129,7 +141,7 @@ it('counts a shared physical pallet once and submits only operator-entered measu
|
||||
})
|
||||
|
||||
it('submits a specific dispatch review with optional approval notes and a reason when rejecting', async () => {
|
||||
const pending: TripDetail = { ...structuredClone(detail), dispatches: [{ dispatchNo: 'DP-A', customerId: 'A', packageCount: 10, boundBy: '1', boundAt: trip.updatedAt, review: reviewState }] }
|
||||
const pending: TripDetail = { ...structuredClone(detail), dispatches: [{ appointmentBatches: [], dispatchNo: 'DP-A', customerId: 'A', packageCount: 10, boundBy: '1', boundAt: trip.updatedAt, review: reviewState }] }
|
||||
vi.mocked(api.getTrip).mockResolvedValue(pending)
|
||||
vi.mocked(api.reviewTripDispatch).mockResolvedValue({ ...pending, trip: { ...trip, recordVersion: 4 } })
|
||||
const wrapper = mount(TripDetailView); await flushPromises()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DispatchBatchReference } from '@/modules/overseas-warehouse'
|
||||
import type { TripBooking } from './bookingTypes'
|
||||
import type { DriverQualification } from './driverTypes'
|
||||
|
||||
@@ -34,8 +35,9 @@ export interface FleetTrip {
|
||||
export interface TripReviewState { status: 'PENDING' | 'APPROVED' | 'REJECTED'; reason: string | null; reviewerId: string | null; reviewedAt: string | null; recordVersion: number | null }
|
||||
export interface TripReviewCommand { recordVersion: number; decision: 'APPROVED' | 'REJECTED'; reason?: string | null }
|
||||
export const tripReviewLabels = { PENDING: '待审核', APPROVED: '已通过', REJECTED: '已打回' } as const
|
||||
export interface TripBinding { dispatchNo: string; customerId: string; packageCount: number; boundBy: string; boundAt: string; review: TripReviewState }
|
||||
export interface TripDetail { trip: FleetTrip; driverQualification: DriverQualification | null; dispatches: TripBinding[]; loadingReview: TripReviewState }
|
||||
export interface TripPreviousDispatch { dispatchNo: string; customerId: string; recordVersion: number; recordedAt: string; appointmentBatches: DispatchBatchReference[] }
|
||||
export interface TripBinding { appointmentBatches: DispatchBatchReference[]; dispatchNo: string; customerId: string; packageCount: number; boundBy: string; boundAt: string; review: TripReviewState }
|
||||
export interface TripDetail { previousDispatches: TripPreviousDispatch[]; appointmentHistoryUnavailableReason: string | null; trip: FleetTrip; driverQualification: DriverQualification | null; dispatches: TripBinding[]; loadingReview: TripReviewState }
|
||||
export interface TripDraft { warehouseId: string; plannedDepartureAt: string; expectedDriverEndAt: string; stops: TripStop[]; remark?: string | null; idempotencyKey: string }
|
||||
export interface TripPlan { recordVersion: number; plannedDepartureAt: string; expectedDriverEndAt: string; stops: TripStop[]; remark?: string | null; reason: string }
|
||||
export interface TripAssignment { recordVersion: number; vehicleId: string; driverId: string; reason: string }
|
||||
|
||||
Reference in New Issue
Block a user