diff --git a/src/modules/trucking/api/platformTrucking.test.ts b/src/modules/trucking/api/platformTrucking.test.ts index 784b5f5f..108d5043 100644 --- a/src/modules/trucking/api/platformTrucking.test.ts +++ b/src/modules/trucking/api/platformTrucking.test.ts @@ -19,17 +19,17 @@ describe('truck appointment API', () => { vi.mocked(http.post).mockResolvedValue({ data: { id: '9001', status: 'DISPATCHED' } }) await expect(getTruckAppointment('9001')).resolves.toMatchObject({ id: '9001' }) await expect(createTruckAppointment({ carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: null, plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0 })).resolves.toMatchObject({ id: '9001' }) - expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/truck-appointments', { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: null, plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0 }) + expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/truck-appointments', { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: null, plannedLoadingAt: '2026-08-10T10:00:00', currency: 'RMB', expectedVersion: 0 }) await expect(performTruckAppointmentAction('9001', 'DISPATCH', 2)).resolves.toMatchObject({ status: 'DISPATCHED' }) expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments/9001/dispatch', { expectedVersion: 2, reason: null }, { headers: { 'Idempotency-Key': expect.stringContaining('truck-appointment-dispatch-9001-') } }) }) it('drops legacy customer fields instead of forwarding hidden compatibility data', async () => { vi.mocked(http.post).mockResolvedValueOnce({ data: { id: '9001', status: 'DRAFT', attachmentCount: 0 } }) - const payload = { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0, customerId: 'legacy-customer', customerName: '旧客户快照' } + const payload = { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0, currency: 'EUR', receivableAmount: '999.00', customerId: 'legacy-customer', customerName: '旧客户快照' } await createTruckAppointment(payload as never) expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments', { - carrierId: '200', oceanBookingId: '300', containerBookingId: '400', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0, + carrierId: '200', oceanBookingId: '300', containerBookingId: '400', plannedLoadingAt: '2026-08-10T10:00:00', currency: 'RMB', expectedVersion: 0, }) }) diff --git a/src/modules/trucking/api/platformTrucking.ts b/src/modules/trucking/api/platformTrucking.ts index 9e58410c..36d4c3a1 100644 --- a/src/modules/trucking/api/platformTrucking.ts +++ b/src/modules/trucking/api/platformTrucking.ts @@ -1,5 +1,5 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' -import { TRUCK_APPOINTMENT_SAVE_FIELDS } from '../types' +import { TRUCK_APPOINTMENT_SAVE_FIELDS, normalizeTruckAppointmentCurrency } from '../types' import type { SaveTruckAppointmentRequest, TruckAppointmentPage, @@ -17,7 +17,7 @@ function operationId(action: string, id: string | number) { function dtoPayload(payload: SaveTruckAppointmentRequest) { return Object.fromEntries(TRUCK_APPOINTMENT_SAVE_FIELDS.flatMap((key) => { - const value = payload[key] + const value = key === 'currency' ? normalizeTruckAppointmentCurrency(payload[key]) : payload[key] return value === undefined ? [] : [[key, value === '' ? null : value]] })) as unknown as SaveTruckAppointmentRequest } diff --git a/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts b/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts index 5047e616..b279b8a6 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts @@ -122,4 +122,21 @@ describe('TruckAppointmentForm booking-only behavior', () => { expect(emitted.containerBookingId).toBeNull() wrapper.unmount() }) + + it('switches the payable currency between RMB and USD and normalizes an illegal legacy value', async () => { + const wrapper = mount(TruckAppointmentForm, { + props: { modelValue: { ...model, payableAmount: '88.00', currency: 'EUR' as never } }, + global: { plugins: [ElementPlus] }, + }) + await flushPromises() + const currencySelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('placeholder') === '请选择币种')! + expect(currencySelect.props('modelValue')).toBe('RMB') + currencySelect.vm.$emit('update:modelValue', 'USD') + await flushPromises() + const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest + expect(emitted.currency).toBe('USD') + expect(wrapper.text()).toContain('USD') + expect(emitted).not.toHaveProperty('receivableAmount') + wrapper.unmount() + }) }) diff --git a/src/modules/trucking/components/TruckAppointmentForm.test.ts b/src/modules/trucking/components/TruckAppointmentForm.test.ts index 73d32c95..d87c4d2b 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.test.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' const source = readFileSync(resolve(process.cwd(), 'src/modules/trucking/components/TruckAppointmentForm.vue'), 'utf8') +const typesSource = readFileSync(resolve(process.cwd(), 'src/modules/trucking/types.ts'), 'utf8') describe('TruckAppointmentForm interaction contract', () => { it('requires a released booking before a real-numbered allocated container', () => { @@ -68,4 +69,22 @@ describe('TruckAppointmentForm interaction contract', () => { expect(source).toContain(':deep(.el-select__selected-item)') expect(source).toContain('text-overflow:ellipsis') }) + + it('uses payable-only fees with a fixed dynamic currency selector', () => { + expect(typesSource).toContain("TRUCK_APPOINTMENT_CURRENCIES = ['RMB', 'USD']") + expect(source).toContain('{{ draft.currency }}') + expect(source).toContain('v-for="currency in TRUCK_APPOINTMENT_CURRENCIES"') + expect(source).toContain('placeholder="请选择币种"') + expect(source).not.toContain('receivableAmount') + expect(source).not.toContain('应收拖车费') + }) + + it('uses four compact desktop columns while retaining two-column relations and wide text fields', () => { + expect(source).toContain('@media (min-width: 1200px)') + expect(source).toContain('max-width:25%; flex:0 0 25%') + expect(source).toContain('class="relation-column"') + expect(source).toContain('class="wide-column"') + expect(source).toContain('max-width:50%; flex-basis:50%') + expect(source).not.toContain('overflow-x:auto') + }) }) diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index a9ee6ae0..04cf933e 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -2,7 +2,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import type { FormInstance, FormRules } from 'element-plus' import { Eye, Save } from '@lucide/vue' -import type { SaveTruckAppointmentRequest, TruckAppointmentResource } from '../types' +import { TRUCK_APPOINTMENT_CURRENCIES, normalizeTruckAppointmentCurrency, type SaveTruckAppointmentRequest, type TruckAppointmentResource } from '../types' import { listSuppliers, type Supplier } from '@/modules/suppliers' import { getOceanBooking, listOceanBookings, type OceanBookingAllocatedContainer, type OceanBookingDocument, type OceanBookingResource } from '@/modules/ocean-bookings' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -16,8 +16,8 @@ const relationWarning = ref('') const bookingContainersLoading = ref(false) const bookingContainersError = ref(null) const bookingDocuments = ref([]) -const defaults: Partial = { expectedVersion: 0, currency: 'RMB', payableAmount: '0.00', receivableAmount: '0.00', packageCount: 0, palletCount: 0, transitMinutes: 0 } -function normalizeDraft(value: SaveTruckAppointmentRequest): SaveTruckAppointmentRequest { return { ...defaults, ...value, currency: value.currency?.trim().toUpperCase() || 'RMB' } } +const defaults: Partial = { expectedVersion: 0, currency: 'RMB', payableAmount: '0.00', packageCount: 0, palletCount: 0, transitMinutes: 0 } +function normalizeDraft(value: SaveTruckAppointmentRequest): SaveTruckAppointmentRequest { return { ...defaults, ...value, currency: normalizeTruckAppointmentCurrency(value.currency) } } const draft = reactive(normalizeDraft(props.modelValue)) watch(() => props.modelValue, (value) => Object.assign(draft, normalizeDraft(value)), { deep: true }) watch(draft, (value) => emit('update:modelValue', { ...value }), { deep: true }) @@ -42,8 +42,7 @@ const rules: FormRules = { plannedLoadingAt: [{ required: true, message: '请选择预计装箱时间', trigger: 'change' }], fleetPhone: [phoneRule('车队电话')], driverPhone: [phoneRule('司机电话')], storePhone: [phoneRule('门店电话')], packageCount: [nonNegative('件数', true)], palletCount: [nonNegative('托盘数量', true)], transitMinutes: [nonNegative('在途分钟', true)], - grossWeightKg: [nonNegative('毛重')], volumeCbm: [nonNegative('体积')], payableAmount: [nonNegative('应付拖车费')], receivableAmount: [nonNegative('应收拖车费')], - currency: [{ pattern: /^[A-Z]{3,8}$/, message: '币种请输入 3-8 位大写字母(如 RMB)', trigger: 'blur' }], + grossWeightKg: [nonNegative('毛重')], volumeCbm: [nonNegative('体积')], payableAmount: [nonNegative('应付拖车费')], } const carriers = ref([]) const containers = ref([]) @@ -160,8 +159,8 @@ watch(() => props.readonly, (readonly) => { if (!readonly && !bookings.value.len 重新加载选项 订舱与柜次(必填) - - {{ bookingContainersError.message }}重新加载只有已分配至该订舱且已录入真实 ISO 集装箱号的柜次可以预约拖车。 + + {{ bookingContainersError.message }}重新加载只有已分配至该订舱且已录入真实 ISO 集装箱号的柜次可以预约拖车。 基本信息 @@ -184,17 +183,16 @@ watch(() => props.readonly, (readonly) => { if (!readonly && !bookings.value.len 费用信息 - RMB - RMB - + {{ draft.currency }} + 门店信息与备注 - + - + 查看附件引用保存 @@ -214,6 +212,10 @@ watch(() => props.readonly, (readonly) => { if (!readonly && !bookings.value.len :deep(.el-input), :deep(.el-textarea), :deep(.el-date-editor) { max-width: 100%; } :deep(.el-select__selected-item), :deep(.el-select-dropdown__item) { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .field-error,.field-hint { display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:4px;color:var(--cc-muted);font-size:12px;line-height:20px }.field-error{color:var(--el-color-danger)} +@media (min-width: 1200px) { + .truck-form :deep(.el-col-sm-12), .truck-form :deep(.el-col-sm-8) { max-width:25%; flex:0 0 25%; } + .truck-form :deep(.relation-column), .truck-form :deep(.wide-column) { max-width:50%; flex-basis:50%; } +} @media (max-width: 480px) { .truck-form { padding-inline: 2px; } .relation-note { align-items:flex-start; flex-direction:column; } diff --git a/src/modules/trucking/pages/TruckAppointmentCreateView.test.ts b/src/modules/trucking/pages/TruckAppointmentCreateView.test.ts index 8d1a6a8a..2e0926c7 100644 --- a/src/modules/trucking/pages/TruckAppointmentCreateView.test.ts +++ b/src/modules/trucking/pages/TruckAppointmentCreateView.test.ts @@ -10,6 +10,8 @@ describe('TruckAppointmentCreateView booking-bound creation contract', () => { expect(source).not.toContain('customerId') expect(source).toContain('oceanBookingId: null, containerBookingId: null, loadingPlanId: null') expect(source).toContain('plannedLoadingAt: null') + expect(source).toContain("payableAmount: '0.00', currency: 'RMB'") + expect(source).not.toContain('receivableAmount') expect(source).toContain('createTruckAppointment({ ...form })') }) diff --git a/src/modules/trucking/pages/TruckAppointmentCreateView.vue b/src/modules/trucking/pages/TruckAppointmentCreateView.vue index 0905a527..f633bfde 100644 --- a/src/modules/trucking/pages/TruckAppointmentCreateView.vue +++ b/src/modules/trucking/pages/TruckAppointmentCreateView.vue @@ -13,7 +13,7 @@ import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue' import { createTruckAppointment } from '../api/platformTrucking' import type { SaveTruckAppointmentRequest } from '../types' const router = useRouter(); const saving = ref(false); const attachmentOpen = ref(false); const error = ref(null) -const form = reactive({ expectedVersion: 0, carrierId: null, oceanBookingId: null, containerBookingId: null, loadingPlanId: null, plannedLoadingAt: null, payableAmount: '0.00', receivableAmount: '0.00', currency: 'RMB' }) +const form = reactive({ expectedVersion: 0, carrierId: null, oceanBookingId: null, containerBookingId: null, loadingPlanId: null, plannedLoadingAt: null, payableAmount: '0.00', currency: 'RMB' }) async function submit() { if (saving.value) return; saving.value = true; error.value = null; try { await createTruckAppointment({ ...form }); ElMessage.success('拖车预约已创建'); router.push({ name: 'trucking' }) } catch (err) { error.value = getApiErrorDetails(err, '创建失败,请稍后重试') } finally { saving.value = false } } diff --git a/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts b/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts index db143079..eb96c572 100644 --- a/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts +++ b/src/modules/trucking/pages/TruckAppointmentDetailView.test.ts @@ -36,6 +36,7 @@ describe('TruckAppointmentDetailView status contract', () => { expect(source).toContain("carrierId: null, oceanBookingId: null, containerBookingId: null, plannedLoadingAt: null") expect(source).not.toContain('customerId') expect(source).toContain('TRUCK_APPOINTMENT_SAVE_FIELDS.map') + expect(source).toContain('normalizeTruckAppointmentCurrency(result.currency)') expect(source).not.toContain('Object.assign(form, { ...result') expect(source).toContain("error.value = getApiErrorDetails(err, '保存失败,请稍后重试')") }) diff --git a/src/modules/trucking/pages/TruckAppointmentDetailView.vue b/src/modules/trucking/pages/TruckAppointmentDetailView.vue index 1d151ad9..f398a6d1 100644 --- a/src/modules/trucking/pages/TruckAppointmentDetailView.vue +++ b/src/modules/trucking/pages/TruckAppointmentDetailView.vue @@ -13,7 +13,7 @@ import { getOceanBooking, type OceanBookingDetail } from '@/modules/ocean-bookin import { getTruckAppointment, listTruckAppointmentAttachments, listTruckAppointmentHistory, performTruckAppointmentAction, rescheduleTruckAppointment, updateTruckAppointment, type TruckAppointmentAction } from '../api/platformTrucking' import TruckAppointmentForm from '../components/TruckAppointmentForm.vue' import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue' -import { TRUCK_APPOINTMENT_SAVE_FIELDS, truckAppointmentActionLabel, truckAppointmentStatusLabel, truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAppointmentStatusHistory } from '../types' +import { TRUCK_APPOINTMENT_SAVE_FIELDS, normalizeTruckAppointmentCurrency, truckAppointmentActionLabel, truckAppointmentStatusLabel, truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAppointmentStatusHistory } from '../types' import { isIso6346ContainerNumber } from '../iso6346' const route = useRoute(); const router = useRouter(); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref(null); const error = ref(null); const form = reactive({ expectedVersion: 0, carrierId: null, oceanBookingId: null, containerBookingId: null, plannedLoadingAt: null }) const history = ref([]) @@ -35,7 +35,7 @@ async function loadRelated() { if (booking.status === 'fulfilled') bookingReadiness.value = booking.value if (attachments.status === 'rejected' || histories.status === 'rejected' || booking.status === 'rejected') relatedError.value = '附件、状态历史或订舱资料状态暂时未能加载,主详情数据不受影响;派车时仍由服务端最终校验。' } -async function load() { loading.value = true; error.value = null; resource.value = null; bookingReadiness.value = null; history.value = []; relatedError.value = ''; try { const result = await getTruckAppointment(id.value); resource.value = result; const draft = Object.fromEntries(TRUCK_APPOINTMENT_SAVE_FIELDS.map((key) => [key, key === 'expectedVersion' ? result.version ?? 0 : result[key as keyof TruckAppointmentResource]])); Object.assign(form, draft); await loadRelated() } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } } +async function load() { loading.value = true; error.value = null; resource.value = null; bookingReadiness.value = null; history.value = []; relatedError.value = ''; try { const result = await getTruckAppointment(id.value); resource.value = result; const draft = Object.fromEntries(TRUCK_APPOINTMENT_SAVE_FIELDS.map((key) => [key, key === 'expectedVersion' ? result.version ?? 0 : key === 'currency' ? normalizeTruckAppointmentCurrency(result.currency) : result[key as keyof TruckAppointmentResource]])); Object.assign(form, draft); await loadRelated() } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } } async function submit() { if (saving.value) return; saving.value = true; error.value = null; try { await updateTruckAppointment(id.value, form); ElMessage.success('拖车预约已保存'); await load() } catch (err) { error.value = getApiErrorDetails(err, '保存失败,请稍后重试') } finally { saving.value = false } } const actionByTarget: Partial> = { PENDING_DISPATCH: 'SUBMIT', DISPATCHED: 'DISPATCH', PICKUP_IN_PROGRESS: 'PICKUP', ARRIVED_WAREHOUSE: 'ARRIVE', LOADING: 'LOAD', DEPARTED: 'DEPART', COMPLETED: 'COMPLETE', EXCEPTION: 'EXCEPTION', CANCELLED: 'CANCEL' } const primaryTargetByStatus: Partial> = { DRAFT: 'PENDING_DISPATCH', PENDING_DISPATCH: 'DISPATCHED', RESCHEDULED: 'DISPATCHED', DISPATCHED: 'PICKUP_IN_PROGRESS', PICKUP_IN_PROGRESS: 'ARRIVED_WAREHOUSE', ARRIVED_WAREHOUSE: 'LOADING', LOADING: 'DEPARTED', DEPARTED: 'COMPLETED' } diff --git a/src/modules/trucking/pages/TruckAppointmentsView.test.ts b/src/modules/trucking/pages/TruckAppointmentsView.test.ts index ed5c4abc..242c1c96 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.test.ts +++ b/src/modules/trucking/pages/TruckAppointmentsView.test.ts @@ -21,7 +21,7 @@ vi.mock('@/modules/iam', () => ({ const row: TruckAppointmentResource = { id: '9001', appointmentNo: 'TA-9001', status: 'DISPATCHED', orderNo: 'ORD-1', oceanBookingNo: 'OB-1', containerBookingId: '77', cabinetNo: 'CAB-1', containerNo: 'CONT-1', warehouseName: '义乌仓', shippingCompanyName: '船公司A', bookingAgentName: '代理A', carrierName: '宁波运输', driverName: '王强', vehicleNo: '浙B12345', - plannedLoadingAt: '2026-08-10T10:00:00', payableAmount: '3200.00', receivableAmount: '3800.00', attachmentCount: 2, attachments: [], + plannedLoadingAt: '2026-08-10T10:00:00', payableAmount: '3200.00', currency: 'RMB', attachmentCount: 2, attachments: [], } describe('TruckAppointmentsView', () => { @@ -125,10 +125,10 @@ describe('TruckAppointmentsView', () => { expect(source).not.toContain("key: 'customerName'") expect(source).toContain("key: 'updatedAt'") expect(source).toContain("key: 'payableAmount', label: '应付拖车费'") - expect(source).toContain("key: 'receivableAmount', label: '应收拖车费'") expect(source.indexOf("key: 'store'")).toBeLessThan(source.indexOf("key: 'payableAmount'")) - expect(source.indexOf("key: 'payableAmount'")).toBeLessThan(source.indexOf("key: 'receivableAmount'")) - expect(source.indexOf("key: 'receivableAmount'")).toBeLessThan(source.indexOf("key: 'plannedLoadingAt'")) + expect(source.indexOf("key: 'payableAmount'")).toBeLessThan(source.indexOf("key: 'plannedLoadingAt'")) + expect(source).not.toContain('receivableAmount') + expect(source).not.toContain('应收拖车费') expect(source).toContain("toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })") expect(source).toContain("key: 'attachmentCount'") }) diff --git a/src/modules/trucking/pages/TruckAppointmentsView.vue b/src/modules/trucking/pages/TruckAppointmentsView.vue index dfb3a7cf..2744331c 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.vue +++ b/src/modules/trucking/pages/TruckAppointmentsView.vue @@ -38,7 +38,6 @@ const columns: DataTableColumn[] = [ { key: 'vehicleNo', prop: 'vehicleNo', label: '车头号', minWidth: 120, formatter: (row: TruckAppointmentResource) => row.vehicleNo || '—' }, { key: 'store', label: '门店', minWidth: 140, slotName: 'store' }, { key: 'payableAmount', label: '应付拖车费', minWidth: 130, align: 'right', formatter: (row: TruckAppointmentResource) => formatAmount(row.payableAmount, row.currency) }, - { key: 'receivableAmount', label: '应收拖车费', minWidth: 130, align: 'right', formatter: (row: TruckAppointmentResource) => formatAmount(row.receivableAmount, row.currency) }, { key: 'plannedLoadingAt', label: '预计装箱日期', minWidth: 170, formatter: (row: TruckAppointmentResource) => formatDateTime(row.plannedLoadingAt) }, { key: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'attachmentCount', label: '附件', width: 90, align: 'right', formatter: (row: TruckAppointmentResource) => String(Number(row.attachmentCount ?? 0)) }, diff --git a/src/modules/trucking/types.ts b/src/modules/trucking/types.ts index 53f5c9c3..c5fed852 100644 --- a/src/modules/trucking/types.ts +++ b/src/modules/trucking/types.ts @@ -53,6 +53,12 @@ export function truckAppointmentActionLabel(action: string | null | undefined) { } export type TruckAttachmentType = 'DISPATCH_ORDER' | 'DRIVER_DOCUMENT' | 'VEHICLE_DOCUMENT' | 'OTHER' +export const TRUCK_APPOINTMENT_CURRENCIES = ['RMB', 'USD'] as const +export type TruckAppointmentCurrency = typeof TRUCK_APPOINTMENT_CURRENCIES[number] +export function normalizeTruckAppointmentCurrency(value: unknown): TruckAppointmentCurrency { + const normalized = String(value ?? '').trim().toUpperCase() + return TRUCK_APPOINTMENT_CURRENCIES.includes(normalized as TruckAppointmentCurrency) ? normalized as TruckAppointmentCurrency : 'RMB' +} export interface TruckAttachmentReference { id?: string @@ -121,8 +127,7 @@ export interface TruckAppointmentResource { volumeCbm?: string | null palletCount?: number | string | null payableAmount?: string | null - receivableAmount?: string | null - currency?: string | null + currency?: TruckAppointmentCurrency | null poNo?: string | null remark?: string | null orderId?: string | null @@ -165,8 +170,7 @@ export interface SaveTruckAppointmentRequest { volumeCbm?: string | null palletCount?: number | string | null payableAmount?: string | null - receivableAmount?: string | null - currency?: string | null + currency?: TruckAppointmentCurrency | null poNo?: string | null remark?: string | null expectedVersion: number @@ -177,7 +181,7 @@ export const TRUCK_APPOINTMENT_SAVE_FIELDS = [ 'carrierId', 'carrierName', 'billOfLadingNo', 'fleetContact', 'fleetPhone', 'driverName', 'driverPhone', 'driverIdCard', 'vehicleNo', 'storeName', 'storeContact', 'storePhone', 'storeAddress', 'storeCity', 'storeZipcode', 'plannedLoadingAt', 'transitMinutes', 'packageCount', - 'grossWeightKg', 'volumeCbm', 'palletCount', 'payableAmount', 'receivableAmount', 'currency', + 'grossWeightKg', 'volumeCbm', 'palletCount', 'payableAmount', 'currency', 'poNo', 'remark', 'expectedVersion', ] as const satisfies readonly (keyof SaveTruckAppointmentRequest)[]