优化V4预订事项用户化展示

This commit is contained in:
andy
2026-07-21 21:33:51 +07:00
parent c521eb6dc0
commit fb980c6814
10 changed files with 163 additions and 55 deletions

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
</div>
<template v-else>
<div class="room-information__chips">
<span>{{ eventType || '-' }}</span>
<span>{{ bookingType || '-' }}</span>
<span>{{ eventTypeLabel }}</span>
<span>{{ bookingTypeLabel }}</span>
</div>
<section
@@ -257,6 +257,29 @@ const roomInformation = computed<ReservationV4RoomInformationDisplayModel | null
})
const eventType = computed(() => roomInformation.value?.event_type ?? props.card.event_type ?? '')
const bookingType = computed(() => roomInformation.value?.booking_type ?? '')
const eventTypeLabel = computed(() => {
const normalizedEventType = eventType.value.trim().toUpperCase()
if (normalizedEventType === 'NEW_BOOKING') {
return t('taskV4.roomInformation.eventType.NEW_BOOKING')
}
if (normalizedEventType === 'UPDATE_BOOKING') {
return t('taskV4.roomInformation.eventType.UPDATE_BOOKING')
}
if (normalizedEventType === 'CANCEL_BOOKING') {
return t('taskV4.roomInformation.eventType.CANCEL_BOOKING')
}
return t('taskV4.roomInformation.eventType.unknown')
})
const bookingTypeLabel = computed(() => {
const normalizedBookingType = bookingType.value.trim().toUpperCase()
if (normalizedBookingType === 'GROUP') {
return t('taskV4.bookingTypeGroup')
}
if (normalizedBookingType === 'FIT') {
return t('taskV4.bookingTypeFit')
}
return t('taskV4.bookingTypeUnknown')
})
const currentValues = computed(() => normalizeValues(roomInformation.value?.current_values))
const proposedValues = computed(() => normalizeValues(roomInformation.value?.proposed_values))
const finalValues = computed(() => normalizeValues(roomInformation.value?.final_values))

View File

