diff --git a/src/modules/downloads/api/platformExportTasks.ts b/src/modules/downloads/api/platformExportTasks.ts index 101305ec..2d004b27 100644 --- a/src/modules/downloads/api/platformExportTasks.ts +++ b/src/modules/downloads/api/platformExportTasks.ts @@ -11,6 +11,7 @@ export type KnownExportTaskType = | 'VEHICLE_LIST' | 'FLEET_QUOTE_LIST' | 'TRIP_OPERATIONS_LIST' + | 'SELFOP_INVENTORY_TURNOVER' | 'CUSTOMER_LIST' | 'DOMESTIC_WAREHOUSE_ORDER_LIST' | 'FINANCE_CUSTOMER_ACCOUNT_LIST' diff --git a/src/modules/downloads/pages/DownloadCenterView.vue b/src/modules/downloads/pages/DownloadCenterView.vue index 888d5541..0b727d0e 100644 --- a/src/modules/downloads/pages/DownloadCenterView.vue +++ b/src/modules/downloads/pages/DownloadCenterView.vue @@ -34,6 +34,7 @@ const exportTypeMeta: Record VEHICLE_LIST: { label: '车辆', retryPermission: 'platform:overseas-master-data:vehicle:export' }, FLEET_QUOTE_LIST: { label: '车队报价', retryPermission: 'platform:fleet:quote:export' }, TRIP_OPERATIONS_LIST: { label: '出车记录', retryPermission: 'platform:fleet:operations:export' }, + SELFOP_INVENTORY_TURNOVER: { label: '库存周转', retryPermission: 'platform:self-operated-analytics:export' }, FINANCE_RECEIVABLE_STATISTICS: { label: '应收统计', retryPermission: 'platform:finance:receivable-statistics:export' }, LOADING_LIST_BUY_DECLARATION_ITEMS: { label: '买单报关明细', retryPermission: 'platform:firstleg:customs:manage' }, CUSTOMER_LIST: { label: '客户列表', retryPermission: 'platform:customer:export' }, diff --git a/src/modules/downloads/useExportTask.test.ts b/src/modules/downloads/useExportTask.test.ts index 1cf0a9ae..de7abc6e 100644 --- a/src/modules/downloads/useExportTask.test.ts +++ b/src/modules/downloads/useExportTask.test.ts @@ -36,7 +36,7 @@ describe('download center export submission', () => { expect(showExportTaskCreatedDialog).not.toHaveBeenCalled() expect(push).not.toHaveBeenCalled() }) - it.each([["overseas-warehouse/pages/LastMileOrdersView.vue","LAST_MILE_ORDER_LIST","platform:overseas-warehouse:last-mile-order:export"],["overseas-warehouse/pages/InventoryView.vue","OVERSEAS_INVENTORY_BALANCE","platform:overseas-warehouse:inventory:export"],["overseas-warehouse/pages/InventoryTransactionsView.vue","OVERSEAS_INVENTORY_LEDGER","platform:overseas-warehouse:inventory:export"],["overseas-master-data/pages/OverseasQuotesView.vue","OVERSEAS_QUOTE_LIST","platform:overseas-master-data:quote:export"],["overseas-master-data/pages/ExchangeRatesView.vue","MONTHLY_EXCHANGE_RATE_LIST","platform:overseas-master-data:exchange-rate:export"],["overseas-master-data/pages/VehiclesView.vue","VEHICLE_LIST","platform:overseas-master-data:vehicle:export"],["overseas-master-data/pages/FleetQuotesView.vue","FLEET_QUOTE_LIST","platform:fleet:quote:export"],["overseas-master-data/pages/TripOperationsView.vue","TRIP_OPERATIONS_LIST","platform:fleet:operations:export"]])('connects %s with the correct type and download-center retry permission', (path, type, permission) => { + it.each([["self-operated/components/InventoryTurnoverView.vue","SELFOP_INVENTORY_TURNOVER","platform:self-operated-analytics:export"],["overseas-warehouse/pages/LastMileOrdersView.vue","LAST_MILE_ORDER_LIST","platform:overseas-warehouse:last-mile-order:export"],["overseas-warehouse/pages/InventoryView.vue","OVERSEAS_INVENTORY_BALANCE","platform:overseas-warehouse:inventory:export"],["overseas-warehouse/pages/InventoryTransactionsView.vue","OVERSEAS_INVENTORY_LEDGER","platform:overseas-warehouse:inventory:export"],["overseas-master-data/pages/OverseasQuotesView.vue","OVERSEAS_QUOTE_LIST","platform:overseas-master-data:quote:export"],["overseas-master-data/pages/ExchangeRatesView.vue","MONTHLY_EXCHANGE_RATE_LIST","platform:overseas-master-data:exchange-rate:export"],["overseas-master-data/pages/VehiclesView.vue","VEHICLE_LIST","platform:overseas-master-data:vehicle:export"],["overseas-master-data/pages/FleetQuotesView.vue","FLEET_QUOTE_LIST","platform:fleet:quote:export"],["overseas-master-data/pages/TripOperationsView.vue","TRIP_OPERATIONS_LIST","platform:fleet:operations:export"]])('connects %s with the correct type and download-center retry permission', (path, type, permission) => { const page = readFileSync(new URL('../' + path, import.meta.url), 'utf8') expect(page).toContain("await submitExport('" + type + "'") expect(page).not.toMatch(/saveCsv\(|saveFleetCsv\(/) diff --git a/src/modules/self-operated/api/analytics.test.ts b/src/modules/self-operated/api/analytics.test.ts index f3efdeef..a54fc1e8 100644 --- a/src/modules/self-operated/api/analytics.test.ts +++ b/src/modules/self-operated/api/analytics.test.ts @@ -5,6 +5,11 @@ const http = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() })) vi.mock('@/shared/api/http', () => ({ http, unwrapApiResponse: (value: { data: unknown }) => value.data })) beforeEach(() => { vi.clearAllMocks(); http.get.mockResolvedValue({ data: {} }); http.post.mockResolvedValue({ data: { taskNo: 'COL-001', status: 'QUEUED' } }) }) describe('self-operated platform transport contract', () => { + it('sends multiple turnover owners as repeated query keys, without bracket suffixes', async () => { + const params = { sourceAccount: 'primary', warehouseCode: 'W', endDate: '2026-10-14', baseWindowDays: 7, groupBy: 'SKU' as const, customerCodes: ['A', 'B'], focus: 'ALL' as const, sortBy: 'PHYSICAL_DESC' as const, current: 1, size: 20, includeOverview: true } + await analyticsApi.turnover(params) + expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/self-operated-analytics/inventory-turnover', { params, paramsSerializer: { indexes: null }, signal: undefined }) + }) it('forwards the inventory window, inclusive end and pinned page without renaming them', async () => { const params = { sourceAccount: 'lingxing-primary', warehouseCode: 'WH-A', salesWindowDays: 10 as const, salesWindowEndDate: '2026-10-12', current: 2, size: 20, salesRunId: '11', inventoryRunId: '12', queryAsOf: '2026-10-13T00:00:00Z' } await analyticsApi.inventory(params) diff --git a/src/modules/self-operated/api/analytics.ts b/src/modules/self-operated/api/analytics.ts index 9f9a1c06..9fe72980 100644 --- a/src/modules/self-operated/api/analytics.ts +++ b/src/modules/self-operated/api/analytics.ts @@ -9,7 +9,10 @@ async function get(path: string, params: object, signal?: AbortSignal): Promi } export const analyticsApi = { ownerDirectory: (query: { sourceAccount: string; warehouseCode: string } & Partial, signal?: AbortSignal) => get('owner-directory', query, signal), - turnover: (query: TurnoverQuery, signal?: AbortSignal) => get('inventory-turnover', query, signal), + async turnover(query: TurnoverQuery, signal?: AbortSignal): Promise { + const response = await http.get, ApiEnvelope>(`${base}/inventory-turnover`, { params: query, paramsSerializer: { indexes: null }, signal }) + return unwrapApiResponse(response) + }, dashboard: (query: DashboardQuery, signal?: AbortSignal) => get('dashboard', query, signal), owners: (query: OwnerQuery, signal?: AbortSignal) => get>('owners', query, signal), ownerOverview: (query: OwnerOverviewQuery, signal?: AbortSignal) => get('owner-overview', query, signal), diff --git a/src/modules/self-operated/components/DashboardCharts.test.ts b/src/modules/self-operated/components/DashboardCharts.test.ts index 06d82946..ec78ee0d 100644 --- a/src/modules/self-operated/components/DashboardCharts.test.ts +++ b/src/modules/self-operated/components/DashboardCharts.test.ts @@ -10,6 +10,17 @@ function point(date: string, value: number | null, dataStatus = 'COMPLETE'): Dai return { businessDate: date, outboundQuantity: dataStatus === 'COMPLETE' ? value : null, observedQuantity: value, dataStatus, reasonCodes: [], quantityUnit: 'PCS' } } describe('dashboard chart integrity and interaction', () => { + it('keeps dates and line explanations behind the description action', async () => { + const wrapper = mount(DashboardTrend, { props: { points: [point('2026-10-01', 2)] }, slots: { explanation: '

统计日期示例

' } }) + expect(wrapper.text()).not.toContain('统计日期示例') + expect(wrapper.text()).not.toContain('实线/实心点') + await wrapper.findAll('button').find(button => button.text() === '说明')!.trigger('click') + expect(wrapper.text()).toContain('统计日期示例') + expect(wrapper.text()).toContain('实线/实心点') + await wrapper.setProps({ points: [point('2026-10-02', 3)] }) + expect(wrapper.find('[role=dialog]').exists()).toBe(false) + wrapper.unmount() + }) it('fills only contiguous known segments and keeps unknown days unavailable for drilldown', async () => { const wrapper = mount(DashboardTrend, { props: { points: [point('2026-10-01', 2), point('2026-10-02', 4), point('2026-10-03', null, 'UNKNOWN'), point('2026-10-04', 1, 'PARTIAL')] } }) const chart = wrapper.findComponent({ name: 'VChart' }) diff --git a/src/modules/self-operated/components/DashboardTrend.vue b/src/modules/self-operated/components/DashboardTrend.vue index 3360c2eb..41fae0d3 100644 --- a/src/modules/self-operated/components/DashboardTrend.vue +++ b/src/modules/self-operated/components/DashboardTrend.vue @@ -19,10 +19,12 @@ function selectChart(event: ECElementEvent) { const point = props.points[event.d