diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index d45a60b5..fce3cb8c 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -6,6 +6,7 @@ import { getContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, getCustomerContainerTransitSummary, + createContainerSimulation, linkContainerOceanBooking, unlinkContainerOceanBooking, updateContainer, updateContainerTransport, } from './platformContainerLoading' @@ -46,6 +47,17 @@ describe('platform container loading API', () => { expect(http.patch).toHaveBeenCalledWith('/platform/v1/containers/7/transport', { version: 3, containerNo: 'MSCU1', voyageNo: 'V8' }) }) + it('uses the frozen simulation create and ocean booking link contracts', async () => { + vi.mocked(http.post).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: 101, oceanBookingRequirementId: 202 } }) + vi.mocked(http.delete).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: null, oceanBookingRequirementId: null } }) + await createContainerSimulation({ channelCode: 'NY_EXX', warehouseId: '8', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000 }) + await linkContainerOceanBooking('7', { oceanBookingId: '101', requirementId: '202', expectedVersion: 3 }) + await unlinkContainerOceanBooking('7', 4, '空柜改配') + expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/containers', { channelCode: 'NY_EXX', warehouseId: '8', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000 }) + expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/containers/7/ocean-booking-link', { oceanBookingId: '101', requirementId: '202', expectedVersion: 3 }) + expect(http.delete).toHaveBeenCalledWith('/platform/v1/containers/7/ocean-booking-link', { data: { expectedVersion: 4, reason: '空柜改配' } }) + }) + it('normalizes every long id in candidates and plans', async () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', data: container }) diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index 54f05bac..ab6ef314 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -13,6 +13,8 @@ import type { ContainerPage, ContainerQuery, ContainerSaveRequest, + ContainerSimulationCreateRequest, + ContainerOceanBookingLinkRequest, SmartLoadingCandidate, SmartLoadingItem, SmartLoadingPlan, @@ -68,6 +70,9 @@ function container(raw: ContainerBookingResource): ContainerBookingResource { id: id(raw.id), planId: optionalId(raw.planId), warehouseId: id(raw.warehouseId), + oceanBookingId: optionalId(raw.oceanBookingId), + oceanBookingRequirementId: optionalId(raw.oceanBookingRequirementId), + customerId: optionalId(raw.customerId), shippingCompanyId: optionalId(raw.shippingCompanyId), version: numberValue(raw.version), capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm), @@ -149,11 +154,21 @@ export function downloadContainerPdaPhoto(downloadPath: string) { return http.get(downloadPath, { responseType: 'blob' }) } -export async function createContainer(payload: ContainerSaveRequest) { +export async function createContainerSimulation(payload: ContainerSimulationCreateRequest) { const response = await http.post, ApiEnvelope>(containerPath, payload) return container(unwrapApiResponse(response)) } +export async function linkContainerOceanBooking(containerId: string, payload: ContainerOceanBookingLinkRequest) { + const response = await http.post, ApiEnvelope>(`${containerPath}/${encodeURIComponent(containerId)}/ocean-booking-link`, payload) + return container(unwrapApiResponse(response)) +} + +export async function unlinkContainerOceanBooking(containerId: string, expectedVersion: number, reason: string) { + const response = await http.delete, ApiEnvelope>(`${containerPath}/${encodeURIComponent(containerId)}/ocean-booking-link`, { data: { expectedVersion, reason } }) + return container(unwrapApiResponse(response)) +} + export async function updateContainer(containerId: string, version: number, data: ContainerSaveRequest) { const response = await http.put, ApiEnvelope>(`${containerPath}/${encodeURIComponent(containerId)}`, { version, data }) return container(unwrapApiResponse(response)) diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index c75213be..f03259cc 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -226,10 +226,24 @@ describe('ContainersView', () => { expect(source).not.toContain('上传入口待') }) - it('does not expose manual container creation without an ocean-booking channel snapshot', () => { + it('creates an internal simulation without asking for real booking transport facts', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') expect(source).not.toContain('新增货柜') - expect(source).not.toContain('createContainer(') + expect(source).toContain('新增模拟柜') + expect(source).toContain('createContainerSimulation({ ...simulationForm })') + expect(source).toContain("channelCode: '', warehouseId: '', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000") + expect(source).toContain('服务端会生成稳定的内部柜次') expect(source).not.toContain('openCreate') }) + + it('allows first linking after preloading but only offers unlinking for wait-loading containers', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') + expect(source).toContain("!row.oceanBookingId && ['WAIT_LOADING','EDITING'].includes(row.planStatus)") + expect(source).toContain("row.oceanBookingId && row.planStatus === 'WAIT_LOADING'") + expect(source).toContain('已预排货但尚未进入 PDA、取证或单证阶段的模拟柜仍可首次关联') + expect(source).toContain('内部柜次与装柜明细保持不变') + expect(source).toContain('只有尚未排货的空柜可以解除') + expect(source).toContain('linkContainerOceanBooking(target.id, { oceanBookingId: candidate.oceanBookingId, requirementId: candidate.requirementId, expectedVersion: target.version })') + expect(source).toContain('unlinkContainerOceanBooking(target.id, target.version, unlinkReason.value.trim())') + }) }) diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index 9eb9f6c2..f0eaed9e 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -1,13 +1,13 @@ @@ -61,8 +60,7 @@ watch([page, pageSize], () => void load()); onMounted(() => { void load(); void
- - + diff --git a/src/modules/ocean-bookings/types.ts b/src/modules/ocean-bookings/types.ts index 68f385ad..dfd639ef 100644 --- a/src/modules/ocean-bookings/types.ts +++ b/src/modules/ocean-bookings/types.ts @@ -2,7 +2,7 @@ export type OceanBookingStatus = 'DRAFT' | 'SUBMITTED' | 'CONFIRMED' | 'RELEASED export interface OceanBookingParty { id?: string; partyType: string; partyName: string; contactName?: string | null; contactPhone?: string | null; address?: string | null } export interface OceanBookingContainer { id?: string; containerType: string; quantity: number | null; containerKind?: string | null; capacityCbm: number | null; maxWeightKg: number | null } -export interface OceanBookingContainerAllocation extends OceanBookingContainer { allocatedQuantity?: number | null } +export interface OceanBookingContainerAllocation { id?: string; oceanBookingId?: string; requirementId?: string; containerBookingId?: string | null; allocatedQuantity?: number | null } export interface OceanBookingDraftData { customerId: string | null; customerName: string | null; parties: OceanBookingParty[]; containerRequirements: OceanBookingContainer[] channelCode: string; warehouseId: string; warehouseName: string