修复旧码同名明细核销选择
This commit is contained in:
@@ -3,7 +3,9 @@ import { test } from 'node:test'
|
|||||||
import {
|
import {
|
||||||
findWriteOffItem,
|
findWriteOffItem,
|
||||||
getWriteOffItemAvailability,
|
getWriteOffItemAvailability,
|
||||||
getWriteOffItemQuantity
|
getWriteOffItemQuantity,
|
||||||
|
getWriteOffStatusText,
|
||||||
|
resolveWriteOffItemSelection
|
||||||
} from '../writeOffItem.ts'
|
} from '../writeOffItem.ts'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
@@ -51,3 +53,64 @@ test('marks legacy or unsupported or completed item unavailable', () => {
|
|||||||
assert.equal(getWriteOffItemAvailability({ ...items[0], snapshotOperationSupported: false }).available, false)
|
assert.equal(getWriteOffItemAvailability({ ...items[0], snapshotOperationSupported: false }).available, false)
|
||||||
assert.equal(getWriteOffItemAvailability({ ...items[0], writeOffCount: 2 }).available, false)
|
assert.equal(getWriteOffItemAvailability({ ...items[0], writeOffCount: 2 }).available, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('locks scanned v2 code to the specified write off item id', () => {
|
||||||
|
const selection = resolveWriteOffItemSelection(items, {
|
||||||
|
source: 'scan',
|
||||||
|
orderWriteOffItemId: 'item-2',
|
||||||
|
packageName: '双人套餐'
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(selection.locked, true)
|
||||||
|
assert.deepEqual(selection.options.map((item) => item.orderWriteOffItemId), ['item-2'])
|
||||||
|
assert.equal(selection.selectedItemId, 'item-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not auto select when legacy package name matches multiple available items', () => {
|
||||||
|
const selection = resolveWriteOffItemSelection(items, {
|
||||||
|
source: 'scan',
|
||||||
|
packageName: '双人套餐'
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(selection.mismatch, false)
|
||||||
|
assert.equal(selection.requiresManualSelection, true)
|
||||||
|
assert.equal(selection.selectedItemId, '')
|
||||||
|
assert.deepEqual(selection.options.map((item) => item.orderWriteOffItemId), ['item-1', 'item-2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('auto selects the only available item matched by legacy package name', () => {
|
||||||
|
const selection = resolveWriteOffItemSelection(
|
||||||
|
[
|
||||||
|
{ ...items[0], writeOffCount: 2 },
|
||||||
|
items[1]
|
||||||
|
],
|
||||||
|
{
|
||||||
|
source: 'scan',
|
||||||
|
packageName: '双人套餐'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(selection.selectedItemId, 'item-2')
|
||||||
|
assert.equal(selection.requiresManualSelection, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('default selection skips unavailable items', () => {
|
||||||
|
const selection = resolveWriteOffItemSelection([
|
||||||
|
{ ...items[0], writeOffCount: 2 },
|
||||||
|
items[1]
|
||||||
|
])
|
||||||
|
|
||||||
|
assert.equal(selection.selectedItemId, 'item-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('infers partial write off status from quantity when status is 0', () => {
|
||||||
|
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 0 }), '待核销')
|
||||||
|
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 1 }), '部分核销')
|
||||||
|
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 2 }), '已核销')
|
||||||
|
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 1, writeOffCount: 0 }), '已核销')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not override explicit non-zero write off status with quantity inference', () => {
|
||||||
|
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 2, writeOffCount: 0 }), '部分核销')
|
||||||
|
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 'PART_WRITE_OFF', writeOffCount: 0 }), '部分核销')
|
||||||
|
})
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ export interface WriteOffItemAvailability {
|
|||||||
reason: string
|
reason: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WriteOffItemSelectionInput {
|
||||||
|
source?: string
|
||||||
|
orderWriteOffItemId?: string
|
||||||
|
packageName?: string
|
||||||
|
fallbackName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WriteOffItemSelection {
|
||||||
|
options: CommodityPackageConfig[]
|
||||||
|
selectedItemId: string
|
||||||
|
locked: boolean
|
||||||
|
mismatch: boolean
|
||||||
|
requiresManualSelection: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
'0': '待核销',
|
'0': '待核销',
|
||||||
'1': '已核销',
|
'1': '已核销',
|
||||||
@@ -86,9 +101,79 @@ export const getFirstAvailableWriteOffItem = (items: CommodityPackageConfig[]) =
|
|||||||
return items.find((item) => getWriteOffItemAvailability(item).available)
|
return items.find((item) => getWriteOffItemAvailability(item).available)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matchesWriteOffItemName = (item: CommodityPackageConfig, packageName: string, fallbackName = '') => {
|
||||||
|
return getWriteOffItemName(item, fallbackName) === packageName || item.packageName === packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveWriteOffItemSelection = (
|
||||||
|
items: CommodityPackageConfig[],
|
||||||
|
input: WriteOffItemSelectionInput = {}
|
||||||
|
): WriteOffItemSelection => {
|
||||||
|
const source = String(input.source || '')
|
||||||
|
const itemId = String(input.orderWriteOffItemId || '').trim()
|
||||||
|
const packageName = String(input.packageName || '').trim()
|
||||||
|
const isScan = source === 'scan'
|
||||||
|
const locked = isScan && Boolean(itemId)
|
||||||
|
|
||||||
|
let options = items
|
||||||
|
if (locked) {
|
||||||
|
options = items.filter((item) => getWriteOffItemId(item) === itemId)
|
||||||
|
} else if (isScan && packageName) {
|
||||||
|
options = items.filter((item) => matchesWriteOffItemName(item, packageName, input.fallbackName))
|
||||||
|
}
|
||||||
|
|
||||||
|
const mismatch = isScan && Boolean(itemId || packageName) && options.length === 0
|
||||||
|
const availableOptions = options.filter((item) => getWriteOffItemAvailability(item).available)
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
selectedItemId: getWriteOffItemId(options[0]),
|
||||||
|
locked,
|
||||||
|
mismatch,
|
||||||
|
requiresManualSelection: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScan && packageName && availableOptions.length > 1) {
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
selectedItemId: '',
|
||||||
|
locked,
|
||||||
|
mismatch,
|
||||||
|
requiresManualSelection: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedById = itemId ? options.find((item) => getWriteOffItemId(item) === itemId) : undefined
|
||||||
|
const selectedItem = (
|
||||||
|
matchedById && getWriteOffItemAvailability(matchedById).available
|
||||||
|
? matchedById
|
||||||
|
: availableOptions[0] || matchedById || options[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
selectedItemId: getWriteOffItemId(selectedItem),
|
||||||
|
locked,
|
||||||
|
mismatch,
|
||||||
|
requiresManualSelection: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getWriteOffStatusText = (item?: CommodityPackageConfig) => {
|
export const getWriteOffStatusText = (item?: CommodityPackageConfig) => {
|
||||||
const status = item?.writeOffStatus ?? item?.packageStatus
|
const status = item?.writeOffStatus ?? item?.packageStatus
|
||||||
if (status === undefined || status === null || status === '') return ''
|
|
||||||
const key = String(status)
|
const key = String(status)
|
||||||
|
const writtenOff = toNumber(item?.writeOffCount)
|
||||||
|
const total = toNumber(item?.count)
|
||||||
|
if (key === '1') return '已核销'
|
||||||
|
if ((status === undefined || status === null || status === '' || key === '0') && (writtenOff !== undefined || total !== undefined)) {
|
||||||
|
const writtenOffCount = Math.max(writtenOff ?? 0, 0)
|
||||||
|
const totalCount = Math.max(total ?? 0, 0)
|
||||||
|
if (writtenOffCount <= 0) return '待核销'
|
||||||
|
if (totalCount > 0 && writtenOffCount >= totalCount) return '已核销'
|
||||||
|
return '部分核销'
|
||||||
|
}
|
||||||
|
if (status === undefined || status === null || status === '') return ''
|
||||||
return statusTextMap[key] || key
|
return statusTextMap[key] || key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import StatusTag from '@/components/StatusTag.vue'
|
|||||||
import type { CommodityPackageConfig, UserOrderDetail } from '@/types/order'
|
import type { CommodityPackageConfig, UserOrderDetail } from '@/types/order'
|
||||||
import { canWriteOff } from '@/utils/constants'
|
import { canWriteOff } from '@/utils/constants'
|
||||||
import {
|
import {
|
||||||
findWriteOffItem,
|
|
||||||
getFirstAvailableWriteOffItem,
|
|
||||||
getWriteOffItemAvailability,
|
getWriteOffItemAvailability,
|
||||||
getWriteOffItemContent,
|
getWriteOffItemContent,
|
||||||
getWriteOffItemId,
|
getWriteOffItemId,
|
||||||
getWriteOffItemName,
|
getWriteOffItemName,
|
||||||
getWriteOffItemQuantity,
|
getWriteOffItemQuantity,
|
||||||
getWriteOffStatusText,
|
getWriteOffStatusText,
|
||||||
|
resolveWriteOffItemSelection,
|
||||||
toNumber
|
toNumber
|
||||||
} from '@/utils/writeOffItem'
|
} from '@/utils/writeOffItem'
|
||||||
import {
|
import {
|
||||||
@@ -36,8 +35,6 @@ const orderId = computed(() => String(route.query.orderId || ''))
|
|||||||
const routePackageName = computed(() => String(route.query.packageName || '').trim())
|
const routePackageName = computed(() => String(route.query.packageName || '').trim())
|
||||||
const routeWriteOffItemId = computed(() => String(route.query.orderWriteOffItemId || '').trim())
|
const routeWriteOffItemId = computed(() => String(route.query.orderWriteOffItemId || '').trim())
|
||||||
const isScanSource = computed(() => String(route.query.source || '') === 'scan')
|
const isScanSource = computed(() => String(route.query.source || '') === 'scan')
|
||||||
const isScanItemLocked = computed(() => isScanSource.value && Boolean(routeWriteOffItemId.value))
|
|
||||||
const hasScanMatchCriteria = computed(() => isScanSource.value && Boolean(routeWriteOffItemId.value || routePackageName.value))
|
|
||||||
|
|
||||||
const packageConfigs = computed<CommodityPackageConfig[]>(() => {
|
const packageConfigs = computed<CommodityPackageConfig[]>(() => {
|
||||||
if (!detail.value) return []
|
if (!detail.value) return []
|
||||||
@@ -51,24 +48,22 @@ const packageConfigs = computed<CommodityPackageConfig[]>(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const packageOptions = computed(() => {
|
const itemSelection = computed(() => {
|
||||||
if (isScanSource.value && routeWriteOffItemId.value) {
|
return resolveWriteOffItemSelection(packageConfigs.value, {
|
||||||
return packageConfigs.value.filter((item) => getWriteOffItemId(item) === routeWriteOffItemId.value)
|
source: String(route.query.source || ''),
|
||||||
}
|
orderWriteOffItemId: routeWriteOffItemId.value,
|
||||||
if (isScanSource.value && routePackageName.value) {
|
packageName: routePackageName.value,
|
||||||
return packageConfigs.value.filter(
|
fallbackName: detail.value?.commodityName
|
||||||
(item) =>
|
})
|
||||||
getWriteOffItemName(item, detail.value?.commodityName) === routePackageName.value ||
|
|
||||||
item.packageName === routePackageName.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return packageConfigs.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const packageMismatch = computed(() => hasScanMatchCriteria.value && packageOptions.value.length === 0)
|
const packageOptions = computed(() => itemSelection.value.options)
|
||||||
|
const isScanItemLocked = computed(() => itemSelection.value.locked)
|
||||||
|
const packageMismatch = computed(() => itemSelection.value.mismatch)
|
||||||
|
const requiresManualSelection = computed(() => itemSelection.value.requiresManualSelection)
|
||||||
|
|
||||||
const selectedPackage = computed(() => {
|
const selectedPackage = computed(() => {
|
||||||
return packageOptions.value.find((item) => getWriteOffItemId(item) === selectedItemId.value) || packageOptions.value[0]
|
return packageOptions.value.find((item) => getWriteOffItemId(item) === selectedItemId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const quantityInfo = computed(() => getWriteOffItemQuantity(selectedPackage.value))
|
const quantityInfo = computed(() => getWriteOffItemQuantity(selectedPackage.value))
|
||||||
@@ -119,13 +114,7 @@ const loadDetail = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
detail.value = await fetchOrderDetail(orderId.value)
|
detail.value = await fetchOrderDetail(orderId.value)
|
||||||
const matchedPackage = findWriteOffItem(packageConfigs.value, {
|
selectedItemId.value = itemSelection.value.selectedItemId
|
||||||
orderWriteOffItemId: routeWriteOffItemId.value,
|
|
||||||
packageName: routePackageName.value
|
|
||||||
})
|
|
||||||
const firstAvailablePackage = getFirstAvailableWriteOffItem(packageOptions.value)
|
|
||||||
const fallbackPackage = packageOptions.value[0] || packageConfigs.value[0]
|
|
||||||
selectedItemId.value = getWriteOffItemId(matchedPackage || firstAvailablePackage || fallbackPackage)
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -282,6 +271,10 @@ onMounted(loadDetail)
|
|||||||
<BadgeCheck :size="20" />
|
<BadgeCheck :size="20" />
|
||||||
<span>该订单当前不可核销,请返回订单详情核对状态。</span>
|
<span>该订单当前不可核销,请返回订单详情核对状态。</span>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-else-if="requiresManualSelection" class="panel warning-panel">
|
||||||
|
<BadgeCheck :size="20" />
|
||||||
|
<span>扫码套餐匹配到多个可核销明细,请选择后再核销。</span>
|
||||||
|
</section>
|
||||||
<section v-else-if="selectedPackage && !selectedItemAvailability.available" class="panel warning-panel">
|
<section v-else-if="selectedPackage && !selectedItemAvailability.available" class="panel warning-panel">
|
||||||
<BadgeCheck :size="20" />
|
<BadgeCheck :size="20" />
|
||||||
<span>{{ selectedItemAvailability.reason }}</span>
|
<span>{{ selectedItemAvailability.reason }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user