接入V4任务卡目录查询
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
v-if="isEditable(field) && isSelectField(field)"
|
||||
class="v4-field__control"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:disabled="selectDisabled(field)"
|
||||
:value="fieldValue(field)"
|
||||
@change="updateField(field, $event)"
|
||||
>
|
||||
@@ -76,6 +77,12 @@
|
||||
v-if="field.control_hint"
|
||||
class="v4-field__hint"
|
||||
>{{ field.control_hint }}</small>
|
||||
<small
|
||||
v-for="hint in lookupHints(field)"
|
||||
:key="hint.key"
|
||||
class="v4-field__hint"
|
||||
:class="`v4-field__hint--${hint.tone}`"
|
||||
>{{ hint.message }}</small>
|
||||
<small
|
||||
v-for="error in visibleFieldErrors(field)"
|
||||
:key="error"
|
||||
@@ -86,15 +93,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ReservationRecord, ReservationV4TaskCardFieldResult } from '@/types/reservation'
|
||||
import {
|
||||
fetchReservationV4AccountLookups,
|
||||
fetchReservationV4RateCodeLookups,
|
||||
fetchReservationV4RoomTypeLookups,
|
||||
} from '@/services/reservationService'
|
||||
import type {
|
||||
ReservationRecord,
|
||||
ReservationV4CatalogLookupItem,
|
||||
ReservationV4CatalogLookupResult,
|
||||
ReservationV4TaskCardFieldResult,
|
||||
} from '@/types/reservation'
|
||||
import {
|
||||
isReservationV4EditableField,
|
||||
readV4FieldValue,
|
||||
reservationV4LookupKindForOptionsSource,
|
||||
reservationV4FieldKey,
|
||||
staticReservationV4Options,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
type ReservationV4LookupKind,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -103,6 +122,7 @@ const props = defineProps<{
|
||||
readOnly: boolean
|
||||
validationErrors?: Record<string, string>
|
||||
editableKeys?: string[]
|
||||
hotelId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -110,6 +130,65 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
type FieldOption = {
|
||||
value: string
|
||||
label: string
|
||||
item?: ReservationV4CatalogLookupItem
|
||||
}
|
||||
type LookupHintTone = 'normal' | 'warning' | 'error'
|
||||
type LookupHint = {
|
||||
key: string
|
||||
message: string
|
||||
tone: LookupHintTone
|
||||
}
|
||||
type LookupState = {
|
||||
loading: boolean
|
||||
error: string
|
||||
result: ReservationV4CatalogLookupResult | null
|
||||
}
|
||||
|
||||
const invalidLookupValues = reactive<Record<string, string>>({})
|
||||
const lookupStates = reactive<Record<ReservationV4LookupKind, LookupState>>({
|
||||
ACCOUNT: createLookupState(),
|
||||
ROOM_TYPE: createLookupState(),
|
||||
RATE_CODE: createLookupState(),
|
||||
})
|
||||
const lookupRequestVersions: Record<ReservationV4LookupKind, number> = {
|
||||
ACCOUNT: 0,
|
||||
ROOM_TYPE: 0,
|
||||
RATE_CODE: 0,
|
||||
}
|
||||
const lookupKinds = computed(() => {
|
||||
const kinds = props.fields
|
||||
.filter((field) => isSelectControl(field) && isReservationV4EditableField(field))
|
||||
.map((field) => reservationV4LookupKindForOptionsSource(field.options_source))
|
||||
.filter((kind): kind is ReservationV4LookupKind => Boolean(kind))
|
||||
return [...new Set(kinds)]
|
||||
})
|
||||
|
||||
watch(
|
||||
[lookupKinds, () => props.hotelId],
|
||||
() => {
|
||||
resetLookupStates()
|
||||
lookupKinds.value.forEach((kind) => {
|
||||
void loadLookup(kind)
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.modelValue,
|
||||
() => lookupStates.ACCOUNT.result,
|
||||
() => lookupStates.ROOM_TYPE.result,
|
||||
() => lookupStates.RATE_CODE.result,
|
||||
],
|
||||
() => {
|
||||
clearInvalidLoadedCatalogValues()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
function fieldKey(field: ReservationV4TaskCardFieldResult): string {
|
||||
return reservationV4FieldKey(field)
|
||||
@@ -123,7 +202,7 @@ function isEditable(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
}
|
||||
|
||||
function isSelectField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'SELECT' && fieldOptions(field).length > 0
|
||||
return isSelectControl(field) && Boolean(reservationV4LookupKindForOptionsSource(field.options_source))
|
||||
}
|
||||
|
||||
function isDateField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
@@ -138,8 +217,25 @@ function isTextAreaField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'TEXTAREA'
|
||||
}
|
||||
|
||||
function fieldOptions(field: ReservationV4TaskCardFieldResult): Array<{ value: string; label: string }> {
|
||||
return staticReservationV4Options(field.options_source)
|
||||
function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const options = kind
|
||||
? (lookupStates[kind].result?.items ?? []).map((item) => ({
|
||||
value: item.code,
|
||||
label: lookupItemLabel(item),
|
||||
item,
|
||||
}))
|
||||
: []
|
||||
return options
|
||||
}
|
||||
|
||||
function selectDisabled(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
if (!kind) {
|
||||
return false
|
||||
}
|
||||
const state = lookupStates[kind]
|
||||
return state.loading || Boolean(state.error)
|
||||
}
|
||||
|
||||
function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
||||
@@ -158,12 +254,92 @@ function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
||||
|
||||
function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
||||
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
delete invalidLookupValues[reservationV4FieldKey(field)]
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[reservationV4FieldKey(field)]: target.value,
|
||||
})
|
||||
}
|
||||
|
||||
function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
if (!kind) {
|
||||
return []
|
||||
}
|
||||
const state = lookupStates[kind]
|
||||
if (state.loading) {
|
||||
return [{
|
||||
key: `${kind}-loading`,
|
||||
message: t('taskV4.lookup.loading'),
|
||||
tone: 'normal',
|
||||
}]
|
||||
}
|
||||
if (state.error) {
|
||||
return [{
|
||||
key: `${kind}-error`,
|
||||
message: state.error,
|
||||
tone: 'error',
|
||||
}]
|
||||
}
|
||||
const result = state.result
|
||||
if (!result) {
|
||||
return []
|
||||
}
|
||||
|
||||
const hints: LookupHint[] = []
|
||||
const selectedItem = selectedLookupItem(field)
|
||||
if (kind === 'ACCOUNT' && selectedItem && (selectedItem.market_code || selectedItem.source_code)) {
|
||||
hints.push({
|
||||
key: `${kind}-derived`,
|
||||
message: t('taskV4.lookup.accountDerived', {
|
||||
market: selectedItem.market_code ?? '-',
|
||||
source: selectedItem.source_code ?? '-',
|
||||
}),
|
||||
tone: 'normal',
|
||||
})
|
||||
}
|
||||
if (result.stale) {
|
||||
hints.push({
|
||||
key: `${kind}-stale`,
|
||||
message: t('taskV4.lookup.stale'),
|
||||
tone: 'warning',
|
||||
})
|
||||
}
|
||||
result.warnings.forEach((warning, index) => {
|
||||
hints.push({
|
||||
key: `${kind}-warning-${index}`,
|
||||
message: t('taskV4.lookup.warning', { warning }),
|
||||
tone: 'warning',
|
||||
})
|
||||
})
|
||||
if (!result.items.length) {
|
||||
hints.push({
|
||||
key: `${kind}-empty`,
|
||||
message: t('taskV4.lookup.empty'),
|
||||
tone: 'warning',
|
||||
})
|
||||
}
|
||||
if (result.catalog_source || result.catalog_version) {
|
||||
hints.push({
|
||||
key: `${kind}-meta`,
|
||||
message: t('taskV4.lookup.catalogMeta', {
|
||||
source: result.catalog_source ?? '-',
|
||||
version: result.catalog_version ?? '-',
|
||||
}),
|
||||
tone: 'normal',
|
||||
})
|
||||
}
|
||||
const invalidValue = invalidLookupValues[reservationV4FieldKey(field)]
|
||||
if (invalidValue) {
|
||||
hints.push({
|
||||
key: `${kind}-invalid-current`,
|
||||
message: t('taskV4.lookup.currentNotInCatalog', { value: invalidValue }),
|
||||
tone: 'error',
|
||||
})
|
||||
}
|
||||
return hints
|
||||
}
|
||||
|
||||
function visibleFieldErrors(field: ReservationV4TaskCardFieldResult): string[] {
|
||||
return [
|
||||
...(field.validation_errors ?? []),
|
||||
@@ -186,6 +362,122 @@ function stringifyV4Value(value: unknown): string {
|
||||
function normalizeControlType(value: string | null): string {
|
||||
return value?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
|
||||
}
|
||||
|
||||
function isSelectControl(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return ['SELECT', 'LOOKUP'].includes(normalizeControlType(field.control_type))
|
||||
}
|
||||
|
||||
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
||||
if (item.display_name && item.display_name !== item.code) {
|
||||
return `${item.code} - ${item.display_name}`
|
||||
}
|
||||
return item.code
|
||||
}
|
||||
|
||||
function selectedLookupItem(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const value = String(fieldValue(field) || '')
|
||||
if (!kind || !value) {
|
||||
return null
|
||||
}
|
||||
return lookupStates[kind].result?.items.find((item) => item.code === value) ?? null
|
||||
}
|
||||
|
||||
function createLookupState(): LookupState {
|
||||
return {
|
||||
loading: false,
|
||||
error: '',
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
function resetLookupStates(): void {
|
||||
const allKinds: ReservationV4LookupKind[] = ['ACCOUNT', 'ROOM_TYPE', 'RATE_CODE']
|
||||
allKinds.forEach((kind) => {
|
||||
lookupRequestVersions[kind] += 1
|
||||
lookupStates[kind].loading = false
|
||||
lookupStates[kind].error = ''
|
||||
lookupStates[kind].result = null
|
||||
})
|
||||
Object.keys(invalidLookupValues).forEach((key) => {
|
||||
delete invalidLookupValues[key]
|
||||
})
|
||||
}
|
||||
|
||||
async function loadLookup(kind: ReservationV4LookupKind): Promise<void> {
|
||||
const state = lookupStates[kind]
|
||||
const requestVersion = ++lookupRequestVersions[kind]
|
||||
state.loading = true
|
||||
state.error = ''
|
||||
try {
|
||||
const result = await lookupFetcher(kind)({
|
||||
...(props.hotelId ? { hotel_id: props.hotelId } : {}),
|
||||
page_num: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
if (requestVersion === lookupRequestVersions[kind]) {
|
||||
state.result = {
|
||||
...result,
|
||||
items: result.items ?? [],
|
||||
warnings: result.warnings ?? [],
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (requestVersion === lookupRequestVersions[kind]) {
|
||||
state.error = t('taskV4.lookup.error')
|
||||
}
|
||||
} finally {
|
||||
if (requestVersion === lookupRequestVersions[kind]) {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lookupFetcher(kind: ReservationV4LookupKind) {
|
||||
switch (kind) {
|
||||
case 'ACCOUNT':
|
||||
return fetchReservationV4AccountLookups
|
||||
case 'ROOM_TYPE':
|
||||
return fetchReservationV4RoomTypeLookups
|
||||
case 'RATE_CODE':
|
||||
return fetchReservationV4RateCodeLookups
|
||||
}
|
||||
}
|
||||
|
||||
function clearInvalidLoadedCatalogValues(): void {
|
||||
const nextValues = { ...props.modelValue }
|
||||
let changed = false
|
||||
props.fields.forEach((field) => {
|
||||
if (!isSelectField(field) || !isReservationV4EditableField(field)) {
|
||||
return
|
||||
}
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
const result = kind ? lookupStates[kind].result : null
|
||||
if (!kind || !result || !isFullyLoadedCatalog(result)) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = reservationV4FieldKey(field)
|
||||
const currentValue = String(readV4FieldValue(field, nextValues) || '')
|
||||
if (!currentValue) {
|
||||
return
|
||||
}
|
||||
if (result.items.some((item) => item.code === currentValue)) {
|
||||
delete invalidLookupValues[key]
|
||||
return
|
||||
}
|
||||
invalidLookupValues[key] = currentValue
|
||||
nextValues[key] = ''
|
||||
changed = true
|
||||
})
|
||||
if (changed) {
|
||||
emit('update:modelValue', nextValues)
|
||||
}
|
||||
}
|
||||
|
||||
function isFullyLoadedCatalog(result: ReservationV4CatalogLookupResult): boolean {
|
||||
return result.items.length >= result.page.total
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -255,6 +547,16 @@ function normalizeControlType(value: string | null): string {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.v4-field__hint--warning {
|
||||
color: var(--th-color-warning);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.v4-field__hint--error {
|
||||
color: var(--th-color-danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.v4-field__error {
|
||||
color: var(--th-color-danger);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user