1
This commit is contained in:
@@ -37,7 +37,7 @@ describe('InboundTaskList measurement scope', () => {
|
||||
|
||||
expect(source).toContain(':tenant-id="selectedInbound.tenantId"')
|
||||
expect(source).toContain(':customer-id="selectedInbound.tenantId"')
|
||||
expect(source).toContain(':customer-name="selectedInbound.customerName || selectedInbound.companyName"')
|
||||
expect(source).toContain(':customer-name="customerDisplayName(selectedInbound)"')
|
||||
expect(source).toContain(':channel-name="selectedInbound.channelName"')
|
||||
expect(source).not.toContain('selectedInbound.customerId || selectedInbound.tenantId')
|
||||
})
|
||||
|
||||
@@ -65,6 +65,9 @@ const actionPermission = computed(() => isReview.value
|
||||
const taskLabel = computed(() => isReview.value ? '审核' : '测量')
|
||||
const emptyText = computed(() => isReview.value ? '暂无待审核的测量数据' : '暂无当前可测量的订单')
|
||||
const preferenceKey = computed(() => isReview.value ? 'inbound-measurement-reviews' : 'inbound-measurements')
|
||||
const customerNameById = computed(() => new Map(customers.value.map((customer) => [
|
||||
String(customer.id), customer.legalName?.trim() || customer.tenantName,
|
||||
])))
|
||||
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{
|
||||
@@ -111,6 +114,11 @@ function text(value: unknown) {
|
||||
return value === null || value === undefined || value === '' ? '—' : String(value)
|
||||
}
|
||||
|
||||
function customerDisplayName(inbound: InboundOrderSummary) {
|
||||
const snapshot = inbound.customerName?.trim() || inbound.companyName?.trim()
|
||||
return snapshot || customerNameById.value.get(String(inbound.customerId || inbound.tenantId)) || ''
|
||||
}
|
||||
|
||||
function formatNumber(value: number | null | undefined) {
|
||||
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: 3 })
|
||||
}
|
||||
@@ -440,6 +448,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
v-if="!isReview && selectedInbound"
|
||||
v-model="measurementDialogOpen"
|
||||
:customer-id="selectedInbound.tenantId"
|
||||
:customer-name="customerDisplayName(selectedInbound)"
|
||||
:inbound-id="selectedInbound.id"
|
||||
:inbound-code="selectedInbound.inboundCode"
|
||||
@updated="handleMeasurementUpdated"
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('InboundWorkflowWorkspace', () => {
|
||||
expect(source).toContain('@click="reviewDialogOpen = true"')
|
||||
expect(source).toContain(':key="`review-dialog-${revision}`"')
|
||||
expect(source).toContain(':customer-name="detail?.summary.customerName || detail?.summary.companyName"')
|
||||
expect(source).toContain(':customer-name="detail?.summary.customerName || detail?.summary.companyName"')
|
||||
expect(source).toContain(':channel-name="detail?.summary.channelName"')
|
||||
expect(source).toContain('@changed="handleReviewChanged"')
|
||||
expect(source).not.toContain('<template #footer>')
|
||||
|
||||
@@ -197,6 +197,7 @@ onBeforeUnmount(() => {
|
||||
<InboundMeasurementDialog
|
||||
v-model="measurementDialogOpen"
|
||||
:customer-id="customerId"
|
||||
:customer-name="detail?.summary.customerName || detail?.summary.companyName"
|
||||
:inbound-id="inboundId"
|
||||
:inbound-code="title"
|
||||
@updated="handleDomainChanged"
|
||||
|
||||
@@ -20,6 +20,8 @@ describe('InboundMeasurementDialog', () => {
|
||||
expect(densitySource).toContain('padding: var(--admin-space-4, 10px) 48px var(--admin-space-4, 10px) var(--admin-space-6, 16px)')
|
||||
expect(source).toContain('<WeightOcrPanel')
|
||||
expect(source).toContain('<InboundMeasurementPanel')
|
||||
expect(source).toContain('customerName?: string | null')
|
||||
expect(source).toContain(':customer-name="customerName"')
|
||||
expect(source).toContain('class="measurement-dialog-weight"')
|
||||
expect(source).not.toContain('class="surface-panel measurement-dialog-weight"')
|
||||
expect(source).toContain('border-bottom: 1px solid var(--cc-border)')
|
||||
|
||||
@@ -10,6 +10,7 @@ import WeightOcrPanel from '../weight/components/WeightOcrPanel.vue'
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
customerId: string
|
||||
customerName?: string | null
|
||||
inboundId: string
|
||||
inboundCode?: string | null
|
||||
openWarehouseSuggestion?: boolean
|
||||
@@ -92,6 +93,7 @@ function handleSubmitted() {
|
||||
<InboundMeasurementPanel
|
||||
ref="measurementPanelRef"
|
||||
:customer-id="customerId"
|
||||
:customer-name="customerName"
|
||||
:inbound-id="inboundId"
|
||||
:open-warehouse-suggestion="openWarehouseSuggestion"
|
||||
@updated="handleUpdated"
|
||||
|
||||
@@ -52,6 +52,7 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
|
||||
channelName: '美西海运',
|
||||
processStatus: 'MEASURING',
|
||||
measureReviewStatus: null,
|
||||
customerFeeConfirmed: false,
|
||||
recommendedPalletType: 'STANDARD',
|
||||
reviewReason: null,
|
||||
reviewerId: null,
|
||||
@@ -135,7 +136,7 @@ async function mountPanel(permissions = [
|
||||
'platform:firstleg:inbound:measure:edit',
|
||||
'platform:firstleg:inbound:measure:manual-entry',
|
||||
'platform:firstleg:inbound:measure:submit',
|
||||
]) {
|
||||
], props: Partial<{ customerId: string; inboundId: string; customerName: string }> = {}) {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const auth = useAuthStore()
|
||||
@@ -143,7 +144,7 @@ async function mountPanel(permissions = [
|
||||
auth.permissions = permissions
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
const wrapper = mount(InboundMeasurementPanel, {
|
||||
props: { customerId: '10001', inboundId: '7001' },
|
||||
props: { customerId: '10001', inboundId: '7001', ...props },
|
||||
attachTo: document.body,
|
||||
global: { plugins: [pinia, ElementPlus] },
|
||||
})
|
||||
@@ -195,6 +196,13 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(wrapper.find('button[aria-label="预览测量图片"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('uses the list customer name when a legacy measurement detail has no customer snapshot', async () => {
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ companyName: '' }))
|
||||
const wrapper = await mountPanel(undefined, { customerName: '柏悦物流' })
|
||||
|
||||
expect(wrapper.find('.measurement-summary-context').text()).toContain('柏悦物流')
|
||||
})
|
||||
|
||||
it('warns when declared dimensions or gross weight differ from measurements by at least ten percent', async () => {
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({
|
||||
actualGrossWeightKg: 132,
|
||||
@@ -499,7 +507,7 @@ describe('InboundMeasurementPanel', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(
|
||||
expect.stringContaining('准备托盘与库位占用'),
|
||||
expect.stringContaining('提交成功后本批次不能继续编辑'),
|
||||
'提交测量审核',
|
||||
expect.objectContaining({ confirmButtonText: '确认提交' }),
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ const props = defineProps<{
|
||||
customerId?: string
|
||||
tenantId?: string
|
||||
inboundId: string
|
||||
customerName?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -110,6 +111,7 @@ const canEdit = computed(() => access.can('platform:firstleg:inbound:measure:edi
|
||||
const canManualEntry = computed(() => access.can('platform:firstleg:inbound:measure:manual-entry'))
|
||||
const canSubmit = computed(() => access.can('platform:firstleg:inbound:measure:submit'))
|
||||
const customerId = computed(() => props.customerId || props.tenantId || '')
|
||||
const displayCustomerName = computed(() => detail.value?.companyName?.trim() || props.customerName?.trim() || '客户未命名')
|
||||
const hasContext = computed(() => Boolean(customerId.value && props.inboundId))
|
||||
const countEditableProcess = computed(() => ['PREPARING', 'WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
const submitEditableProcess = computed(() => ['PREPARING', 'WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
@@ -698,7 +700,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
<section class="measurement-section measurement-summary-section">
|
||||
<header class="measurement-summary-header">
|
||||
<div class="measurement-summary-context">
|
||||
<p><span>客户</span>{{ detail.companyName || '客户未命名' }}</p>
|
||||
<p><span>客户</span>{{ displayCustomerName }}</p>
|
||||
<p><span>渠道</span>{{ detail.channelName || detail.channelCode || '渠道未填写' }}</p>
|
||||
</div>
|
||||
<div class="measurement-summary-status">
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface InboundMeasurementDetail {
|
||||
expectedPalletCount?: number | null
|
||||
processStatus: InboundMeasurementProcessStatus
|
||||
measureReviewStatus: string | null
|
||||
customerFeeConfirmed: boolean
|
||||
recommendedPalletType: string | null
|
||||
reviewReason: string | null
|
||||
reviewerId: MeasurementIdentifier | null
|
||||
|
||||
@@ -21,10 +21,12 @@ export interface DispatchPalletCandidateDto {
|
||||
planNo?: string | null
|
||||
orderType?: string | null
|
||||
volumeWeightKg?: number | null
|
||||
grossWeightKg?: number | null
|
||||
containerNo?: string | null
|
||||
appointmentAt?: string | null
|
||||
expectedLoadingAt?: string | null
|
||||
overseasWarehouseId?: string | null
|
||||
overseasWarehouseCode?: string | null
|
||||
overseasWarehouseName?: string | null
|
||||
orderNo?: string | null
|
||||
orderRefs?: string | null
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface InboundOrderSummaryDto {
|
||||
inboundCode?: string | null
|
||||
lxCode?: string | null
|
||||
orderType?: string | null
|
||||
overseasWarehouseId?: string | null
|
||||
overseasWarehouseCode?: string | null
|
||||
overseasWarehouseName?: string | null
|
||||
warehouseId?: string | null
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { getInventorySummary, listInventoryBalances, listInventoryLedger, listInventoryPositions } from './inventory'
|
||||
import { downloadInventoryBalances, downloadInventoryLedger, getInventorySummary, listInventoryBalances, listInventoryLedger, listInventoryPositions } from './inventory'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } }))
|
||||
|
||||
describe('overseas inventory API', () => {
|
||||
beforeEach(() => { vi.clearAllMocks() })
|
||||
|
||||
it('returns the binary response directly for inventory exports', async () => {
|
||||
const balances = new Blob(['customerId\\r\\n10001\\r\\n'], { type: 'text/csv' })
|
||||
const ledger = new Blob(['occurredAt\\r\\n'], { type: 'text/csv' })
|
||||
vi.mocked(overseasHttp.get).mockResolvedValueOnce(balances).mockResolvedValueOnce(ledger)
|
||||
|
||||
await expect(downloadInventoryBalances({ skuCode: 'SKU-1' })).resolves.toBe(balances)
|
||||
await expect(downloadInventoryLedger({ inboundCode: 'IN-1' })).resolves.toBe(ledger)
|
||||
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/export', {
|
||||
params: { skuCode: 'SKU-1' }, responseType: 'blob',
|
||||
})
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger/export', {
|
||||
params: { inboundCode: 'IN-1' }, responseType: 'blob',
|
||||
})
|
||||
})
|
||||
|
||||
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 = { page: 1, size: 20, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' }
|
||||
|
||||
@@ -118,8 +118,7 @@ export async function listInventoryBalances(query: InventoryBalanceQuery) {
|
||||
}
|
||||
|
||||
export async function downloadInventoryBalances(query: Partial<InventoryBalanceQuery>) {
|
||||
const response = await overseasHttp.get<Blob>('/platform/v1/last-leg/inventory/balances/export', { params: query, responseType: 'blob' })
|
||||
return response.data
|
||||
return overseasHttp.get<Blob, Blob>('/platform/v1/last-leg/inventory/balances/export', { params: query, responseType: 'blob' })
|
||||
}
|
||||
|
||||
export async function getInventorySummary(query: InventoryBalanceQuery) {
|
||||
@@ -168,6 +167,5 @@ export async function listInventoryLedger(query: InventoryLedgerQuery) {
|
||||
}
|
||||
|
||||
export async function downloadInventoryLedger(query: Partial<InventoryLedgerQuery>) {
|
||||
const response = await overseasHttp.get<Blob>('/platform/v1/last-leg/inventory/ledger/export', { params: query, responseType: 'blob' })
|
||||
return response.data
|
||||
return overseasHttp.get<Blob, Blob>('/platform/v1/last-leg/inventory/ledger/export', { params: query, responseType: 'blob' })
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
import { dispatchVehicleSpecs } from '../dispatchVehicleSpecs'
|
||||
import OrderReferenceText from './OrderReferenceText.vue'
|
||||
import { matchesSmartDispatchWarehouseTab, type SmartDispatchWarehouseTab } from '../smartDispatchWarehouseTab'
|
||||
|
||||
interface LoadingContainer {
|
||||
id: string
|
||||
@@ -50,7 +51,7 @@ const query = reactive({
|
||||
sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED' | 'RELABEL',
|
||||
fbaWarehouseCode: '',
|
||||
})
|
||||
const warehouseTab = ref<'ALL' | 'LAX' | 'NYC'>('ALL')
|
||||
const warehouseTab = ref<SmartDispatchWarehouseTab>('ALL')
|
||||
|
||||
const orderTypeOptions = [
|
||||
{ value: 'FIRST_LEG', label: '头程订单' },
|
||||
@@ -65,9 +66,7 @@ const visibleCandidates = computed(() =>
|
||||
candidates.value.filter((item) => {
|
||||
const typeMatches = !query.orderType || String(item.orderType || '') === query.orderType
|
||||
if (!typeMatches) return false
|
||||
if (warehouseTab.value === 'ALL') return true
|
||||
const warehouse = `${item.overseasWarehouseId || ''} ${item.overseasWarehouseName || ''}`.toLowerCase()
|
||||
return warehouseTab.value === 'LAX' ? /lax|洛杉矶|los angeles/.test(warehouse) : /nyc|jfk|纽约|new york/.test(warehouse)
|
||||
return matchesSmartDispatchWarehouseTab(item, warehouseTab.value)
|
||||
}),
|
||||
)
|
||||
const orderGroups = computed(() => {
|
||||
@@ -88,7 +87,14 @@ const assigned = computed(() => assignedIds.value.map((id) => palletSnapshot(id)
|
||||
const assignedPackages = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
const assignedVolume = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const selectedVehicleSpec = computed(() => dispatchVehicleSpecs.find((item) => item.value === selectedVehicle.value))
|
||||
const weightLb = (item?: DispatchPalletCandidateDto) => Number(item?.volumeWeightKg || 0) * 2.2046226218
|
||||
function weightKg(item?: DispatchPalletCandidateDto) {
|
||||
const grossWeightKg = Number(item?.grossWeightKg)
|
||||
if (Number.isFinite(grossWeightKg) && grossWeightKg > 0) return grossWeightKg
|
||||
const volumeWeightKg = Number(item?.volumeWeightKg)
|
||||
return Number.isFinite(volumeWeightKg) && volumeWeightKg > 0 ? volumeWeightKg : 0
|
||||
}
|
||||
const weightLb = (item?: DispatchPalletCandidateDto) => weightKg(item) * 2.2046226218
|
||||
const orderWeightLb = (pallets: DispatchPalletCandidateDto[]) => pallets.reduce((sum, item) => sum + weightLb(item), 0)
|
||||
const containerWeightLb = (container: LoadingContainer) => container.palletIds.reduce((sum, id) => sum + weightLb(palletSnapshot(id)), 0)
|
||||
|
||||
function palletId(item: DispatchPalletCandidateDto) {
|
||||
@@ -390,7 +396,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
:key="tab.value"
|
||||
type="button"
|
||||
:class="['warehouse-tab', { active: warehouseTab === tab.value }]"
|
||||
@click="warehouseTab = tab.value as 'ALL' | 'LAX' | 'NYC'"
|
||||
@click="warehouseTab = tab.value as SmartDispatchWarehouseTab"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
@@ -428,7 +434,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
><template #default="{ row }">{{ orderType(row.first.orderType) }}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="重量(lb)" width="100" align="right"
|
||||
><template #default="{ row }">{{ number(Number(row.first.volumeWeightKg || 0) * 2.2046226218, 0) }}LB</template></el-table-column
|
||||
><template #default="{ row }">{{ number(orderWeightLb(row.pallets), 0) }}LB</template></el-table-column
|
||||
>
|
||||
<el-table-column label="托盘数量" width="90" align="right"
|
||||
><template #default="{ row }">{{ row.pallets.length }}</template></el-table-column
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatOverseasDateTime, inventoryBusinessTypeLabel, lastMileOrderReference, lastMileStatusLabel, packageDifferenceText, relabelBusinessStatusLabel, toLastMileOrderListItem } from './lastMilePresentation'
|
||||
import { formatOverseasDateTime, inboundPlanStatusLabel, inventoryBusinessTypeLabel, lastMileOrderReference, lastMileStatusLabel, packageDifferenceText, relabelBusinessStatusLabel, toLastMileOrderListItem } from './lastMilePresentation'
|
||||
|
||||
describe('last-mile order presentation adapter', () => {
|
||||
it('formats overseas timestamps with zero-padded seconds', () => {
|
||||
@@ -109,6 +109,18 @@ describe('last-mile order presentation adapter', () => {
|
||||
expect(lastMileStatusLabel(status)).toBe(expected)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['PENDING_ASSIGNMENT', '待入库'],
|
||||
['ASSIGNED', '待入库'],
|
||||
['PDA_PENDING', '待入库'],
|
||||
['SIGNED', '已签收'],
|
||||
['PICKING', '拣货中'],
|
||||
['PARTIAL_INBOUND', '部分入库'],
|
||||
['COMPLETED', '全部入库'],
|
||||
])('maps inbound plan status %s to %s', (status, expected) => {
|
||||
expect(inboundPlanStatusLabel(status)).toBe(expected)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['RELABEL_APPLICATION', '换标下单'],
|
||||
['WAREHOUSE_TRANSFER', '仓储转运'],
|
||||
|
||||
@@ -38,6 +38,16 @@ const statusLabels: Record<string, string> = {
|
||||
CANCELLED: '已取消',
|
||||
}
|
||||
|
||||
const inboundPlanStatusLabels: Record<string, string> = {
|
||||
PENDING_ASSIGNMENT: '待入库',
|
||||
ASSIGNED: '待入库',
|
||||
PDA_PENDING: '待入库',
|
||||
SIGNED: '已签收',
|
||||
PICKING: '拣货中',
|
||||
PARTIAL_INBOUND: '部分入库',
|
||||
COMPLETED: '全部入库',
|
||||
}
|
||||
|
||||
const inventoryBusinessTypeLabels: Record<string, string> = {
|
||||
IN_TRANSIT: '在途入账',
|
||||
STOCK_IN: '入库',
|
||||
@@ -74,6 +84,11 @@ export function lastMileStatusLabel(value: unknown) {
|
||||
return statusLabels[code] || text(code)
|
||||
}
|
||||
|
||||
export function inboundPlanStatusLabel(value: unknown) {
|
||||
const code = String(value ?? '').trim()
|
||||
return inboundPlanStatusLabels[code] || text(code)
|
||||
}
|
||||
|
||||
export function packageDifferenceText(value: number | '—' | null | undefined) {
|
||||
if (value == null || value === '—' || !Number.isFinite(value)) return '—'
|
||||
return value > 0 ? `+${value}` : String(value)
|
||||
|
||||
@@ -8,7 +8,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { getInboundPlan, type InboundPlanDetailDto } from '../api/inboundPlans'
|
||||
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
import { formatOverseasDateTime, lastMileOrderTypeLabel } from '../lastMilePresentation'
|
||||
import { formatOverseasDateTime, inboundPlanStatusLabel, lastMileOrderTypeLabel } from '../lastMilePresentation'
|
||||
|
||||
const route = useRoute(); const router = useRouter()
|
||||
const planNo = computed(() => String(route.params.orderId || ''))
|
||||
@@ -21,7 +21,7 @@ function receivingWarehouse(code: unknown, name: unknown) {
|
||||
const normalizedName = String(name ?? '').trim()
|
||||
return normalizedCode && normalizedName ? `${normalizedCode} · ${normalizedName}` : '—'
|
||||
}
|
||||
function status(value: unknown) { const labels: Record<string, string> = { PENDING_ASSIGNMENT: '待入库', PDA_PENDING: '待入库', ASSIGNED: '待入库', PICKING: '拣货中', PARTIAL_INBOUND: '部分入库', COMPLETED: '全部入库' }; return labels[String(value || '')] || text(value) }
|
||||
function status(value: unknown) { return inboundPlanStatusLabel(value) }
|
||||
const firstLine = computed(() => detail.value?.lines[0])
|
||||
const fieldValues = computed<Record<string, string | number>>(() => ({
|
||||
入库单号: text(detail.value?.planNo), 入仓码: text(firstLine.value?.inboundCode), 类型: lastMileOrderTypeLabel(detail.value?.orderType), 货主: text(detail.value?.customerName), 关联订单: text(detail.value?.sourceOrderNo),
|
||||
|
||||
@@ -18,7 +18,7 @@ import SmartDispatchDialog from '../components/SmartDispatchDialog.vue'
|
||||
import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, updateInboundPlan, type InboundOrderSummaryDto } from '../api/inboundOrders'
|
||||
import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { formatOverseasDateTime, lastMileOrderTypeLabel } from '../lastMilePresentation'
|
||||
import { formatOverseasDateTime, inboundPlanStatusLabel, lastMileOrderTypeLabel } from '../lastMilePresentation'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -37,7 +37,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderNo', type: 'input', label: '入库单号', width: 165, placeholder: '入库单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 145, placeholder: '入仓码' },
|
||||
{ key: 'lxCode', type: 'input', label: 'LX代码', width: 160, placeholder: '领星入库单代码' },
|
||||
{ key: 'status', type: 'select', label: '状态', width: 130, placeholder: '全部状态', options: [{ label: '待入库', value: 'PENDING_ASSIGNMENT' }, { label: '拣货中', value: 'PICKING' }, { label: '全部入库', value: 'COMPLETED' }, { label: '部分入库', value: 'PARTIAL_INBOUND' }] },
|
||||
{ key: 'status', type: 'select', label: '状态', width: 130, placeholder: '全部状态', options: [{ label: '待入库', value: 'PENDING_ASSIGNMENT' }, { label: '已签收', value: 'SIGNED' }, { label: '拣货中', value: 'PICKING' }, { label: '全部入库', value: 'COMPLETED' }, { label: '部分入库', value: 'PARTIAL_INBOUND' }] },
|
||||
{ key: 'warehouse', type: 'input', label: '海外仓', width: 155, placeholder: '全部海外仓' },
|
||||
{ key: 'fbaWarehouse', type: 'input', label: 'FBA仓库', width: 145, placeholder: '全部FBA仓库' },
|
||||
{ key: 'appointmentStatus', type: 'select', label: '预约状态', width: 145, placeholder: '全部预约状态', options: [{ label: '未预约', value: 'UNAPPOINTED' }, { label: '预约中', value: 'PENDING' }, { label: '预约成功', value: 'SUCCESS' }, { label: '预约失败', value: 'FAILED' }, { label: '已取消', value: 'CANCELLED' }] },
|
||||
@@ -95,7 +95,6 @@ function receivingWarehouse(code: unknown, name: unknown) {
|
||||
}
|
||||
function number(value: unknown) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : 0 }
|
||||
function typeLabel(value: unknown) { return lastMileOrderTypeLabel(value) }
|
||||
function planStatusLabel(value: unknown) { return ({ PENDING_ASSIGNMENT: '待入库', ASSIGNED: '待入库', PDA_PENDING: '待入库', PICKING: '拣货中', COMPLETED: '全部入库', PARTIAL_INBOUND: '部分入库' } as Record<string, string>)[String(value || '')] || text(value) }
|
||||
function appointmentStatusLabel(value: unknown) { return !value ? '未预约' : ({ PENDING: '预约中', SUCCESS: '预约成功', FAILED: '预约失败', CANCELLED: '已取消' } as Record<string, string>)[String(value)] || text(value) }
|
||||
|
||||
function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem {
|
||||
@@ -105,7 +104,7 @@ function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem {
|
||||
orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), lxCode: text(dto.lxCode), type: typeLabel(dto.orderType), warehouse: receivingWarehouse(dto.overseasWarehouseCode, dto.overseasWarehouseName),
|
||||
fbaWarehouse: text(dto.fbaWarehouseCode), owner: text(dto.customerName), cartons: number(dto.packageCount), pickedCartons: number(dto.pickedPackageCount), volume: number(dto.volumeCbm), pallets: number(dto.palletCount),
|
||||
assigned: dto.assigned ? '已分配' : '未分配', mergeAllowed: dto.mergeAllowed ? '可合托' : '不合托', urgent: dto.urgent ? '加急' : '普通',
|
||||
status: planStatusLabel(dto.status), appointmentNo: text(dto.appointmentNo), appointmentStatus: appointmentStatusLabel(dto.appointmentStatus),
|
||||
status: inboundPlanStatusLabel(dto.status), appointmentNo: text(dto.appointmentNo), appointmentStatus: appointmentStatusLabel(dto.appointmentStatus),
|
||||
appointmentAt: formatOverseasDateTime(dto.appointmentAt), expectedLoadingAt: formatOverseasDateTime(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车',
|
||||
createdAt: formatOverseasDateTime(dto.createdAt), version: number(dto.version),
|
||||
}
|
||||
|
||||
@@ -246,6 +246,13 @@ describe('LastMileOrdersView pending order and container conversion', () => {
|
||||
expect(availableFields.find((field) => field.key === 'lxCode')).toMatchObject({
|
||||
label: 'LX代码', placeholder: '请输入LX代码',
|
||||
})
|
||||
expect(availableFields.filter((field) => ['orderType', 'status', 'abnormal', 'detained'].includes(field.key)))
|
||||
.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ key: 'orderType', placeholder: '请选择订单类型' }),
|
||||
expect.objectContaining({ key: 'status', placeholder: '请选择当前状态' }),
|
||||
expect.objectContaining({ key: 'abnormal', placeholder: '请选择是否异常' }),
|
||||
expect.objectContaining({ key: 'detained', placeholder: '请选择是否暂扣' }),
|
||||
]))
|
||||
expect(availableFields.map((field) => field.key)).not.toEqual(expect.arrayContaining(['planNo', 'inboundOrderNo']))
|
||||
|
||||
await wrapper.findAll('.el-radio-button').find((item) => item.text() === '待分配')!.trigger('click')
|
||||
|
||||
@@ -122,7 +122,7 @@ const allFields: QueryField[] = [
|
||||
{ key: 'relabelNo', type: 'input', label: '换标单号', width: 190, placeholder: '请输入换标单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' },
|
||||
{ key: 'lxCode', type: 'input', label: 'LX代码', width: 180, placeholder: '请输入LX代码' },
|
||||
{ key: 'orderType', type: 'select', label: '订单类型', width: 170, placeholder: '全部类型', options: [
|
||||
{ key: 'orderType', type: 'select', label: '订单类型', width: 170, placeholder: '请选择订单类型', options: [
|
||||
{ label: '头程订单', value: 'FIRST_LEG' }, { label: '换标下单', value: 'RELABEL_APPLICATION' },
|
||||
{ label: '仓储转运', value: 'WAREHOUSE_TRANSFER' }, { label: '本土散板', value: 'LOCAL_LOOSEBOARD' },
|
||||
{ label: '退货换标', value: 'RETURN_RELABEL' }, { label: '一件代发', value: 'ONE_CLICK_DROPSHIP' },
|
||||
@@ -132,7 +132,7 @@ const allFields: QueryField[] = [
|
||||
{ key: 'sourceOrderNo', type: 'input', label: '来源订单号', width: 190, placeholder: '请输入来源订单号' },
|
||||
{ key: 'containerNo', type: 'input', label: '集装箱号', width: 180, placeholder: '请输入集装箱号' },
|
||||
{ key: 'transferWarehouseCode', type: 'input', label: '转仓代码', width: 170, placeholder: '请输入转仓代码' },
|
||||
{ key: 'status', type: 'select', label: '当前状态', width: 160, placeholder: '全部状态', options: [
|
||||
{ key: 'status', type: 'select', label: '当前状态', width: 160, placeholder: '请选择当前状态', options: [
|
||||
{ label: '待分配', value: 'PENDING_ASSIGNMENT' },
|
||||
{ label: '待换标', value: 'WAIT_RELABEL' },
|
||||
{ label: '待排车', value: 'READY_FOR_DISPATCH' },
|
||||
@@ -143,8 +143,8 @@ const allFields: QueryField[] = [
|
||||
{ label: '派送中', value: 'DELIVERING' },
|
||||
{ label: '派送完成', value: 'DELIVERED' },
|
||||
] },
|
||||
{ key: 'abnormal', type: 'select', label: '是否异常', width: 140, placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'detained', type: 'select', label: '是否暂扣', width: 140, placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'abnormal', type: 'select', label: '是否异常', width: 140, placeholder: '请选择是否异常', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'detained', type: 'select', label: '是否暂扣', width: 140, placeholder: '请选择是否暂扣', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] },
|
||||
{ key: 'createdAt', type: 'daterange', label: '创建时间', width: 280 },
|
||||
]
|
||||
const fields = computed<QueryField[]>(() => statusTab.value === '柜子'
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { matchesSmartDispatchWarehouseTab } from './smartDispatchWarehouseTab'
|
||||
|
||||
describe('smart dispatch warehouse tabs', () => {
|
||||
it('uses the overseas warehouse code when the warehouse name is unavailable', () => {
|
||||
const losAngeles = { overseasWarehouseCode: 'US-LAX-01', overseasWarehouseName: null }
|
||||
const newYork = { overseasWarehouseCode: 'US-NY-01', overseasWarehouseName: null }
|
||||
|
||||
expect(matchesSmartDispatchWarehouseTab(losAngeles, 'LAX')).toBe(true)
|
||||
expect(matchesSmartDispatchWarehouseTab(losAngeles, 'NYC')).toBe(false)
|
||||
expect(matchesSmartDispatchWarehouseTab(newYork, 'NYC')).toBe(true)
|
||||
expect(matchesSmartDispatchWarehouseTab(newYork, 'LAX')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the all tab unfiltered and supports warehouse names as a fallback', () => {
|
||||
const warehouse = { overseasWarehouseCode: null, overseasWarehouseName: '洛杉矶接柜仓' }
|
||||
|
||||
expect(matchesSmartDispatchWarehouseTab(warehouse, 'ALL')).toBe(true)
|
||||
expect(matchesSmartDispatchWarehouseTab(warehouse, 'LAX')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { DispatchPalletCandidateDto } from './api/dispatches'
|
||||
|
||||
export type SmartDispatchWarehouseTab = 'ALL' | 'LAX' | 'NYC'
|
||||
|
||||
export function matchesSmartDispatchWarehouseTab(
|
||||
candidate: Pick<DispatchPalletCandidateDto, 'overseasWarehouseCode' | 'overseasWarehouseName'>,
|
||||
tab: SmartDispatchWarehouseTab,
|
||||
) {
|
||||
if (tab === 'ALL') return true
|
||||
const warehouse = [candidate.overseasWarehouseCode, candidate.overseasWarehouseName]
|
||||
.map((value) => String(value || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
if (tab === 'LAX') return /lax|洛杉矶|los angeles/.test(warehouse)
|
||||
return /nyc|jfk|(?:^|[^a-z])ny(?:[^a-z]|$)|纽约|new[ -]?york/.test(warehouse)
|
||||
}
|
||||
@@ -28,6 +28,11 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(sendFeeSource).toContain("ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)")
|
||||
})
|
||||
|
||||
it('locks measurement changes as soon as the customer confirms the fee', () => {
|
||||
expect(source).toContain('&& !measurementDetail.value.customerFeeConfirmed')
|
||||
expect(source).toContain('费用已由客户确认,测量数据已锁定')
|
||||
})
|
||||
|
||||
it('prevents duplicate confirm-and-send chains and reuses one attempt idempotency key set', () => {
|
||||
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
|
||||
|
||||
@@ -440,7 +445,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"')
|
||||
expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !measurementStrategyEditable"/g)).toHaveLength(1)
|
||||
expect(source).toContain('const measurementStrategyEditable = computed')
|
||||
expect(source).toContain('客户已确认且该票已装柜完成后,箱规计算规则将冻结')
|
||||
expect(source).toContain('费用已由客户确认,测量数据已锁定')
|
||||
expect(source).toContain('if (!measurementStrategyEditable.value)')
|
||||
expect(source).toContain('const previousMode = measurementSectionModes.outer')
|
||||
expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode')
|
||||
|
||||
@@ -231,11 +231,13 @@ const measurementStrategyEditable = computed(() => (
|
||||
can('platform:firstleg:inbound:measure:edit')
|
||||
&& Boolean(measurementDetail.value?.measurementId)
|
||||
&& measurementDetail.value?.version != null
|
||||
&& !measurementDetail.value.customerFeeConfirmed
|
||||
))
|
||||
const measurementStrategyLockedHint = computed(() => {
|
||||
if (!can('platform:firstleg:inbound:measure:edit')) return '当前账号缺少测量编辑权限'
|
||||
if (!measurementDetail.value?.measurementId) return '当前没有可修改的测量批次'
|
||||
return '客户已确认且该票已装柜完成后,箱规计算规则将冻结'
|
||||
if (measurementDetail.value.customerFeeConfirmed) return '费用已由客户确认,测量数据已锁定'
|
||||
return '当前测量数据不可修改'
|
||||
})
|
||||
const cargoImagePreviewUrls = new Set<string>()
|
||||
const measurementDialogOpen = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user