修复V4目录字段只读与精确查询
This commit is contained in:
@@ -146,8 +146,14 @@ type LookupState = {
|
||||
error: string
|
||||
result: ReservationV4CatalogLookupResult | null
|
||||
}
|
||||
type ExactLookupState = {
|
||||
loading: boolean
|
||||
searched: boolean
|
||||
item: ReservationV4CatalogLookupItem | null
|
||||
}
|
||||
|
||||
const invalidLookupValues = reactive<Record<string, string>>({})
|
||||
const exactLookupStates = reactive<Record<string, ExactLookupState>>({})
|
||||
const lookupStates = reactive<Record<ReservationV4LookupKind, LookupState>>({
|
||||
ACCOUNT: createLookupState(),
|
||||
ROOM_TYPE: createLookupState(),
|
||||
@@ -160,14 +166,15 @@ const lookupRequestVersions: Record<ReservationV4LookupKind, number> = {
|
||||
}
|
||||
const lookupKinds = computed(() => {
|
||||
const kinds = props.fields
|
||||
.filter((field) => isSelectControl(field) && isReservationV4EditableField(field))
|
||||
.filter((field) => isSelectControl(field) && isEditable(field))
|
||||
.map((field) => reservationV4LookupKindForOptionsSource(field.options_source))
|
||||
.filter((kind): kind is ReservationV4LookupKind => Boolean(kind))
|
||||
return [...new Set(kinds)]
|
||||
})
|
||||
const lookupKindKey = computed(() => lookupKinds.value.join('|'))
|
||||
|
||||
watch(
|
||||
[lookupKinds, () => props.hotelId],
|
||||
[lookupKindKey, () => props.hotelId],
|
||||
() => {
|
||||
resetLookupStates()
|
||||
lookupKinds.value.forEach((kind) => {
|
||||
@@ -226,6 +233,14 @@ function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
item,
|
||||
}))
|
||||
: []
|
||||
const exactItem = exactLookupItemForField(field)
|
||||
if (exactItem && !options.some((option) => option.value === exactItem.code)) {
|
||||
options.push({
|
||||
value: exactItem.code,
|
||||
label: lookupItemLabel(exactItem),
|
||||
item: exactItem,
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -287,6 +302,13 @@ function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] {
|
||||
}
|
||||
|
||||
const hints: LookupHint[] = []
|
||||
if (isExactLookupLoading(field)) {
|
||||
hints.push({
|
||||
key: `${kind}-exact-loading`,
|
||||
message: t('taskV4.lookup.loading'),
|
||||
tone: 'normal',
|
||||
})
|
||||
}
|
||||
const selectedItem = selectedLookupItem(field)
|
||||
if (kind === 'ACCOUNT' && selectedItem && (selectedItem.market_code || selectedItem.source_code)) {
|
||||
hints.push({
|
||||
@@ -376,11 +398,13 @@ function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
||||
|
||||
function selectedLookupItem(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const value = String(fieldValue(field) || '')
|
||||
const value = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
if (!kind || !value) {
|
||||
return null
|
||||
}
|
||||
return lookupStates[kind].result?.items.find((item) => item.code === value) ?? null
|
||||
return lookupStates[kind].result?.items.find((item) => item.code === value)
|
||||
?? exactLookupStates[exactLookupKey(kind, value)]?.item
|
||||
?? null
|
||||
}
|
||||
|
||||
function createLookupState(): LookupState {
|
||||
@@ -402,6 +426,9 @@ function resetLookupStates(): void {
|
||||
Object.keys(invalidLookupValues).forEach((key) => {
|
||||
delete invalidLookupValues[key]
|
||||
})
|
||||
Object.keys(exactLookupStates).forEach((key) => {
|
||||
delete exactLookupStates[key]
|
||||
})
|
||||
}
|
||||
|
||||
async function loadLookup(kind: ReservationV4LookupKind): Promise<void> {
|
||||
@@ -448,12 +475,12 @@ function clearInvalidLoadedCatalogValues(): void {
|
||||
const nextValues = { ...props.modelValue }
|
||||
let changed = false
|
||||
props.fields.forEach((field) => {
|
||||
if (!isSelectField(field) || !isReservationV4EditableField(field)) {
|
||||
if (!isSelectField(field) || !isEditable(field)) {
|
||||
return
|
||||
}
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const result = kind ? lookupStates[kind].result : null
|
||||
if (!kind || !result || !isFullyLoadedCatalog(result)) {
|
||||
if (!kind || !result) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -466,8 +493,18 @@ function clearInvalidLoadedCatalogValues(): void {
|
||||
delete invalidLookupValues[key]
|
||||
return
|
||||
}
|
||||
invalidLookupValues[key] = currentValue
|
||||
nextValues[key] = ''
|
||||
if (!isFullyLoadedCatalog(result)) {
|
||||
const exactState = exactLookupStates[exactLookupKey(kind, currentValue)]
|
||||
if (!exactState) {
|
||||
void loadExactLookup(kind, currentValue)
|
||||
return
|
||||
}
|
||||
if (exactState.loading || (exactState.searched && exactState.item)) {
|
||||
delete invalidLookupValues[key]
|
||||
return
|
||||
}
|
||||
}
|
||||
clearInvalidCatalogValue(field, currentValue, nextValues)
|
||||
changed = true
|
||||
})
|
||||
if (changed) {
|
||||
@@ -478,6 +515,68 @@ function clearInvalidLoadedCatalogValues(): void {
|
||||
function isFullyLoadedCatalog(result: ReservationV4CatalogLookupResult): boolean {
|
||||
return result.items.length >= result.page.total
|
||||
}
|
||||
|
||||
async function loadExactLookup(kind: ReservationV4LookupKind, code: string): Promise<void> {
|
||||
const key = exactLookupKey(kind, code)
|
||||
exactLookupStates[key] = {
|
||||
loading: true,
|
||||
searched: false,
|
||||
item: null,
|
||||
}
|
||||
try {
|
||||
const result = await lookupFetcher(kind)({
|
||||
...(props.hotelId ? { hotel_id: props.hotelId } : {}),
|
||||
keyword: code,
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
exactLookupStates[key] = {
|
||||
loading: false,
|
||||
searched: true,
|
||||
item: (result.items ?? []).find((item) => item.code === code) ?? null,
|
||||
}
|
||||
} catch {
|
||||
exactLookupStates[key] = {
|
||||
loading: false,
|
||||
searched: true,
|
||||
item: null,
|
||||
}
|
||||
} finally {
|
||||
clearInvalidLoadedCatalogValues()
|
||||
}
|
||||
}
|
||||
|
||||
function clearInvalidCatalogValue(
|
||||
field: ReservationV4TaskCardFieldResult,
|
||||
currentValue: string,
|
||||
nextValues: ReservationRecord,
|
||||
): void {
|
||||
const key = reservationV4FieldKey(field)
|
||||
invalidLookupValues[key] = currentValue
|
||||
nextValues[key] = ''
|
||||
}
|
||||
|
||||
function exactLookupItemForField(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const value = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
if (!kind || !value) {
|
||||
return null
|
||||
}
|
||||
return exactLookupStates[exactLookupKey(kind, value)]?.item ?? null
|
||||
}
|
||||
|
||||
function isExactLookupLoading(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const value = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
if (!kind || !value) {
|
||||
return false
|
||||
}
|
||||
return exactLookupStates[exactLookupKey(kind, value)]?.loading ?? false
|
||||
}
|
||||
|
||||
function exactLookupKey(kind: ReservationV4LookupKind, code: string): string {
|
||||
return `${props.hotelId ?? ''}:${kind}:${code}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type {
|
||||
ReservationV4CatalogLookupResult,
|
||||
ReservationV4OrderTaskDetailResult,
|
||||
ReservationV4SourceNotificationDetailResult,
|
||||
ReservationV4TaskCardResult,
|
||||
@@ -230,6 +231,7 @@ describe('reservation V4 pages', () => {
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('UNKNOWN_ACCOUNT')
|
||||
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.currentNotInCatalog.replace('{value}', 'UNKNOWN_ACCOUNT'))
|
||||
@@ -241,6 +243,90 @@ describe('reservation V4 pages', () => {
|
||||
expect(wrapper.text()).toContain(zhCN.taskV4.validationFailed)
|
||||
})
|
||||
|
||||
it('keeps readonly catalog field values even when active lookup does not contain them', async () => {
|
||||
const detail = createOrderTaskDetail({
|
||||
basicAccountValue: 'HISTORICAL_ACCOUNT',
|
||||
basicCardStatus: 'CONFIRMED',
|
||||
basicCardAvailability: {
|
||||
editable: false,
|
||||
read_only: true,
|
||||
confirmable: false,
|
||||
},
|
||||
})
|
||||
vi.mocked(service.fetchReservationV4AccountLookups).mockResolvedValue(createCatalogLookupResult('ACCOUNT', []))
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(service.fetchReservationV4AccountLookups).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('HISTORICAL_ACCOUNT')
|
||||
expect(wrapper.text()).not.toContain(
|
||||
zhCN.taskV4.lookup.currentNotInCatalog.replace('{value}', 'HISTORICAL_ACCOUNT'),
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps a current catalog value found by exact keyword lookup outside the first page', async () => {
|
||||
const detail = createOrderTaskDetail({
|
||||
basicAccountValue: 'ACC-101',
|
||||
basicAccountRequired: true,
|
||||
})
|
||||
vi.mocked(service.fetchReservationV4AccountLookups)
|
||||
.mockResolvedValueOnce(createCatalogLookupResult('ACCOUNT', [
|
||||
{
|
||||
code: 'ACC-001',
|
||||
display_name: 'Account 001',
|
||||
market_code: 'LEISURE',
|
||||
source_code: 'TRAVEL_AGENT',
|
||||
},
|
||||
], 101))
|
||||
.mockResolvedValueOnce(createCatalogLookupResult('ACCOUNT', [
|
||||
{
|
||||
code: 'ACC-101',
|
||||
display_name: 'Account 101',
|
||||
market_code: 'MICE',
|
||||
source_code: 'DIRECT',
|
||||
},
|
||||
], 1, 20))
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
await flushPromises()
|
||||
|
||||
expect(service.fetchReservationV4AccountLookups).toHaveBeenNthCalledWith(1, {
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
page_num: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
expect(service.fetchReservationV4AccountLookups).toHaveBeenNthCalledWith(2, {
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
keyword: 'ACC-101',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
expect(wrapper.text()).toContain('MICE')
|
||||
expect(wrapper.text()).toContain('DIRECT')
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('确认卡片'))?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-basic', {
|
||||
version: 7,
|
||||
confirmed_payload: {
|
||||
basic_information: {
|
||||
account_code: 'ACC-101',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('requires confirmed order id before resolving an unresolved V4 review card', async () => {
|
||||
const detail = createOrderTaskDetail({
|
||||
businessCardStatus: 'REVIEW_REQUIRED',
|
||||
@@ -437,6 +523,8 @@ function mockCatalogLookups() {
|
||||
}
|
||||
|
||||
function createOrderTaskDetail(options: {
|
||||
basicCardStatus?: string
|
||||
basicCardAvailability?: Partial<ReservationV4TaskCardResult['availability']>
|
||||
businessCardStatus?: string
|
||||
businessCardAvailability?: Partial<ReservationV4TaskCardResult['availability']>
|
||||
sourceDisplayPayload?: ReservationV4TaskCardResult['display_payload']
|
||||
@@ -457,8 +545,9 @@ function createOrderTaskDetail(options: {
|
||||
],
|
||||
},
|
||||
})
|
||||
const basicCard = createCard('card-basic', 'BASIC_INFORMATION', 'PENDING_CONFIRM', {
|
||||
const basicCard = createCard('card-basic', 'BASIC_INFORMATION', options.basicCardStatus ?? 'PENDING_CONFIRM', {
|
||||
version: 7,
|
||||
availability: createAvailability(options.basicCardAvailability),
|
||||
fields: [
|
||||
createField('/basic_information/account_code', {
|
||||
display_name: 'Account',
|
||||
@@ -592,6 +681,39 @@ function createSourceNotificationDetail(): ReservationV4SourceNotificationDetail
|
||||
}
|
||||
}
|
||||
|
||||
function createCatalogLookupResult(
|
||||
catalog_type: ReservationV4CatalogLookupResult['catalog_type'],
|
||||
items: Array<Partial<ReservationV4CatalogLookupResult['items'][number]> & { code: string }>,
|
||||
total = items.length,
|
||||
pageSize = 100,
|
||||
): ReservationV4CatalogLookupResult {
|
||||
return {
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
catalog_type,
|
||||
catalog_source: 'SYSTEM_MANAGED',
|
||||
catalog_version: 'catalog-v1',
|
||||
stale: false,
|
||||
items: items.map((item) => ({
|
||||
code: item.code,
|
||||
display_name: item.display_name ?? item.code,
|
||||
status: item.status ?? 'ACTIVE',
|
||||
catalog_source: item.catalog_source ?? 'SYSTEM_MANAGED',
|
||||
market_code: item.market_code ?? null,
|
||||
market_name: item.market_name ?? null,
|
||||
source_code: item.source_code ?? null,
|
||||
source_name: item.source_name ?? null,
|
||||
adult_capacity: item.adult_capacity ?? null,
|
||||
pricing_available: item.pricing_available ?? null,
|
||||
})),
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: pageSize,
|
||||
total,
|
||||
},
|
||||
warnings: [],
|
||||
}
|
||||
}
|
||||
|
||||
function createCard(
|
||||
card_id: string,
|
||||
card_type: string,
|
||||
|
||||
Reference in New Issue
Block a user