diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index 6a584e66..0f6b2895 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -18,6 +18,7 @@ describe('adminNavigation', () => { '货柜管理', '海外业务', '海外仓', + '客户服务', '财务管理', '销售管理', '通用', @@ -34,7 +35,7 @@ describe('adminNavigation', () => { '库位管理', '整柜业务', ]) - expect(adminNavigation.find((item) => item.key === 'service-center')).toBeUndefined() + expect(adminNavigation.find((item) => item.key === 'customer-service')?.children?.map((item) => item.title)).toEqual(['异常订单', '客户回访']) expect(adminNavigation.find((item) => item.key === 'supplier-center')?.children?.map((item) => item.title)).toEqual([ '供应商列表', '快递公司', @@ -51,6 +52,8 @@ describe('adminNavigation', () => { '尾程订单', '入库单', '排车列表', + '出库单', + '派送单列表', '换标单', '托盘管理', '库存管理', @@ -141,6 +144,10 @@ describe('adminNavigation', () => { 'platform:overseas-warehouse:last-mile-order:list', 'platform:overseas-warehouse:inbound-order:list', 'platform:overseas-warehouse:dispatch:list', + 'platform:overseas-warehouse:outbound:list', + 'platform:overseas-warehouse:delivery:list', + 'platform:overseas-warehouse:exception-order:list', + 'platform:overseas-warehouse:customer-visit:list', 'platform:overseas-warehouse:relabel-order:list', 'platform:overseas-warehouse:pallet:list', 'platform:overseas-warehouse:inventory:list', @@ -157,6 +164,10 @@ describe('adminNavigation', () => { '/overseas-warehouse/last-mile-orders', '/overseas-warehouse/inbound-orders', '/overseas-warehouse/dispatches', + '/overseas-warehouse/outbounds', + '/overseas-warehouse/delivery-orders', + '/overseas-warehouse/exception-orders', + '/overseas-warehouse/customer-visits', '/overseas-warehouse/relabel-orders', '/overseas-warehouse/pallets', '/overseas-warehouse/inventory', @@ -169,7 +180,7 @@ describe('adminNavigation', () => { ])) expect(paths).not.toContain('/overseas-warehouse/cartons') expect(paths).not.toContain('/overseas-warehouse/products') - expect(paths.some((path) => path.includes('outbound') || path.includes('delivery'))).toBe(false) + expect(paths).toEqual(expect.arrayContaining(['/overseas-warehouse/outbounds', '/overseas-warehouse/delivery-orders'])) }) it('filters protected pages without mutating the source tree', () => { diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 4493ff34..957847b9 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -124,11 +124,21 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/overseas-warehouse/inbound-orders', permission: 'platform:overseas-warehouse:inbound-order:list' }, { key: 'fba-dispatch', title: '排车列表', icon: Truck, path: '/overseas-warehouse/dispatches', permission: 'platform:overseas-warehouse:dispatch:list' }, { key: 'fba-outbound', title: '出库单', icon: ClipboardList, path: '/overseas-warehouse/outbounds', permission: 'platform:overseas-warehouse:outbound:list' }, + { key: 'fba-delivery', title: '派送单列表', icon: Truck, path: '/overseas-warehouse/delivery-orders', permission: 'platform:overseas-warehouse:delivery: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: 'overseas-inventory-list', title: '库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/inventory', permission: 'platform:overseas-warehouse:inventory:list' }, ], }, + { + key: 'customer-service', + title: '客户服务', + icon: ContactRound, + children: [ + { key: 'exception-orders', title: '异常订单', icon: ClipboardList, path: '/overseas-warehouse/exception-orders', permission: 'platform:overseas-warehouse:exception-order:list' }, + { key: 'customer-visits', title: '客户回访', icon: ContactRound, path: '/overseas-warehouse/customer-visits', permission: 'platform:overseas-warehouse:customer-visit:list' }, + ], + }, { key: 'finance', title: '财务管理', diff --git a/src/modules/overseas-warehouse/api/customerVisits.test.ts b/src/modules/overseas-warehouse/api/customerVisits.test.ts new file mode 100644 index 00000000..88f1e078 --- /dev/null +++ b/src/modules/overseas-warehouse/api/customerVisits.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { + getCustomerVisit, + listCustomerVisits, + saveCustomerVisit, + updateDeliveryOrderStatus, + uploadCustomerVisitAttachment, +} from './customerVisits' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) + +describe('customer visit API', () => { + beforeEach(() => { + vi.mocked(overseasHttp.get).mockReset() + vi.mocked(overseasHttp.post).mockReset() + vi.mocked(overseasHttp.put).mockReset() + }) + + it('loads the customer visit page and detail in customer scope', async () => { + vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], current: 1, size: 10, total: 0, pages: 0, visitable: 3, pending: 2, completed: 1 } }) + await listCustomerVisits({ page: 1, size: 10, keyword: 'DEL' }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/customer-visits', { params: { page: 1, size: 10, keyword: 'DEL' } }) + + vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } }) + await getCustomerVisit('9', '100') + expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/customer-visits/9', { params: { customerId: '100' } }) + }) + + it('updates delivery status so selecting delivered can generate a visit', async () => { + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { deliveryStatus: 'DELIVERED' } }) + await updateDeliveryOrderStatus('DEL/1', '100', { status: 'DELIVERED', remark: '完成' }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/status', { status: 'DELIVERED', remark: '完成' }, { params: { customerId: '100' } }) + }) + + it('uploads and saves repeatable visit material', async () => { + const file = new File(['data'], 'visit.pdf', { type: 'application/pdf' }) + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { fileId: '8', fileName: 'visit.pdf', sizeBytes: 4 } }) + await uploadCustomerVisitAttachment('9', '100', file) + const body = vi.mocked(overseasHttp.post).mock.calls[0]?.[1] as FormData + expect(body.get('file')).toBe(file) + + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } }) + await saveCustomerVisit('9', '100', { selectedInboundOrderId: '7', visitResult: 'NORMAL', attachments: [] }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/customer-visits/9', expect.objectContaining({ selectedInboundOrderId: '7' }), { params: { customerId: '100' } }) + }) +}) diff --git a/src/modules/overseas-warehouse/api/customerVisits.ts b/src/modules/overseas-warehouse/api/customerVisits.ts new file mode 100644 index 00000000..c657755c --- /dev/null +++ b/src/modules/overseas-warehouse/api/customerVisits.ts @@ -0,0 +1,116 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' + +export interface CustomerVisitAttachment { + fileId: string + fileName: string + contentType?: string | null + sizeBytes: number +} + +export interface CustomerVisitRow { + id: string + customerId: string + deliveryNo: string + customerName?: string | null + contactName?: string | null + contactPhone?: string | null + destination?: string | null + deliveryCompletedAt: string + status: 'PENDING' | 'COMPLETED' + selectedInboundCode?: string | null + visitResult?: string | null + visitedAt?: string | null + visitCount: number + attachments: CustomerVisitAttachment[] +} + +export interface CustomerVisitPage { + records: CustomerVisitRow[] + current: number + size: number + total: number + pages: number + visitable: number + pending: number + completed: number +} + +export interface CustomerVisitInboundOption { + inboundOrderId: string + inboundCode: string + lastMileOrderId: string + lastMileOrderNo: string + destinationWarehouseCode?: string | null + destinationWarehouseName?: string | null + packageCount: number +} + +export interface CustomerVisitDetail { + visit: CustomerVisitRow + inboundOrders: CustomerVisitInboundOption[] + customerFeedback?: string | null + remark?: string | null + visitedBy?: string | null +} + +export async function listCustomerVisits(params: { + page: number + size: number + keyword?: string + status?: string + customerId?: string + completedFrom?: string + completedTo?: string +}) { + const response = await overseasHttp.get, ApiEnvelope>( + '/platform/v1/last-leg/customer-visits', { params }, + ) + return unwrapApiResponse(response) +} + +export async function getCustomerVisit(visitId: string, customerId: string) { + const response = await overseasHttp.get, ApiEnvelope>( + `/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function saveCustomerVisit(visitId: string, customerId: string, payload: { + selectedInboundOrderId: string + visitResult: string + customerFeedback?: string + remark?: string + attachments: CustomerVisitAttachment[] +}) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, payload, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function uploadCustomerVisitAttachment(visitId: string, customerId: string, file: File) { + const body = new FormData() + body.append('file', file) + const response = await overseasHttp.post, ApiEnvelope>( + `/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}/attachments`, body, + { params: { customerId }, headers: { 'Content-Type': 'multipart/form-data' } }, + ) + return unwrapApiResponse(response) +} + +export async function completeDeliveryOrder(deliveryNo: string, customerId: string) { + const response = await overseasHttp.post, ApiEnvelope<{ visitId: string; deliveryNo: string; deliveryStatus: string; visitStatus: string; replayed: boolean }>>( + `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, undefined, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function updateDeliveryOrderStatus(deliveryNo: string, customerId: string, payload: { status: string; remark?: string }) { + const response = await overseasHttp.put, ApiEnvelope<{ visitId?: string | null; deliveryNo: string; deliveryStatus: string; visitStatus?: string | null; replayed: boolean }>>( + `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, payload, + { params: { customerId } }, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.test.ts b/src/modules/overseas-warehouse/api/deliveryOrders.test.ts new file mode 100644 index 00000000..86892b2a --- /dev/null +++ b/src/modules/overseas-warehouse/api/deliveryOrders.test.ts @@ -0,0 +1,22 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { getDeliveryOrder, listDeliveryOrders } from './deliveryOrders' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) + +describe('delivery order API', () => { + beforeEach(() => vi.mocked(overseasHttp.get).mockReset()) + + it('passes paging and expanded filters and normalizes page numbers', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [], page: '2', size: '20', total: '41', pages: '3' } }) + const result = await listDeliveryOrders({ page: 2, size: 20, documentNo: 'DEL-1', vehiclePlate: 'CA-1' }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders', { params: { page: 2, size: 20, documentNo: 'DEL-1', vehiclePlate: 'CA-1' } }) + expect(result).toMatchObject({ page: 2, size: 20, total: 41, pages: 3 }) + }) + + it('loads an encoded detail in its explicit customer scope', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { deliveryNo: 'DEL/1' }, skus: [], fees: [], operations: [] } }) + await getDeliveryOrder('DEL/1', '1001') + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001' } }) + }) +}) diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.ts b/src/modules/overseas-warehouse/api/deliveryOrders.ts new file mode 100644 index 00000000..6c11f8be --- /dev/null +++ b/src/modules/overseas-warehouse/api/deliveryOrders.ts @@ -0,0 +1,88 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' +import type { ExceptionRelation } from './exceptionOrders' + +export interface DeliveryOrderQuery { + page: number + size: number + customerId?: string + documentNo?: string + dispatchNo?: string + waybillNo?: string + warehouseId?: string + status?: string + vehiclePlate?: string + driverName?: string + deliveryMethod?: string + plannedFrom?: string + plannedTo?: string +} + +export interface DeliveryOrderRow { + id: string + customerId: string + customerName?: string | null + deliveryNo: string + outboundNo: string + dispatchNo: string + overseasWarehouseId?: string | null + overseasWarehouseName?: string | null + status: string + waybillNo?: string | null + deliveryMethod?: string | null + carrierName?: string | null + pickupMethod?: string | null + loadingType?: string | null + vehicleType?: string | null + vehicleSize?: string | null + vehiclePlate?: string | null + driverName?: string | null + driverPhone?: string | null + appointmentAccount?: string | null + sellerName?: string | null + palletCount: number + packageCount: number + totalWeightKg?: number | null + totalVolumeCbm?: number | null + plannedDeliveryAt?: string | null + dispatchedAt?: string | null + expectedArrivalAt?: string | null + signedAt?: string | null + remark?: string | null + createdAt: string +} + +export interface DeliverySkuRow { inboundCode?: string | null; customerName?: string | null; destinationWarehouseCode?: string | null; receivingAddress?: string | null; shipmentNo?: string | null; fbaTrackingNo?: string | null; skuCode?: string | null; productName?: string | null; packageCount: number; weightKg?: number | null; volumeCbm?: number | null } +export interface DeliveryFeeRow { id: string; feeDirection: string; feeType: string; currency: string; amount: number; remark?: string | null } +export interface DeliveryOperationRow { id: string; operatedAt: string; operationNode: string; operatorName?: string | null; result: string } +export interface DeliveryOrderDetail { order: DeliveryOrderRow; skus: DeliverySkuRow[]; fees: DeliveryFeeRow[]; operations: DeliveryOperationRow[] } + +export async function listDeliveryOrders(query: DeliveryOrderQuery) { + const { customerId, ...params } = query + const response = await overseasHttp.get>, ApiEnvelope>>( + '/platform/v1/last-leg/delivery-orders', { params: { ...params, ...(customerId ? { customerId } : {}) } }, + ) + return normalizeOverseasPage(unwrapApiResponse(response)) +} + +export async function getDeliveryOrder(deliveryNo: string, customerId: string) { + const response = await overseasHttp.get, ApiEnvelope>( + `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function listDeliveryReturnInboundOptions(deliveryNo: string, customerId: string) { + const response = await overseasHttp.get, ApiEnvelope>( + `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function markDeliveryReturn(deliveryNo: string, customerId: string, payload: { inboundOrderId: string; reason: string; returnDirection: string; returnLogisticsNo?: string; packageCount?: number; skuCode?: string }) { + const response = await overseasHttp.post, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>( + `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, payload, { params: { customerId } }, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index ffd6158f..2812973b 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -171,7 +171,25 @@ export async function releaseDispatchDraft(draftToken: string) { } export interface TransferDeliveryResult { dispatchNo: string; outboundNo: string; outboundStatus: string; deliveryNo: string; deliveryStatus: string; palletCount: number; packageCount: number; replayed: boolean } -export async function transferDispatchToDelivery(dispatchNo: string, payload: { idempotencyKey: string; remark?: string }) { +export interface TransferDeliveryPayload { + idempotencyKey: string + plannedDeliveryAt: string + deliveryMethod: string + waybillNo?: string + carrierName?: string + pickupMethod: string + loadingType: string + vehicleType?: string + vehicleSize?: string + driverName?: string + driverPhone?: string + vehiclePlate?: string + appointmentAccount?: string + sellerName?: string + remark?: string + fees: Array<{ feeType: string; currency: string; amount: number; remark?: string }> +} +export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) { const response = await overseasHttp.post, ApiEnvelope>( `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, payload, ) diff --git a/src/modules/overseas-warehouse/api/exceptionOrders.test.ts b/src/modules/overseas-warehouse/api/exceptionOrders.test.ts new file mode 100644 index 00000000..c56716e5 --- /dev/null +++ b/src/modules/overseas-warehouse/api/exceptionOrders.test.ts @@ -0,0 +1,26 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation } from './exceptionOrders' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) + +describe('exception order API', () => { + beforeEach(() => { vi.mocked(overseasHttp.get).mockReset(); vi.mocked(overseasHttp.post).mockReset() }) + + it('queries the real paginated exception endpoint', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [], page: '1', size: '20', total: '2', pages: '1' } }) + const result = await listExceptionOrders({ page: 1, size: 20, keyword: 'EXC' }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', { params: { page: 1, size: 20, keyword: 'EXC' } }) + expect(result.total).toBe(2) + }) + + it('resolves relation before creating an exception document', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { inboundOrderId: '11', inboundCode: 'IN-1', lastMileOrderId: '12', lastMileOrderNo: 'LM-1', customerId: '1' } }) + await resolveExceptionRelation({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders/resolve', { params: { inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' } }) + + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { id: '99', exceptionNo: 'EXC-1', status: 'PENDING' } }) + await createExceptionOrder({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', exceptionType: 'RETURN', reason: '拒收' }) + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', expect.objectContaining({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' })) + }) +}) diff --git a/src/modules/overseas-warehouse/api/exceptionOrders.ts b/src/modules/overseas-warehouse/api/exceptionOrders.ts new file mode 100644 index 00000000..241c8f9a --- /dev/null +++ b/src/modules/overseas-warehouse/api/exceptionOrders.ts @@ -0,0 +1,32 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' + +export interface ExceptionOrderQuery { page: number; size: number; keyword?: string; exceptionType?: string; status?: string; customerId?: string; warehouseId?: string; createdFrom?: string; createdTo?: string } +export interface ExceptionOrderRow { + id: string; exceptionNo: string; exceptionType: string; status: string; reason: string + customerId: string; customerName?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null + lastMileOrderId?: string | null; lastMileOrderNo?: string | null; inboundOrderId?: string | null; inboundCode?: string | null + deliveryOrderId?: string | null; deliveryNo?: string | null; outboundNo?: string | null + skuCode?: string | null; packageCount?: number | null; returnDirection?: string | null; returnLogisticsNo?: string | null + handlingAction?: string | null; handlerName?: string | null; exceptionAt: string; updatedAt: string +} +export interface ExceptionRelation { + lastMileOrderId?: string | null; lastMileOrderNo?: string | null; inboundOrderId?: string | null; inboundCode?: string | null + customerId: string; customerName?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null + deliveryNo?: string | null; outboundNo?: string | null; availablePackages?: number | null +} +export interface CreateExceptionOrderPayload { customerId?: string; inboundCode?: string; lastMileOrderNo?: string; exceptionType: string; reason: string; packageCount?: number; skuCode?: string } + +export async function listExceptionOrders(query: ExceptionOrderQuery) { + const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/exception-orders', { params: query }) + return normalizeOverseasPage(unwrapApiResponse(response)) +} +export async function resolveExceptionRelation(params: { customerId?: string; inboundCode?: string; lastMileOrderNo?: string }) { + const response = await overseasHttp.get, ApiEnvelope>('/platform/v1/last-leg/exception-orders/resolve', { params }) + return unwrapApiResponse(response) +} +export async function createExceptionOrder(payload: CreateExceptionOrderPayload) { + const response = await overseasHttp.post, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>('/platform/v1/last-leg/exception-orders', payload) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/outbounds.ts b/src/modules/overseas-warehouse/api/outbounds.ts index a1dd16f3..edf65236 100644 --- a/src/modules/overseas-warehouse/api/outbounds.ts +++ b/src/modules/overseas-warehouse/api/outbounds.ts @@ -12,3 +12,7 @@ export async function createOneClickDropshipOutbound(customerId: string, payload const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId } }) return unwrapApiResponse(response) } +export async function cancelOutbound(outboundNo: string, reason: string, customerId?: string) { + const response = await overseasHttp.post, ApiEnvelope>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}/cancel`, { reason }, { params: customerId ? { customerId } : undefined }) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/pages/CustomerVisitsView.vue b/src/modules/overseas-warehouse/pages/CustomerVisitsView.vue new file mode 100644 index 00000000..6858ce23 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/CustomerVisitsView.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue new file mode 100644 index 00000000..c7478193 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue new file mode 100644 index 00000000..978726c1 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue @@ -0,0 +1,78 @@ + + + + diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index d04449e5..1573ee4a 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -1,5 +1,5 @@ @@ -156,5 +188,6 @@ onMounted(load) - +

派送信息

费用信息(USD)

+ diff --git a/src/modules/overseas-warehouse/pages/ExceptionOrdersView.vue b/src/modules/overseas-warehouse/pages/ExceptionOrdersView.vue new file mode 100644 index 00000000..2506fcf6 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/ExceptionOrdersView.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue index 68cc7d03..308959ef 100644 --- a/src/modules/overseas-warehouse/pages/OutboundsView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue @@ -1,24 +1,41 @@ - + + diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 19020720..684e896c 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -19,6 +19,10 @@ describe('overseas warehouse first-batch prototype contract', () => { ['LastMileOrdersView.vue', 'overseas-last-mile-orders-query'], ['InboundOrdersView.vue', 'overseas-inbound-orders-query'], ['DispatchesView.vue', 'overseas-dispatches-query'], + ['DeliveryOrdersView.vue', 'overseas-delivery-orders-query'], + ['CustomerVisitsView.vue', 'overseas-customer-visits-query'], + ['ExceptionOrdersView.vue', 'overseas-exception-orders-query'], + ['OutboundsView.vue', 'overseas-outbounds-query'], ['InventoryView.vue', 'overseas-inventory-query'], ['InventoryTransactionsView.vue', 'overseas-inventory-transactions-query'], ['RelabelOrdersView.vue', 'overseas-relabel-orders-query'], diff --git a/src/modules/overseas-warehouse/permissions.ts b/src/modules/overseas-warehouse/permissions.ts index 22ade117..dc22f323 100644 --- a/src/modules/overseas-warehouse/permissions.ts +++ b/src/modules/overseas-warehouse/permissions.ts @@ -10,6 +10,13 @@ export const overseasWarehousePermissions = { dispatchList: 'platform:overseas-warehouse:dispatch:list', outboundList: 'platform:overseas-warehouse:outbound:list', outboundCreate: 'platform:overseas-warehouse:outbound:create', + deliveryList: 'platform:overseas-warehouse:delivery:list', + exceptionOrderList: 'platform:overseas-warehouse:exception-order:list', + exceptionOrderCreate: 'platform:overseas-warehouse:exception-order:create', + deliveryReturn: 'platform:overseas-warehouse:delivery:return', + deliveryStatus: 'platform:overseas-warehouse:delivery:status', + customerVisitList: 'platform:overseas-warehouse:customer-visit:list', + customerVisitEdit: 'platform:overseas-warehouse:customer-visit:edit', inventoryList: 'platform:overseas-warehouse:inventory:list', inventoryExport: 'platform:overseas-warehouse:inventory:export', inventoryEdit: 'platform:overseas-warehouse:inventory:edit', diff --git a/src/modules/overseas-warehouse/routes.test.ts b/src/modules/overseas-warehouse/routes.test.ts index a72c562e..06c9bdb6 100644 --- a/src/modules/overseas-warehouse/routes.test.ts +++ b/src/modules/overseas-warehouse/routes.test.ts @@ -11,6 +11,11 @@ describe('overseas warehouse routes', () => { ['overseas-warehouse/inbound-orders/:orderId', permission.inboundOrderList], ['overseas-warehouse/dispatches', permission.dispatchList], ['overseas-warehouse/dispatches/:dispatchNo', permission.dispatchList], + ['overseas-warehouse/outbounds', permission.outboundList], + ['overseas-warehouse/delivery-orders', permission.deliveryList], + ['overseas-warehouse/delivery-orders/:deliveryNo', permission.deliveryList], + ['overseas-warehouse/exception-orders', permission.exceptionOrderList], + ['overseas-warehouse/customer-visits', permission.customerVisitList], ['overseas-warehouse/inventory', permission.inventoryList], ['overseas-warehouse/inventory-transactions', permission.inventoryList], ['overseas-warehouse/inventory/:inventoryId', permission.inventoryList], @@ -23,7 +28,7 @@ describe('overseas warehouse routes', () => { it('keeps all details as formal parameterized pages', () => { const details = overseasWarehouseRoutes.filter((route) => route.path.includes('/:')) - expect(details).toHaveLength(6) + expect(details).toHaveLength(7) expect(details.every((route) => route.meta.showPageHeading === false)).toBe(true) }) @@ -39,6 +44,8 @@ describe('overseas warehouse routes', () => { permission.palletCreate, permission.palletEdit, permission.cartonCreate, permission.cartonEdit, permission.productCreate, permission.productEdit, - ]).size).toBe(8) + permission.exceptionOrderCreate, permission.deliveryReturn, permission.deliveryStatus, + permission.customerVisitEdit, + ]).size).toBe(12) }) }) diff --git a/src/modules/overseas-warehouse/routes.ts b/src/modules/overseas-warehouse/routes.ts index fe3e9e0d..c596dfaf 100644 --- a/src/modules/overseas-warehouse/routes.ts +++ b/src/modules/overseas-warehouse/routes.ts @@ -44,6 +44,30 @@ export const overseasWarehouseRoutes = [ component: () => import('./pages/OutboundsView.vue'), meta: { title: '出库单', description: '查询海外仓出库单并创建一件代发出库', permission: permission.outboundList }, }, + { + path: 'overseas-warehouse/delivery-orders', + name: 'overseas-delivery-orders', + component: () => import('./pages/DeliveryOrdersView.vue'), + meta: { title: '派送单列表', description: '查询海外仓派送订单及履约信息', permission: permission.deliveryList }, + }, + { + path: 'overseas-warehouse/delivery-orders/:deliveryNo', + name: 'overseas-delivery-order-detail', + component: () => import('./pages/DeliveryOrderDetailView.vue'), + meta: { title: '派送单详情', description: '查看派送订单、SKU、费用和操作记录', permission: permission.deliveryList, showPageHeading: false }, + }, + { + path: 'overseas-warehouse/exception-orders', + name: 'overseas-exception-orders', + component: () => import('./pages/ExceptionOrdersView.vue'), + meta: { title: '异常订单', description: '查询和新增海外仓异常、拒收及退货单据', permission: permission.exceptionOrderList }, + }, + { + path: 'overseas-warehouse/customer-visits', + name: 'overseas-customer-visits', + component: () => import('./pages/CustomerVisitsView.vue'), + meta: { title: '客户回访', description: '跟进已完成派送单并维护入仓单维度回访资料', permission: permission.customerVisitList }, + }, { path: 'overseas-warehouse/inventory', name: 'overseas-inventory',