From 1a52cc0817e68c967a71469f0c0528e4ffddaac4 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 3 Oct 2026 10:56:47 +0800 Subject: [PATCH] fix(fleet): normalize nullable maintenance fields before editing --- .../api/vehicleCare.test.ts | 9 +++++++++ .../overseas-master-data/api/vehicleCare.ts | 20 ++++++++++++------- .../components/VehicleCareBehavior.test.ts | 9 +++++++++ .../components/VehicleCareEditor.vue | 2 +- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/modules/overseas-master-data/api/vehicleCare.test.ts b/src/modules/overseas-master-data/api/vehicleCare.test.ts index c83cb523..f09b6ea5 100644 --- a/src/modules/overseas-master-data/api/vehicleCare.test.ts +++ b/src/modules/overseas-master-data/api/vehicleCare.test.ts @@ -8,6 +8,15 @@ const details: VehicleCareDraft = { recordType: 'FUEL', occurredAt: '2026-01-01T const wire = { recordNo: 'VR-ONE', vehicleId: id, vehiclePlate: 'TEST', recordVersion: '2', details: { ...details, odometer: '100', fuelQuantity: '28.6' }, files: [{ fileId: '9223372036854775702', fileName: 'meter.png', contentType: 'image/png', sizeBytes: '8' }], updatedBy: 'operator', updatedAt: '2026-01-01T00:00:00Z' } const ok = (data: unknown) => ({ code: '200', message: '成功', data }) beforeEach(() => vi.clearAllMocks()) +it('normalizes omitted nullable incident fields without injecting maintenance defaults', async () => { + const repair = { ...wire, details: { recordType: 'REPAIR', occurredAt: details.occurredAt, + incident: { description: '轮胎维修', item: '换胎', place: '现场', processingStatus: 'PENDING', reportedCost: '125.50', currency: 'USD' } } } + vi.mocked(http.get).mockResolvedValue(ok({ records: [repair], page: 1, size: 10, total: 1, pages: 1 })) + const result = (await listVehicleCare(id, 'REPAIR')).records[0]! + expect(result.details).toMatchObject({ distanceUnit: null, odometer: null, fuelUnit: null, verificationStatus: null, + lastServiceOdometer: null, nextServiceOdometer: null, fuelQuantity: null, remark: null, + incident: { handler: null, exceptionType: null, reportedCost: 125.5, currency: 'USD' } }) +}) it('keeps vehicle and file IDs exact while normalizing values without changing units', async () => { vi.mocked(http.get).mockResolvedValue(ok({ records: [wire], page: '1', size: '10', total: '1', pages: '1' })) const result = await listVehicleCare(id, 'FUEL') diff --git a/src/modules/overseas-master-data/api/vehicleCare.ts b/src/modules/overseas-master-data/api/vehicleCare.ts index 96d499d3..dcfe5c54 100644 --- a/src/modules/overseas-master-data/api/vehicleCare.ts +++ b/src/modules/overseas-master-data/api/vehicleCare.ts @@ -6,9 +6,9 @@ import type { PageResult } from './overseasMasterData' type Numeric = number | string type Wire = Omit & { recordVersion: Numeric - details: Omit & { - odometer: Numeric | null; lastServiceOdometer: Numeric | null; nextServiceOdometer: Numeric | null; fuelQuantity: Numeric | null - incident: (Omit & { reportedCost: Numeric | null }) | null + details: Pick & Partial> & { + odometer?: Numeric | null; lastServiceOdometer?: Numeric | null; nextServiceOdometer?: Numeric | null; fuelQuantity?: Numeric | null + incident?: (Pick & Partial> & { reportedCost?: Numeric | null }) | null } files: Array & { sizeBytes: Numeric }> } @@ -24,10 +24,16 @@ function decimal(value: Numeric): number { return result } function record(value: Wire): VehicleCareRecord { - const optional = (number: Numeric | null) => number == null ? null : decimal(number) - return { ...value, recordVersion: integer(value.recordVersion), details: { ...value.details, odometer: optional(value.details.odometer), - lastServiceOdometer: optional(value.details.lastServiceOdometer), nextServiceOdometer: optional(value.details.nextServiceOdometer), fuelQuantity: optional(value.details.fuelQuantity), - incident: value.details.incident ? { ...value.details.incident, reportedCost: optional(value.details.incident.reportedCost) } : null }, + const optional = (number: Numeric | null | undefined) => number == null ? null : decimal(number) + const details = value.details, incident = details.incident + return { ...value, recordVersion: integer(value.recordVersion), paymentStatus: value.paymentStatus ?? null, details: { + recordType: details.recordType, occurredAt: details.occurredAt, distanceUnit: details.distanceUnit ?? null, + odometer: optional(details.odometer), lastServiceOdometer: optional(details.lastServiceOdometer), + nextServiceOdometer: optional(details.nextServiceOdometer), fuelQuantity: optional(details.fuelQuantity), + fuelUnit: details.fuelUnit ?? null, verificationStatus: details.verificationStatus ?? null, remark: details.remark ?? null, + incident: incident ? { description: incident.description, processingStatus: incident.processingStatus, + item: incident.item ?? null, place: incident.place ?? null, handler: incident.handler ?? null, + exceptionType: incident.exceptionType ?? null, reportedCost: optional(incident.reportedCost), currency: incident.currency ?? null } : null }, files: value.files.map(file => ({ ...file, sizeBytes: integer(file.sizeBytes) })) } } export async function listVehicleCare(vehicleId: string, recordType: VehicleCareType, page = 1, size = 10): Promise> { diff --git a/src/modules/overseas-master-data/components/VehicleCareBehavior.test.ts b/src/modules/overseas-master-data/components/VehicleCareBehavior.test.ts index d35bfe96..f6d78bce 100644 --- a/src/modules/overseas-master-data/components/VehicleCareBehavior.test.ts +++ b/src/modules/overseas-master-data/components/VehicleCareBehavior.test.ts @@ -29,6 +29,15 @@ function mount(component: object, props: Record): VueWrapper { } beforeEach(() => { vi.clearAllMocks(); access.write = true; vi.mocked(api.listVehicleCare).mockResolvedValue({ records: [structuredClone(row)], page: 1, size: 10, total: 1, pages: 1 }) }) afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount())) +it('saving an unchanged record does not create a correction due to JSON property order', async () => { + const record = structuredClone(row) + record.details = Object.fromEntries(Object.entries(record.details).reverse()) as typeof record.details + const wrapper = mount(VehicleCareEditor, { modelValue: false, vehicleId: '71', plate: 'VEH-A', recordType: 'FUEL', record }) + await wrapper.setProps({ modelValue: true }) + wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + expect(api.updateVehicleCare).not.toHaveBeenCalled() + expect(wrapper.emitted('saved')).toHaveLength(1) +}) it('loads care only after the detail tab is selected', async () => { vi.mocked(getVehicle).mockResolvedValue({ id: '71', plateNumber: 'VEH-A', model: 'Truck', fuelConsumptionPer100Km: 10, mileageKm: 1000, region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', recordVersion: 0 } as Awaited>) const wrapper = mount(VehicleDetailView, {}); await flushPromises() diff --git a/src/modules/overseas-master-data/components/VehicleCareEditor.vue b/src/modules/overseas-master-data/components/VehicleCareEditor.vue index 8d8a62d8..039cdcd6 100644 --- a/src/modules/overseas-master-data/components/VehicleCareEditor.vue +++ b/src/modules/overseas-master-data/components/VehicleCareEditor.vue @@ -38,7 +38,7 @@ watch([open, identity], ([value, target], [, previousTarget]) => { lastServiceOdometer: null, nextServiceOdometer: null, fuelQuantity: null, fuelUnit: props.recordType === 'FUEL' ? 'L' : null, verificationStatus: meterRecord.value ? 'PENDING' : null, remark: null, incident: meterRecord.value ? null : { description: '', item: null, place: null, handler: null, processingStatus: 'PENDING', exceptionType: null, reportedCost: null, currency: null } }) - lastSaved = props.record ? JSON.stringify(props.record.details) : '' + lastSaved = props.record ? JSON.stringify(form) : '' }) function selectFiles(event: Event) { const element = event.target as HTMLInputElement