优化V4预订事项用户化展示
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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))
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -425,7 +425,8 @@ function handlePrimaryAction(): void {
|
||||
}
|
||||
|
||||
.task-card-section__status-actions {
|
||||
justify-content: flex-start;
|
||||
align-self: stretch;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'หัวข้อ',
|
||||
|
||||
@@ -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: '主题',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user