@@ -369,20 +369,6 @@ function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] {
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`,

View File

@@ -425,7 +425,8 @@ function handlePrimaryAction(): void {
}
.task-card-section__status-actions {
justify-content: flex-start;
align-self: stretch;
justify-content: flex-end;
}
}
</style>

View File

@@ -25,13 +25,13 @@ export default {
},
nav: {
orders: 'Order list',
taskQueue: 'Task queue',
taskQueue: 'Reservation items',
manualInvoice: 'Manual invoice',
roomingList: 'Rooming list',
sourceMessages: 'Source messages',
systemStatus: 'System status',
orderDetail: 'Order detail',
taskDetail: 'Task detail',
orderDetail: 'Order overview',
taskDetail: 'Reservation item handling',
sourceMessageConversation: 'Email conversation',
sourceNotificationDetail: 'Source notification detail',
debugEml: 'Debug EML',
@@ -451,7 +451,7 @@ export default {
},
order: {
summary: 'Order summary',
taskQueue: 'Order task queue',
taskQueue: 'Reservation items for this order',
v4TaskTimeline: 'Processing records',
v4TaskTimelineEmpty: 'No processing records.',
v4TaskTimelineHint: 'Shows source-email processing records and item status summaries only. Complete actions in the reservation item page.',
@@ -609,7 +609,13 @@ export default {
reviewResolved: 'Review submitted and detail refreshed.',
validationFailed: 'Fix current-card field errors first.',
roomInformation: {
noDisplayModel: 'Room Information display model is not returned yet.',
noDisplayModel: 'Room and date information is temporarily unavailable. Refresh later or contact an administrator.',
eventType: {
NEW_BOOKING: 'New booking',
UPDATE_BOOKING: 'Booking update',
CANCEL_BOOKING: 'Booking cancellation',
unknown: 'Reservation item',
},
emptyValues: 'No fields to display.',
changeSummary: 'Change summary',
currentValues: 'Current values',
@@ -661,13 +667,13 @@ export default {
action: 'Fixed action',
},
lookup: {
loading: 'Loading catalog',
empty: 'No options in the current catalog. A no-match search does not mean the catalog is uninitialized.',
loading: 'Loading options',
empty: 'No options are available. Contact an administrator to maintain the choices.',
stale: 'This catalog is a stale snapshot. You can still select from it; backend validation remains final.',
warning: 'Catalog note: {warning}',
error: 'Catalog is temporarily unavailable. Refresh and retry.',
accountDerived: 'Market: {market} / Source: {source} will be shown from the catalog; backend confirmation remains final.',
currentNotInCatalog: 'Original value "{value}" is not in the current catalog. Select again.',
error: 'Options are temporarily unavailable. Refresh and retry.',
accountDerived: 'Selecting this account fills Market: {market} / Source: {source}.',
currentNotInCatalog: 'Original value "{value}" is not in the current options. Select again.',
},
sourceMessage: {
subject: 'Subject',

View File

@@ -25,13 +25,13 @@ export default {
},
nav: {
orders: 'รายการออเดอร์',
taskQueue: 'คิวงาน',
taskQueue: 'รายการจองที่ต้องจัดการ',
manualInvoice: 'ออกใบแจ้งหนี้เอง',
roomingList: 'สร้าง Rooming List',
sourceMessages: 'แหล่งที่มาข้อความ',
systemStatus: 'สถานะระบบ',
orderDetail: 'รายละเอียดออเดอร์',
taskDetail: 'รายละเอียดงาน',
orderDetail: 'ภาพรวมออเดอร์',
taskDetail: 'จัดการรายการจอง',
sourceMessageConversation: 'เธรดอีเมล',
sourceNotificationDetail: 'รายละเอียดแจ้งเตือนต้นทาง',
debugEml: 'Debug EML',
@@ -451,7 +451,7 @@ export default {
},
order: {
summary: 'สรุปออเดอร์',
taskQueue: 'คิวงานของออเดอร์นี้',
taskQueue: 'รายการจองของออเดอร์นี้',
v4TaskTimeline: 'ประวัติการดำเนินการ',
v4TaskTimelineEmpty: 'ยังไม่มีประวัติการดำเนินการ',
v4TaskTimelineHint: 'แสดงเฉพาะประวัติการประมวลผลอีเมลต้นทางและสรุปสถานะรายการ หากต้องดำเนินการให้เข้าไปที่หน้าจัดการรายการจอง',
@@ -609,7 +609,13 @@ export default {
reviewResolved: 'ส่งผลตรวจสอบแล้วและรีเฟรชรายละเอียดแล้ว',
validationFailed: 'โปรดแก้ไขข้อมูลในการ์ดปัจจุบันก่อน',
roomInformation: {
noDisplayModel: 'ยังไม่ได้รับโมเดลแสดงผล Room Information',
noDisplayModel: 'ข้อมูลห้องและวันที่ยังไม่พร้อมใช้งาน โปรดลองรีเฟรชภายหลังหรือติดต่อผู้ดูแลระบบ',
eventType: {
NEW_BOOKING: 'จองใหม่',
UPDATE_BOOKING: 'แก้ไขการจอง',
CANCEL_BOOKING: 'ยกเลิกการจอง',
unknown: 'รายการจอง',
},
emptyValues: 'ไม่มีฟิลด์ให้แสดง',
changeSummary: 'สรุปการเปลี่ยนแปลง',
currentValues: 'ค่าปัจจุบัน',
@@ -661,13 +667,13 @@ export default {
action: 'การดำเนินการคงที่',
},
lookup: {
loading: 'กำลังโหลดแค็ตตาล็อก',
empty: 'ไม่มีตัวเลือกในแค็ตตาล็อกปัจจุบัน หากค้นหาไม่พบไม่ได้หมายความว่าแค็ตตาล็อกยังไม่เริ่มต้น',
loading: 'กำลังโหลดตัวเลือก',
empty: 'ยังไม่มีตัวเลือก หากต้องเพิ่มโปรดติดต่อผู้ดูแล',
stale: 'แค็ตตาล็อกนี้เป็น snapshot เก่า ยังเลือกได้ แต่การตรวจสอบฝั่งหลังบ้านเป็นผลสุดท้าย',
warning: 'หมายเหตุแค็ตตาล็อก: {warning}',
error: 'แค็ตตาล็อกไม่พร้อมใช้งานชั่วคราว โปรดรีเฟรชแล้วลองใหม่',
accountDerived: 'จะแสดง Market: {market} / Source: {source} จากแค็ตตาล็อก โดยผลยืนยันจากหลังบ้านเป็นผลสุดท้าย',
currentNotInCatalog: 'ค่าเดิม "{value}" ไม่อยู่ในแค็ตตาล็อกปัจจุบัน โปรดเลือกใหม่',
error: 'ตัวเลือกไม่พร้อมใช้งานชั่วคราว โปรดรีเฟรชแล้วลองใหม่',
accountDerived: 'เมื่อเลือกแล้วจะแสดง Market: {market} / Source: {source}',
currentNotInCatalog: 'ค่าเดิม "{value}" ไม่อยู่ในตัวเลือกปัจจุบัน โปรดเลือกใหม่',
},
sourceMessage: {
subject: 'หัวข้อ',

View File

@@ -25,13 +25,13 @@ export default {
},
nav: {
orders: '订单列表',
taskQueue: '任务队列',
taskQueue: '预订事项',
manualInvoice: '手工开票',
roomingList: '房表生成',
sourceMessages: '消息来源',
systemStatus: '系统状态',
orderDetail: '订单详情',
taskDetail: '任务详情',
orderDetail: '订单总览',
taskDetail: '订单事项办理',
sourceMessageConversation: '邮件会话',
sourceNotificationDetail: '来源通知详情',
debugEml: 'Debug EML',
@@ -451,7 +451,7 @@ export default {
},
order: {
summary: '订单摘要',
taskQueue: '同订单任务队列',
taskQueue: '同订单预订事项',
v4TaskTimeline: '处理记录',
v4TaskTimelineEmpty: '暂无处理记录。',
v4TaskTimelineHint: '只展示来源邮件处理记录和事项状态摘要;办理动作请进入订单事项办理页完成。',
@@ -609,7 +609,13 @@ export default {
reviewResolved: '复核已提交,详情已刷新。',
validationFailed: '请先修正当前卡片字段。',
roomInformation: {
noDisplayModel: 'Room Information 展示模型暂未返回。',
noDisplayModel: '房型与日期信息暂不可用,请稍后刷新或联系管理员。',
eventType: {
NEW_BOOKING: '新预订',
UPDATE_BOOKING: '修改预订',
CANCEL_BOOKING: '取消预订',
unknown: '预订事项',
},
emptyValues: '暂无可展示字段。',
changeSummary: '本次变更摘要',
currentValues: '当前值',
@@ -661,13 +667,13 @@ export default {
action: '固定动作',
},
lookup: {
loading: '目录加载中',
empty: '当前目录没有可选项;如果是搜索无结果,不代表目录未初始化。',
loading: '选项加载中',
empty: '暂无可选项,如需新增请联系管理员维护资料。',
stale: '当前目录为过期快照,仍可选择,最终以后端校验为准。',
warning: '目录提示:{warning}',
error: '目录暂不可用,请刷新后重试。',
accountDerived: '选择后将参考目录展示 Market{market} / Source{source},最终以后端确认结果为准。',
currentNotInCatalog: '原值“{value}”不在当前目录中,请重新选择。',
error: '选项暂不可用,请刷新后重试。',
accountDerived: '选择后会带出市场{market} / 来源{source}。',
currentNotInCatalog: '原值“{value}”不在当前可选项中,请重新选择。',
},
sourceMessage: {
subject: '主题',

View File

@@ -14,11 +14,24 @@ describe('reservation i18n locales', () => {
it('includes Thai labels for the P0 reservation navigation', () => {
expect(thTH.nav.orders).toBe('รายการออเดอร์')
expect(thTH.nav.taskQueue).toBe('คิวงาน')
expect(thTH.nav.taskQueue).toBe('รายการจองที่ต้องจัดการ')
expect(thTH.nav.debugEml).toBe('Debug EML')
expect(thTH.taskList.viewConversation).toBe('ดูเธรดอีเมล')
})
it('uses user-facing reservation navigation labels instead of legacy task wording', () => {
expect(zhCN.nav.taskQueue).toBe('预订事项')
expect(zhCN.nav.taskDetail).toBe('订单事项办理')
expect(zhCN.nav.orderDetail).toBe('订单总览')
expect(enUS.nav.taskQueue).toBe('Reservation items')
expect(enUS.nav.taskDetail).toBe('Reservation item handling')
expect(enUS.nav.orderDetail).toBe('Order overview')
expect(thTH.nav.taskDetail).toBe('จัดการรายการจอง')
expect(zhCN.nav.taskQueue).not.toBe('任务队列')
expect(zhCN.nav.taskDetail).not.toBe('任务详情')
})
it('covers Debug EML backend error codes in all locales', () => {
const expectedErrorCodes = [
'REQUEST_FIELD_REQUIRED',

View File

@@ -199,6 +199,8 @@ describe('ReservationAppShell', () => {
expect(wrapper.find('a[href="/reservation/orders"]').exists()).toBe(true)
expect(wrapper.find('a[href="/reservation/tasks"]').exists()).toBe(true)
expect(wrapper.text()).toContain('预订事项')
expect(wrapper.text()).not.toContain('任务队列')
expect(wrapper.find('a[href="/debug/eml-superagent"]').exists()).toBe(false)
})

View File

@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import { createMemoryHistory, createRouter } from 'vue-router'
import taskCardSectionSource from '@/components/reservation/ReservationV4TaskCardSection.vue?raw'
import zhCN from '@/i18n/locales/zh-CN'
import { useAuthStore } from '@/stores/authStore'
import type {
@@ -811,6 +812,25 @@ describe('reservation V4 pages', () => {
})
})
it('shows a user-facing empty state when Room Information display data is unavailable', async () => {
const detail = createOrderTaskDetail({
businessDisplayPayload: {},
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
const roomCard = wrapper.find('[data-testid="room-information-card"]')
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.noDisplayModel)
expect(roomCard.text()).not.toContain('Room Information')
expect(roomCard.text()).not.toContain('display model')
expect(roomCard.text()).not.toContain('展示模型')
})
it('keeps backend errors for hidden Room Information fields visible as action messages', async () => {
const detail = createOrderTaskDetail({
businessEventType: 'NEW_BOOKING',
@@ -870,6 +890,8 @@ describe('reservation V4 pages', () => {
await flushPromises()
const roomCard = wrapper.find('[data-testid="room-information-card"]')
expect(roomCard.text()).toContain('新预订')
expect(roomCard.text()).not.toContain('NEW_BOOKING')
expect(roomCard.find('select[name="/room_information/final_values/group_booking_status"]').exists()).toBe(false)
expect(roomCard.text()).toContain('TEN-Tentative')
})
@@ -955,6 +977,8 @@ describe('reservation V4 pages', () => {
await flushPromises()
const roomCard = wrapper.find('[data-testid="room-information-card"]')
expect(roomCard.text()).toContain('修改预订')
expect(roomCard.text()).not.toContain('UPDATE_BOOKING')
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.changeSummary)
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.currentValues)
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.proposedValues)
@@ -1018,6 +1042,8 @@ describe('reservation V4 pages', () => {
await flushPromises()
const roomCard = wrapper.find('[data-testid="room-information-card"]')
expect(roomCard.text()).toContain('取消预订')
expect(roomCard.text()).not.toContain('CANCEL_BOOKING')
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.currentValues)
expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.finalValues)
expect(roomCard.text()).toContain('GRP-V4-RI-CANCEL-001')
@@ -1073,7 +1099,7 @@ describe('reservation V4 pages', () => {
})
})
it('shows non-blocking lookup empty, stale and warning states', async () => {
it('shows lookup empty state without catalog technical warnings', async () => {
const detail = createOrderTaskDetail()
vi.mocked(service.fetchReservationV4AccountLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
@@ -1098,8 +1124,8 @@ describe('reservation V4 pages', () => {
await flushPromises()
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.empty)
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.stale)
expect(wrapper.text()).toContain('PMS sync is stale')
expect(wrapper.text()).not.toContain(zhCN.taskV4.lookup.stale)
expect(wrapper.text()).not.toContain('PMS sync is stale')
expect(wrapper.text()).not.toContain('PMS_SYNC')
expect(wrapper.text()).not.toContain('stale-v1')
})
@@ -1226,6 +1252,45 @@ describe('reservation V4 pages', () => {
})
})
it('hides catalog technical warnings from ordinary V4 business cards', async () => {
const detail = createOrderTaskDetail()
vi.mocked(service.fetchReservationV4AccountLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
catalog_type: 'ACCOUNT',
catalog_source: 'PMS_SYNC',
catalog_version: 'seed-v1',
stale: true,
items: [],
page: {
page_num: 1,
page_size: 100,
total: 0,
},
warnings: ['固定种子初始化,后续接入真实 PMS。'],
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.empty)
expect(wrapper.text()).not.toContain('固定种子初始化')
expect(wrapper.text()).not.toContain('真实 PMS')
expect(wrapper.text()).not.toContain('PMS_SYNC')
expect(wrapper.text()).not.toContain('seed-v1')
expect(wrapper.text()).not.toContain(zhCN.taskV4.lookup.stale)
})
it('keeps the per-card primary action right aligned on narrow screens', () => {
expect(taskCardSectionSource).toContain('@media (max-width: 980px)')
expect(taskCardSectionSource).toContain('.task-card-section__status-actions')
expect(taskCardSectionSource).toContain('align-self: stretch')
expect(taskCardSectionSource).toContain('justify-content: flex-end')
})
it('requires confirmed order id before resolving an unresolved V4 review card', async () => {
const detail = createOrderTaskDetail({
businessCardStatus: 'REVIEW_REQUIRED',