From c4cd4b42b3debbfda9a3b43378f09b2ab351ebe1 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 20 Aug 2026 18:04:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/router/index.test.ts | 6 +- src/core/navigation/adminNavigation.test.ts | 7 +- src/core/navigation/adminNavigation.ts | 7 +- .../API_INTEGRATION_MATRIX.md | 5 +- .../api/inboundOrders.test.ts | 47 +++++ .../overseas-warehouse/api/inboundOrders.ts | 99 +++++++++ .../overseas-warehouse/api/inventory.test.ts | 20 ++ .../overseas-warehouse/api/inventory.ts | 74 +++++++ .../overseas-warehouse/api/pallets.test.ts | 30 +++ src/modules/overseas-warehouse/api/pallets.ts | 84 ++++++++ .../pages/InboundOrdersView.vue | 198 ++++++++++++++++-- .../pages/InventoryView.vue | 90 ++++++-- .../pages/PalletDetailView.vue | 37 +++- .../overseas-warehouse/pages/PalletsView.vue | 159 ++++++++++---- .../pages/prototypeContract.test.ts | 29 ++- src/modules/overseas-warehouse/permissions.ts | 12 +- src/modules/overseas-warehouse/types.ts | 15 +- 17 files changed, 814 insertions(+), 105 deletions(-) create mode 100644 src/modules/overseas-warehouse/api/inboundOrders.test.ts create mode 100644 src/modules/overseas-warehouse/api/inboundOrders.ts create mode 100644 src/modules/overseas-warehouse/api/inventory.test.ts create mode 100644 src/modules/overseas-warehouse/api/inventory.ts create mode 100644 src/modules/overseas-warehouse/api/pallets.test.ts create mode 100644 src/modules/overseas-warehouse/api/pallets.ts diff --git a/src/app/router/index.test.ts b/src/app/router/index.test.ts index 40a612f5..ece2e605 100644 --- a/src/app/router/index.test.ts +++ b/src/app/router/index.test.ts @@ -82,12 +82,12 @@ describe('admin router access metadata', () => { it('registers the confirmed phase-four routes with stable list permissions', () => { const expected = [ ['overseas-last-mile-orders', '/overseas-warehouse/last-mile-orders', 'platform:overseas-warehouse:last-mile-order:list'], - ['overseas-inbound-orders', '/overseas-warehouse/inbound-orders', 'platform:overseas-warehouse:inbound-order:list'], + ['overseas-inbound-orders', '/overseas-warehouse/inbound-orders', 'platform:lastleg:inbound-plan:list'], ['overseas-relabel-orders', '/overseas-warehouse/relabel-orders', 'platform:overseas-warehouse:relabel-order:list'], - ['overseas-pallets', '/overseas-warehouse/pallets', 'platform:overseas-warehouse:pallet:list'], + ['overseas-pallets', '/overseas-warehouse/pallets', 'platform:pallet:list'], ['overseas-cartons', '/overseas-warehouse/cartons', 'platform:overseas-warehouse:carton:list'], ['overseas-products', '/overseas-warehouse/products', 'platform:overseas-warehouse:product:list'], - ['overseas-inventory', '/overseas-warehouse/inventory', 'platform:overseas-warehouse:inventory:list'], + ['overseas-inventory', '/overseas-warehouse/inventory', 'platform:lastleg:inventory:list'], ['overseas-business-purchases', '/overseas-business/purchases', 'platform:overseas-business:purchase:list'], ['overseas-business-trucks', '/overseas-business/trucks', 'platform:overseas-business:truck:list'], ['overseas-business-looseboards', '/overseas-business/looseboards', 'platform:overseas-business:looseboard:list'], diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index 9424586b..42ac5a27 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -32,7 +32,6 @@ describe('adminNavigation', () => { '订单测量', '测量数据审核', '库位管理', - '托盘管理', '整柜业务', ]) expect(adminNavigation.find((item) => item.key === 'service-center')).toBeUndefined() @@ -141,12 +140,12 @@ describe('adminNavigation', () => { it('shows only the confirmed phase-four pages with their list permissions', () => { const filtered = filterAdminNavigation(adminNavigation, new Set([ 'platform:overseas-warehouse:last-mile-order:list', - 'platform:overseas-warehouse:inbound-order:list', + 'platform:lastleg:inbound-plan:list', 'platform:overseas-warehouse:relabel-order:list', - 'platform:overseas-warehouse:pallet:list', + 'platform:pallet:list', 'platform:overseas-warehouse:carton:list', 'platform:overseas-warehouse:product:list', - 'platform:overseas-warehouse:inventory:list', + 'platform:lastleg:inventory:list', 'platform:overseas-business:purchase:list', 'platform:overseas-business:truck:list', 'platform:overseas-business:looseboard:list', diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index f9d3056f..0834b632 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -68,7 +68,6 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'inbound-dedup', title: '订单测量', icon: ClipboardList, path: '/inbounds/measurements', permission: 'platform:firstleg:inbound:measure:view' }, { key: 'inbound-measure', title: '测量数据审核', icon: FileCheck2, path: '/inbounds/measurement-reviews', permission: 'firstleg:inbound:measurement-review' }, { key: 'locations', title: '库位管理', icon: WarehouseIcon, path: '/master-data/locations', permission: 'platform:location:list' }, - { key: 'pallets', title: '托盘管理', icon: WarehouseIcon, path: '/master-data/pallets', permission: 'platform:pallet:list' }, { key: 'domestic-containers', title: '整柜业务', icon: WarehouseIcon, path: '/domestic-containers', permission: 'platform:domestic-container:list' }, ], }, @@ -122,12 +121,12 @@ export const adminNavigation: AdminNavigationNode[] = [ icon: PrototypeOverseasWarehouseIcon, children: [ { key: 'last-mile-orders', title: '尾程订单', icon: ClipboardList, path: '/overseas-warehouse/last-mile-orders', permission: 'platform:overseas-warehouse:last-mile-order:list' }, - { key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/overseas-warehouse/inbound-orders', permission: 'platform:overseas-warehouse:inbound-order:list' }, + { key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/overseas-warehouse/inbound-orders', permission: 'platform:lastleg:inbound-plan:list' }, { key: 'fba-relabel-order', title: '换标单', icon: ClipboardList, path: '/overseas-warehouse/relabel-orders', permission: 'platform:overseas-warehouse:relabel-order:list' }, - { key: 'fba-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/overseas-warehouse/pallets', permission: 'platform:overseas-warehouse:pallet:list' }, + { key: 'fba-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/overseas-warehouse/pallets', permission: 'platform:pallet:list' }, { key: 'fba-carton', title: '箱号管理', icon: ClipboardList, path: '/overseas-warehouse/cartons', permission: 'platform:overseas-warehouse:carton:list' }, { key: 'fba-product', title: '产品库', icon: Database, path: '/overseas-warehouse/products', permission: 'platform:overseas-warehouse:product:list' }, - { key: 'overseas-inventory-list', title: '库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/inventory', permission: 'platform:overseas-warehouse:inventory:list' }, + { key: 'overseas-inventory-list', title: '库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/inventory', permission: 'platform:lastleg:inventory:list' }, ], }, { diff --git a/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md b/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md index d11fc632..9f182081 100644 --- a/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md +++ b/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md @@ -6,7 +6,8 @@ |---|---|---|---|---| | 尾程订单列表 | `fba-last-mile-order` | `GET /platform/v1/last-leg/orders` | 已接入 | 显式 `customerId`;支持 `page/size/orderNo/inboundCode/containerNo/status`。原型其余筛选保留但禁用,不发送无效参数。 | | 尾程订单详情 | `fba-last-mile-order-detail` | `GET /platform/v1/last-leg/orders/{id}` | 已接入 | 展示订单、货物、状态历史和既有单据编号;出库/派送编号只读,不提供离仓动作。 | -| 入库单列表/详情 | `fba-inbound-order*` | 仅有 `GET /platform/v1/last-leg/inbound-plans/{planNo}` | 阻断 | 没有分页列表,无法由详情接口反推列表;当前页面继续明确显示契约待接入。 | +| 入库单列表/FBA预约 | `fba-inbound-order` | `GET /platform/v1/last-leg/inbound-plans`、`POST /platform/v1/last-leg/inbound-plans/fba-appointments/batch` | 前端已接入,待后端合入 | 已实现显式客户范围、分页筛选、按入库明细勾选、同客户/同 FBA 仓校验和预约弹窗;后端仓库写权限获批后需合入分页端点及预计装车时间字段。 | +| 入库单详情 | `fba-inbound-order-detail` | `GET /platform/v1/last-leg/inbound-plans/{planNo}` | 待接入 | 详情端点已存在,当前任务只处理列表分页与列表勾选预约。 | | 换标单列表/详情 | `fba-relabel-order*` | `GET /platform/v1/last-leg/relabel-applications`、`GET /{relabelNo}` | 已接入只读 | 列表 DTO 可展示单号、SKU、箱数和状态;详情补产品名、标签要求、文件。预算/费用继续显示空值并记录缺口,未伪造金额。 | | 库存/进销存/详情 | `fba-inventory*` | `GET /platform/v1/last-leg/inventory/summary|balances|ledger` | 阻断 | API 是入库单/包裹余额口径,原型是客户/仓库/SKU/产品口径,缺 SKU、产品名及原型指标。 | | 托盘列表/详情 | `fba-pallet*` | `GET /platform/v1/warehouse-admin/{warehouseId}/pallets`、`GET /{id}` | 阻断 | API 强制 `customerId + warehouseId` 且资源客户绑定;产品契约要求平台公共资源、独立查询全部客户/仓库。字段还缺 FBA 仓和采购价。 | @@ -15,4 +16,4 @@ 状态映射仅使用服务端已有事实:`PENDING_ASSIGNMENT`(待分配)、`DESTINATION_ASSIGNED`(已分配目的仓)、`INBOUND_PLANNED`(已生成入库计划)。未定义的原型状态不发送到接口。 -当前所有上述海外业务 Controller 均未声明与前端 `platform:overseas-warehouse:*` 对应的服务端 `@PreAuthorize`。因此本轮只接入尾程只读查询;已有写端点在服务端权限、幂等和原型字段未一致前不向 Admin 暴露。 +入库单菜单与预约按钮已统一使用服务端正式权限 `platform:lastleg:inbound-plan:list`、`platform:lastleg:inbound-plan:appointment`。其余海外页面仍按各自契约缺口处理,不用前端按钮权限替代服务端鉴权。 diff --git a/src/modules/overseas-warehouse/api/inboundOrders.test.ts b/src/modules/overseas-warehouse/api/inboundOrders.test.ts new file mode 100644 index 00000000..90ea1ddc --- /dev/null +++ b/src/modules/overseas-warehouse/api/inboundOrders.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment } from './inboundOrders' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) + +describe('inbound order overseas API', () => { + beforeEach(() => { + vi.mocked(overseasHttp.get).mockReset() + vi.mocked(overseasHttp.post).mockReset() + vi.mocked(overseasHttp.put).mockReset() + }) + + it('loads appointment details and updates the existing appointment without replacing its lines', async () => { + const detail = { id: '1', customerId: '190000000000000001', appointmentNo: 'APT/1', appointmentAt: '2026-08-20T10:30:00', expectedLoadingAt: '2026-08-20T09:00:00', appointmentStatus: 'SUCCESS', selectedCount: 2, fbaWarehouseCode: 'ONT8', items: [] } + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: detail }) + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: detail }) + + await getFbaAppointment('190000000000000001', 'APT/1') + await updateFbaAppointment('APT/1', { customerId: '190000000000000001', appointmentAt: detail.appointmentAt, expectedLoadingAt: detail.expectedLoadingAt, appointmentStatus: 'SUCCESS' }) + + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', { params: { customerId: '190000000000000001' } }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', expect.objectContaining({ customerId: '190000000000000001' })) + }) + + it('loads the paginated list with an explicit customer scope', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) + await listInboundOrders({ customerId: '190000000000000001', page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans', { + params: { customerId: '190000000000000001', page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }, + }) + }) + + it('preserves selected line ids as strings when creating an appointment', async () => { + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', appointmentNo: 'APT-1', appointmentStatus: 'SUCCESS', selectedCount: 1, fbaWarehouseCode: 'ONT8' } }) + const payload = { + customerId: '190000000000000001', + lineIds: ['190000000000000099'], + appointmentNo: 'APT-1', + appointmentAt: '2026-08-20T10:30:00', + expectedLoadingAt: '2026-08-20T09:00:00', + appointmentStatus: 'SUCCESS' as const, + } + await createFbaAppointment(payload) + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload) + }) +}) diff --git a/src/modules/overseas-warehouse/api/inboundOrders.ts b/src/modules/overseas-warehouse/api/inboundOrders.ts new file mode 100644 index 00000000..28c29a97 --- /dev/null +++ b/src/modules/overseas-warehouse/api/inboundOrders.ts @@ -0,0 +1,99 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import type { OverseasPageResult } from './lastMileOrders' + +export interface InboundOrderQuery { + customerId: string + page: number + size: number + planNo?: string + inboundCode?: string + status?: string + warehouse?: string + fbaWarehouseCode?: string + appointmentStatus?: string + dispatchStatus?: string + createdFrom?: string + createdTo?: string +} + +export interface InboundOrderSummaryDto { + lineId: string + planId: string + planNo: string + customerId: string + orderId: string + inboundCode?: string | null + orderType?: string | null + warehouseId?: string | null + warehouseCode?: string | null + warehouseName?: string | null + fbaWarehouseCode?: string | null + customerName?: string | null + packageCount?: number | null + volumeCbm?: number | null + palletCount?: number | null + status?: string | null + appointmentNo?: string | null + appointmentStatus?: string | null + appointmentAt?: string | null + expectedLoadingAt?: string | null + dispatchStatus?: string | null + createdAt?: string | null +} + +export interface BatchFbaAppointmentPayload { + customerId: string + lineIds: string[] + appointmentNo: string + appointmentAt: string + expectedLoadingAt: string + appointmentStatus: 'PENDING' | 'SUCCESS' | 'FAILED' | 'CANCELLED' + appointmentRemark?: string +} + +export interface FbaAppointmentDto { + id: string + customerId: string + appointmentNo: string + appointmentAt: string + expectedLoadingAt: string + appointmentStatus: string + appointmentRemark?: string | null + selectedCount: number + fbaWarehouseCode: string + items: Array<{ id: string; planId: string; lineId: string; orderId: string; inboundCode: string; fbaWarehouseCode: string }> +} + +export type UpdateFbaAppointmentPayload = Omit + +export async function listInboundOrders(query: InboundOrderQuery) { + const { customerId, ...params } = query + const response = await overseasHttp.get>, ApiEnvelope>>( + '/platform/v1/last-leg/inbound-plans', { params: { customerId, ...params } }, + ) + return unwrapApiResponse(response) +} + +export async function createFbaAppointment(payload: BatchFbaAppointmentPayload) { + const response = await overseasHttp.post, ApiEnvelope>( + '/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload, + ) + return unwrapApiResponse(response) +} + +export async function getFbaAppointment(customerId: string, appointmentNo: string) { + const response = await overseasHttp.get, ApiEnvelope>( + `/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function updateFbaAppointment(appointmentNo: string, payload: UpdateFbaAppointmentPayload) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`, + payload, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/inventory.test.ts b/src/modules/overseas-warehouse/api/inventory.test.ts new file mode 100644 index 00000000..057d8188 --- /dev/null +++ b/src/modules/overseas-warehouse/api/inventory.test.ts @@ -0,0 +1,20 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { getInventorySummary, listInventoryBalances } from './inventory' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) + +describe('overseas inventory API', () => { + beforeEach(() => { vi.clearAllMocks() }) + + it('keeps skuCode and nullable location filters on real balance queries', async () => { + vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }).mockResolvedValueOnce({ code: '200', data: { totalPackages: 0 } }) + const query = { customerId: '100', page: 1, size: 20, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } + + await listInventoryBalances(query) + await getInventorySummary(query) + + expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances', { params: query }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/summary', { params: { customerId: '100', skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } }) + }) +}) diff --git a/src/modules/overseas-warehouse/api/inventory.ts b/src/modules/overseas-warehouse/api/inventory.ts new file mode 100644 index 00000000..18e53a7b --- /dev/null +++ b/src/modules/overseas-warehouse/api/inventory.ts @@ -0,0 +1,74 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import type { OverseasPageResult } from './lastMileOrders' + +export interface InventoryBalanceDto { + id: string + customerId: string + customerName?: string | null + overseasWarehouseId: string + overseasWarehouseCode?: string | null + overseasWarehouseName?: string | null + destinationWarehouseId?: string | null + destinationWarehouseCode?: string | null + destinationWarehouseName?: string | null + inboundOrderId: string + inboundOrderNo?: string | null + inboundCode: string + skuCodes: string[] + productNames: string[] + warehouseLocationIds: string[] + warehouseLocationCodes: string[] + totalPackages: number + availablePackages: number + holdPackages: number + allocatedPackages: number + pickedPackages: number + inTransitPackages: number + receivingPackages: number + procurementPackages: number + updatedAt?: string | null +} + +export interface InventoryDashboardDto { + inboundOrderCount: number + palletCount: number + totalPackages: number + availablePackages: number + holdPackages: number + allocatedPackages: number + pickedPackages: number + inTransitPackages: number + receivingPackages: number + procurementPackages: number +} + +export interface InventoryBalanceQuery { + customerId: string + page: number + size: number + customerName?: string + overseasWarehouseId?: string + skuCode?: string + productName?: string + batchNo?: string + warehouseLocationCode?: string + inventoryStatus?: string +} + +function params(query: InventoryBalanceQuery) { return { ...query } } + +export async function listInventoryBalances(query: InventoryBalanceQuery) { + const response = await overseasHttp.get>, ApiEnvelope>>( + '/platform/v1/last-leg/inventory/balances', { params: params(query) }, + ) + return unwrapApiResponse(response) +} + +export async function getInventorySummary(query: InventoryBalanceQuery) { + const { page: _page, size: _size, ...summaryParams } = params(query) + const response = await overseasHttp.get, ApiEnvelope>( + '/platform/v1/last-leg/inventory/summary', { params: summaryParams }, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/pallets.test.ts b/src/modules/overseas-warehouse/api/pallets.test.ts new file mode 100644 index 00000000..ff875b2c --- /dev/null +++ b/src/modules/overseas-warehouse/api/pallets.test.ts @@ -0,0 +1,30 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { createPallet, deletePallet, getPallet, listCustomerWarehouses, listPallets, updatePallet } from './pallets' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } })) + +describe('overseas pallet API', () => { + beforeEach(() => { vi.clearAllMocks() }) + + it('uses the customer and warehouse scoped pallet CRUD contract', async () => { + const page = { records: [], page: 1, size: 20, total: 0, pages: 0 } + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: page }) + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: '9' }) + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: null }) + vi.mocked(overseasHttp.delete).mockResolvedValue({ code: '200', data: null }) + const payload = { palletCode: 'PLT-1', palletType: 'WOOD', capacityCbm: 2, unitPrice: 10, status: 'IDLE' } + + await listCustomerWarehouses('100') + await listPallets('100', '200', { page: 1, size: 20, keyword: 'PLT' }) + await getPallet('100', '200', '9') + await createPallet('100', '200', payload) + await updatePallet('100', '200', '9', payload) + await deletePallet('100', '200', '9') + + expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/warehouse-admin/200/pallets', { params: { customerId: '100', page: 1, size: 20, keyword: 'PLT' } }) + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets', payload, { params: { customerId: '100' } }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', payload, { params: { customerId: '100' } }) + expect(overseasHttp.delete).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', { params: { customerId: '100' } }) + }) +}) diff --git a/src/modules/overseas-warehouse/api/pallets.ts b/src/modules/overseas-warehouse/api/pallets.ts new file mode 100644 index 00000000..dac0204d --- /dev/null +++ b/src/modules/overseas-warehouse/api/pallets.ts @@ -0,0 +1,84 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import type { OverseasPageResult } from './lastMileOrders' + +export interface CustomerWarehouseDto { + id: string + customerId: string + warehouseCode: string + warehouseName: string + warehouseType: string + status: string +} + +export interface PalletDto { + id: string + customerId: string + warehouseId: string + locationId?: string | null + palletCode: string + palletType: string + capacityCbm: number + unitPrice: number + status: string + remark?: string | null + createdAt?: string | null + updatedAt?: string | null +} + +export interface SavePalletPayload { + palletCode: string + palletType: string + capacityCbm: number + unitPrice: number + status: string + locationId?: string | null + remark?: string | null +} + +export async function listCustomerWarehouses(customerId: string) { + const response = await overseasHttp.get>, ApiEnvelope>>( + '/platform/v1/warehouse-admin', + { params: { customerId, page: 1, size: 200, status: 'ACTIVE' } }, + ) + return unwrapApiResponse(response) +} + +export async function listPallets(customerId: string, warehouseId: string, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) { + const response = await overseasHttp.get>, ApiEnvelope>>( + `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, + { params: { customerId, ...query } }, + ) + return unwrapApiResponse(response) +} + +export async function getPallet(customerId: string, warehouseId: string, palletId: string) { + const response = await overseasHttp.get, ApiEnvelope>( + `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function createPallet(customerId: string, warehouseId: string, payload: SavePalletPayload) { + const response = await overseasHttp.post, ApiEnvelope>( + `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, payload, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function updatePallet(customerId: string, warehouseId: string, palletId: string, payload: SavePalletPayload) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, + payload, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function deletePallet(customerId: string, warehouseId: string, palletId: string) { + const response = await overseasHttp.delete, ApiEnvelope>( + `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue index a4b87c3f..8b011f7e 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue @@ -1,31 +1,70 @@ + + diff --git a/src/modules/overseas-warehouse/pages/InventoryView.vue b/src/modules/overseas-warehouse/pages/InventoryView.vue index c04a1083..afe0469c 100644 --- a/src/modules/overseas-warehouse/pages/InventoryView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryView.vue @@ -1,63 +1,111 @@ diff --git a/src/modules/overseas-warehouse/pages/PalletDetailView.vue b/src/modules/overseas-warehouse/pages/PalletDetailView.vue index 4505d371..f63dcf53 100644 --- a/src/modules/overseas-warehouse/pages/PalletDetailView.vue +++ b/src/modules/overseas-warehouse/pages/PalletDetailView.vue @@ -1,16 +1,47 @@ - + diff --git a/src/modules/overseas-warehouse/pages/PalletsView.vue b/src/modules/overseas-warehouse/pages/PalletsView.vue index df742a5e..9d0f4b41 100644 --- a/src/modules/overseas-warehouse/pages/PalletsView.vue +++ b/src/modules/overseas-warehouse/pages/PalletsView.vue @@ -1,66 +1,155 @@ diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 27b22ee6..726db13c 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -50,7 +50,6 @@ describe('overseas warehouse first-batch prototype contract', () => { it('preserves the prototype dialog carriers and desktop widths', () => { const expected = [ ['RelabelOrdersView.vue', '760px', '新增换标单', '审核换标单'], - ['PalletsView.vue', '760px', '新增托盘', '编辑托盘'], ['CartonsView.vue', '860px', '新增箱号', '编辑箱号'], ['ProductsView.vue', '560px', '新增产品', '编辑产品'], ] @@ -79,7 +78,7 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(dialog).toContain('confirm-disabled') expect(dialog).toContain('不可提交') expect(dialog).not.toContain('@confirm=') - for (const fileName of ['RelabelOrdersView.vue', 'PalletsView.vue', 'CartonsView.vue', 'ProductsView.vue']) { + for (const fileName of ['RelabelOrdersView.vue', 'CartonsView.vue', 'ProductsView.vue']) { const value = source(fileName) expect(value).not.toContain('ElMessage.success') expect(value).not.toMatch(/https?:\/\//) @@ -118,6 +117,28 @@ describe('overseas warehouse first-batch prototype contract', () => { } }) + it('connects pallet CRUD after the overseas contract is available', () => { + const value = source('PalletsView.vue') + expect(value).toContain(' { + const value = source('InboundOrdersView.vue') + expect(value).toContain('listInboundOrders') + expect(value).toContain("key: 'selection', type: 'selection'") + expect(value).toContain('@selection-change="onSelectionChange"') + expect(value).toContain('createFbaAppointment') + expect(value).toContain('预计装车时间') + expect(value).toContain('所选入库单必须属于同一 FBA 仓库') + expect(value).not.toContain('ContractPendingNotice') + }) + it('adds the prototype inventory transaction page as a read-only inventory route', () => { const inventory = source('InventoryView.vue') const transactions = source('InventoryTransactionsView.vue') @@ -187,11 +208,11 @@ describe('overseas warehouse first-batch prototype contract', () => { 'internalRemark', 'customerRemark', 'warehouseRemark', ]) expect(columnKeys('InboundOrdersView.vue')).toEqual([ - 'actions', 'orderNo', 'inboundCode', 'type', 'warehouse', 'fbaWarehouse', 'owner', 'cartons', 'volume', + 'actions', 'selection', 'orderNo', 'inboundCode', 'type', 'warehouse', 'fbaWarehouse', 'owner', 'cartons', 'volume', 'pallets', 'status', 'appointmentStatus', 'dispatchStatus', ]) expect(columnKeys('InventoryView.vue')).toEqual([ - 'actions', 'owner', 'warehouse', 'sku', 'productName', 'onHand', 'available', 'held', 'looseboard', + 'actions', 'owner', 'warehouse', 'sku', 'productName', 'location', 'onHand', 'available', 'held', 'looseboard', 'inTransit', 'purchase', 'status', 'updatedAt', ]) expect(columnKeys('RelabelOrdersView.vue')).toEqual([ diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index d2c821b4..777944e6 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -1,13 +1,15 @@ export const overseasWarehousePermissions = { lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list', - inboundOrderList: 'platform:overseas-warehouse:inbound-order:list', - inventoryList: 'platform:overseas-warehouse:inventory:list', + inboundOrderList: 'platform:lastleg:inbound-plan:list', + inboundOrderAppointment: 'platform:lastleg:inbound-plan:appointment', + inventoryList: 'platform:lastleg:inventory:list', relabelOrderList: 'platform:overseas-warehouse:relabel-order:list', relabelOrderCreate: 'platform:overseas-warehouse:relabel-order:create', relabelOrderEdit: 'platform:overseas-warehouse:relabel-order:edit', - palletList: 'platform:overseas-warehouse:pallet:list', - palletCreate: 'platform:overseas-warehouse:pallet:create', - palletEdit: 'platform:overseas-warehouse:pallet:edit', + palletList: 'platform:pallet:list', + palletCreate: 'platform:pallet:create', + palletEdit: 'platform:pallet:edit', + palletDelete: 'platform:pallet:delete', cartonList: 'platform:overseas-warehouse:carton:list', cartonCreate: 'platform:overseas-warehouse:carton:create', cartonEdit: 'platform:overseas-warehouse:carton:edit', diff --git a/src/modules/overseas-warehouse/types.ts b/src/modules/overseas-warehouse/types.ts index 13b44004..708ececf 100644 --- a/src/modules/overseas-warehouse/types.ts +++ b/src/modules/overseas-warehouse/types.ts @@ -42,6 +42,10 @@ export interface LastMileOrderListItem { } export interface InboundOrderListItem { + lineId: string + planId: string + customerId: string + orderId: string orderNo: string inboundCode: string type: string @@ -52,20 +56,25 @@ export interface InboundOrderListItem { volume: number pallets: number status: string + appointmentNo: string appointmentStatus: string + appointmentAt: string + expectedLoadingAt: string dispatchStatus: string } export interface InventoryListItem { id: string + customerId: string owner: string warehouse: string sku: string productName: string + location: string onHand: number available: number held: number - looseboard: number + looseboard: number | string inTransit: number purchase: number status: string @@ -88,6 +97,10 @@ export interface RelabelOrderListItem { } export interface PalletListItem { + id: string + customerId: string + warehouseId: string + locationId: string code: string type: string warehouse: string