实现V4 Rooming List轻量事项卡
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="rooming-list-card"
|
||||||
|
data-testid="rooming-list-card"
|
||||||
|
>
|
||||||
|
<div class="rooming-list-card__body">
|
||||||
|
<div>
|
||||||
|
<span>{{ t('taskV4.roomingList.targetOrder') }}</span>
|
||||||
|
<strong>{{ orderLabel }}</strong>
|
||||||
|
</div>
|
||||||
|
<RouterLink
|
||||||
|
v-if="boundOrderId"
|
||||||
|
class="rooming-list-card__link"
|
||||||
|
:to="`/reservation/orders/${boundOrderId}`"
|
||||||
|
>
|
||||||
|
{{ t('task.viewOrder') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ t('taskV4.roomingList.description') }}</p>
|
||||||
|
<small>{{ t('taskV4.roomingList.confirmHint') }}</small>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
orderReference?: string | null
|
||||||
|
targetLocator?: string | null
|
||||||
|
boundOrderId?: string | null
|
||||||
|
boundOrderDisplayName?: string | null
|
||||||
|
}>(), {
|
||||||
|
orderReference: null,
|
||||||
|
targetLocator: null,
|
||||||
|
boundOrderId: null,
|
||||||
|
boundOrderDisplayName: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const orderLabel = computed(() =>
|
||||||
|
readableValue(props.boundOrderDisplayName) ||
|
||||||
|
readableValue(props.orderReference) ||
|
||||||
|
readableValue(props.targetLocator) ||
|
||||||
|
t('taskV4.roomingList.unboundOrder'),
|
||||||
|
)
|
||||||
|
|
||||||
|
function readableValue(value?: string | null): string {
|
||||||
|
const trimmedValue = value?.trim() ?? ''
|
||||||
|
return trimmedValue === '-' ? '' : trimmedValue
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rooming-list-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0 20px 18px;
|
||||||
|
border: 1px solid var(--th-color-slate-200);
|
||||||
|
border-radius: var(--th-radius-sm);
|
||||||
|
background: var(--th-color-slate-50);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card__body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card__body div {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card__body span {
|
||||||
|
color: var(--th-color-slate-500);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card__body strong {
|
||||||
|
color: var(--th-color-slate-900);
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card p,
|
||||||
|
.rooming-list-card small {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--th-color-slate-600);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card small {
|
||||||
|
color: var(--th-color-slate-500);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card__link {
|
||||||
|
color: var(--th-color-blue-600);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooming-list-card__link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.rooming-list-card__body {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -46,8 +46,15 @@
|
|||||||
@update:model-value="emit('update:modelValue', $event)"
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<ReservationV4RoomingListCard
|
||||||
|
v-if="isRoomingListCard"
|
||||||
|
:order-reference="orderReference"
|
||||||
|
:target-locator="targetLocator"
|
||||||
|
:bound-order-id="boundOrderId"
|
||||||
|
:bound-order-display-name="boundOrderDisplayName"
|
||||||
|
/>
|
||||||
<ReservationV4PaymentAttachmentPreview
|
<ReservationV4PaymentAttachmentPreview
|
||||||
v-if="isPaymentCard"
|
v-else-if="isPaymentCard"
|
||||||
:card="card"
|
:card="card"
|
||||||
:source-message-id="sourceMessageId"
|
:source-message-id="sourceMessageId"
|
||||||
/>
|
/>
|
||||||
@@ -65,7 +72,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<details
|
<details
|
||||||
v-if="!isRoomInformationCard && safePayloadRows.length"
|
v-if="!isRoomInformationCard && !isRoomingListCard && safePayloadRows.length"
|
||||||
class="safe-payload"
|
class="safe-payload"
|
||||||
>
|
>
|
||||||
<summary>
|
<summary>
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
|
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
|
||||||
</button>
|
</button>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="boundOrderId"
|
v-if="boundOrderId && !isRoomingListCard"
|
||||||
class="card-order-link"
|
class="card-order-link"
|
||||||
:to="`/reservation/orders/${boundOrderId}`"
|
:to="`/reservation/orders/${boundOrderId}`"
|
||||||
>
|
>
|
||||||
@@ -155,7 +162,7 @@
|
|||||||
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
|
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
|
||||||
</button>
|
</button>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="boundOrderId"
|
v-if="boundOrderId && !isRoomingListCard"
|
||||||
class="card-order-link"
|
class="card-order-link"
|
||||||
:to="`/reservation/orders/${boundOrderId}`"
|
:to="`/reservation/orders/${boundOrderId}`"
|
||||||
>
|
>
|
||||||
@@ -173,6 +180,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||||
import ReservationV4PaymentAttachmentPreview from '@/components/reservation/ReservationV4PaymentAttachmentPreview.vue'
|
import ReservationV4PaymentAttachmentPreview from '@/components/reservation/ReservationV4PaymentAttachmentPreview.vue'
|
||||||
import ReservationV4RoomInformationCard from '@/components/reservation/ReservationV4RoomInformationCard.vue'
|
import ReservationV4RoomInformationCard from '@/components/reservation/ReservationV4RoomInformationCard.vue'
|
||||||
|
import ReservationV4RoomingListCard from '@/components/reservation/ReservationV4RoomingListCard.vue'
|
||||||
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
||||||
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
|
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
|
||||||
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
|
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
|
||||||
@@ -196,6 +204,8 @@ const props = withDefaults(defineProps<{
|
|||||||
hotelId?: string
|
hotelId?: string
|
||||||
orderTaskId?: string | null
|
orderTaskId?: string | null
|
||||||
sourceMessageId?: string | null
|
sourceMessageId?: string | null
|
||||||
|
orderReference?: string | null
|
||||||
|
targetLocator?: string | null
|
||||||
actionErrors?: string[]
|
actionErrors?: string[]
|
||||||
successMessage?: string
|
successMessage?: string
|
||||||
reviewForm?: ReviewForm
|
reviewForm?: ReviewForm
|
||||||
@@ -203,11 +213,14 @@ const props = withDefaults(defineProps<{
|
|||||||
canConfirm: boolean
|
canConfirm: boolean
|
||||||
canReview: boolean
|
canReview: boolean
|
||||||
boundOrderId?: string | null
|
boundOrderId?: string | null
|
||||||
|
boundOrderDisplayName?: string | null
|
||||||
}>(), {
|
}>(), {
|
||||||
validationErrors: () => ({}),
|
validationErrors: () => ({}),
|
||||||
hotelId: undefined,
|
hotelId: undefined,
|
||||||
orderTaskId: null,
|
orderTaskId: null,
|
||||||
sourceMessageId: null,
|
sourceMessageId: null,
|
||||||
|
orderReference: null,
|
||||||
|
targetLocator: null,
|
||||||
actionErrors: () => [],
|
actionErrors: () => [],
|
||||||
successMessage: '',
|
successMessage: '',
|
||||||
reviewForm: () => ({
|
reviewForm: () => ({
|
||||||
@@ -215,6 +228,7 @@ const props = withDefaults(defineProps<{
|
|||||||
reason: '',
|
reason: '',
|
||||||
}),
|
}),
|
||||||
boundOrderId: null,
|
boundOrderId: null,
|
||||||
|
boundOrderDisplayName: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -227,6 +241,7 @@ const emit = defineEmits<{
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const isRoomInformationCard = computed(() => props.card.card_type === 'ROOM_INFORMATION')
|
const isRoomInformationCard = computed(() => props.card.card_type === 'ROOM_INFORMATION')
|
||||||
const isPaymentCard = computed(() => props.card.card_type === 'PAYMENT')
|
const isPaymentCard = computed(() => props.card.card_type === 'PAYMENT')
|
||||||
|
const isRoomingListCard = computed(() => props.card.card_type === 'ROOMING_LIST')
|
||||||
const visibleFields = computed(() =>
|
const visibleFields = computed(() =>
|
||||||
isPaymentCard.value
|
isPaymentCard.value
|
||||||
? props.card.fields.filter((field) => !isPaymentAttachmentIdField(field))
|
? props.card.fields.filter((field) => !isPaymentAttachmentIdField(field))
|
||||||
|
|||||||
@@ -623,6 +623,13 @@ export default {
|
|||||||
unavailableReason: 'Unavailable reason: {reason}',
|
unavailableReason: 'Unavailable reason: {reason}',
|
||||||
fileMeta: '{type} · {size}',
|
fileMeta: '{type} · {size}',
|
||||||
},
|
},
|
||||||
|
roomingList: {
|
||||||
|
title: 'Rooming List action',
|
||||||
|
targetOrder: 'Target order',
|
||||||
|
unboundOrder: 'No visible order is bound yet',
|
||||||
|
description: 'The current source message contains a Rooming List item that needs manual handling.',
|
||||||
|
confirmHint: 'This first version only confirms that the item has been handled manually. Guest details and attachment content are not displayed or processed here.',
|
||||||
|
},
|
||||||
lookup: {
|
lookup: {
|
||||||
loading: 'Loading catalog',
|
loading: 'Loading catalog',
|
||||||
empty: 'No options in the current catalog. A no-match search does not mean the catalog is uninitialized.',
|
empty: 'No options in the current catalog. A no-match search does not mean the catalog is uninitialized.',
|
||||||
|
|||||||
@@ -623,6 +623,13 @@ export default {
|
|||||||
unavailableReason: 'เหตุผลที่ใช้ไม่ได้: {reason}',
|
unavailableReason: 'เหตุผลที่ใช้ไม่ได้: {reason}',
|
||||||
fileMeta: '{type} · {size}',
|
fileMeta: '{type} · {size}',
|
||||||
},
|
},
|
||||||
|
roomingList: {
|
||||||
|
title: 'งาน Rooming List',
|
||||||
|
targetOrder: 'ออเดอร์เป้าหมาย',
|
||||||
|
unboundOrder: 'ยังไม่มีออเดอร์ที่มองเห็นได้ผูกอยู่',
|
||||||
|
description: 'ข้อความต้นทางปัจจุบันมีรายการ Rooming List ที่ต้องให้เจ้าหน้าที่จัดการ',
|
||||||
|
confirmHint: 'เวอร์ชันแรกนี้ใช้ยืนยันว่ารายการได้รับการจัดการด้วยคนแล้วเท่านั้น ไม่แสดงหรือประมวลผลรายชื่อแขกและเนื้อหาไฟล์แนบในหน้านี้',
|
||||||
|
},
|
||||||
lookup: {
|
lookup: {
|
||||||
loading: 'กำลังโหลดแค็ตตาล็อก',
|
loading: 'กำลังโหลดแค็ตตาล็อก',
|
||||||
empty: 'ไม่มีตัวเลือกในแค็ตตาล็อกปัจจุบัน หากค้นหาไม่พบไม่ได้หมายความว่าแค็ตตาล็อกยังไม่เริ่มต้น',
|
empty: 'ไม่มีตัวเลือกในแค็ตตาล็อกปัจจุบัน หากค้นหาไม่พบไม่ได้หมายความว่าแค็ตตาล็อกยังไม่เริ่มต้น',
|
||||||
|
|||||||
@@ -623,6 +623,13 @@ export default {
|
|||||||
unavailableReason: '不可用原因:{reason}',
|
unavailableReason: '不可用原因:{reason}',
|
||||||
fileMeta: '{type} · {size}',
|
fileMeta: '{type} · {size}',
|
||||||
},
|
},
|
||||||
|
roomingList: {
|
||||||
|
title: 'Rooming List / 房表事项',
|
||||||
|
targetOrder: '目标订单',
|
||||||
|
unboundOrder: '暂未绑定可见订单',
|
||||||
|
description: '当前来源消息包含需要人工处理的 Rooming List 事项,请在酒店内部流程处理后确认该卡片。',
|
||||||
|
confirmHint: '第一版仅确认该事项已由人工处理,不展示或处理名单明细和附件内容。',
|
||||||
|
},
|
||||||
lookup: {
|
lookup: {
|
||||||
loading: '目录加载中',
|
loading: '目录加载中',
|
||||||
empty: '当前目录没有可选项;如果是搜索无结果,不代表目录未初始化。',
|
empty: '当前目录没有可选项;如果是搜索无结果,不代表目录未初始化。',
|
||||||
|
|||||||
@@ -386,6 +386,134 @@ describe('reservation V4 pages', () => {
|
|||||||
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders Rooming List cards as lightweight action items without rows, attachments or raw payloads', async () => {
|
||||||
|
const detail = createOrderTaskDetail()
|
||||||
|
useRoomingListBusinessCard(detail)
|
||||||
|
useBoundOrder(detail)
|
||||||
|
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||||
|
|
||||||
|
const wrapper = await mountWithPlugins(
|
||||||
|
ReservationV4OrderTaskDetailView,
|
||||||
|
'/reservation/order-tasks/9001',
|
||||||
|
)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const roomingListSection = wrapper
|
||||||
|
.findAll('.task-card-section')
|
||||||
|
.find((section) => section.find('[data-testid="rooming-list-card"]').exists())
|
||||||
|
expect(roomingListSection).toBeTruthy()
|
||||||
|
const roomingListCard = roomingListSection!.find('[data-testid="rooming-list-card"]')
|
||||||
|
expect(roomingListCard.exists()).toBe(true)
|
||||||
|
expect(roomingListSection!.text()).toContain(zhCN.taskV4.roomingList.title)
|
||||||
|
expect(roomingListCard.text()).toContain(zhCN.taskV4.roomingList.description)
|
||||||
|
expect(roomingListCard.text()).toContain('GRP-001')
|
||||||
|
expect(roomingListSection!.findAll('a').filter((link) => link.text() === zhCN.task.viewOrder)).toHaveLength(1)
|
||||||
|
expect(wrapper.text()).toContain(zhCN.taskV4.sourceMessageCard)
|
||||||
|
expect(wrapper.text().indexOf(zhCN.taskV4.sourceMessageCard)).toBeGreaterThan(
|
||||||
|
wrapper.text().indexOf(zhCN.taskV4.roomingList.title),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(roomingListSection!.find('details.safe-payload').exists()).toBe(false)
|
||||||
|
expect(roomingListSection!.text()).not.toContain('ROW_SHOULD_NOT_RENDER')
|
||||||
|
expect(roomingListSection!.text()).not.toContain('attachment_ids')
|
||||||
|
expect(roomingListSection!.text()).not.toContain('https://oss.example/private/rooming-list.xlsx')
|
||||||
|
expect(roomingListSection!.text()).not.toContain('AI_PAYLOAD_SHOULD_NOT_RENDER')
|
||||||
|
expect(roomingListSection!.text()).not.toContain('PMS_IMPORT_SHOULD_NOT_RENDER')
|
||||||
|
expect(roomingListSection!.text()).not.toContain('OHIP_SHOULD_NOT_RENDER')
|
||||||
|
expect(roomingListSection!.text()).not.toContain('Excel')
|
||||||
|
expect(wrapper.find('[data-testid="payment-attachment-preview"]').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows an explanatory unbound order label for Rooming List cards without target order clues', async () => {
|
||||||
|
const detail = createOrderTaskDetail()
|
||||||
|
useRoomingListBusinessCard(detail)
|
||||||
|
detail.order_task.display_order_key = null
|
||||||
|
detail.order_task.order_ref = null
|
||||||
|
detail.order_task.target_locator_type = null
|
||||||
|
detail.order_task.target_locator_value = null
|
||||||
|
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||||
|
|
||||||
|
const wrapper = await mountWithPlugins(
|
||||||
|
ReservationV4OrderTaskDetailView,
|
||||||
|
'/reservation/order-tasks/9001',
|
||||||
|
)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const roomingListCard = wrapper.find('[data-testid="rooming-list-card"]')
|
||||||
|
expect(roomingListCard.text()).toContain(zhCN.taskV4.roomingList.unboundOrder)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confirms Rooming List cards with version only and applies the refreshed backend detail', async () => {
|
||||||
|
const detail = createOrderTaskDetail()
|
||||||
|
useRoomingListBusinessCard(detail)
|
||||||
|
const confirmedDetail = createOrderTaskDetail()
|
||||||
|
useRoomingListBusinessCard(confirmedDetail, 'CONFIRMED')
|
||||||
|
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||||
|
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue(confirmedDetail)
|
||||||
|
|
||||||
|
const wrapper = await mountWithPlugins(
|
||||||
|
ReservationV4OrderTaskDetailView,
|
||||||
|
'/reservation/order-tasks/9001',
|
||||||
|
)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const roomingListSection = wrapper
|
||||||
|
.findAll('.task-card-section')
|
||||||
|
.find((section) => section.find('[data-testid="rooming-list-card"]').exists())
|
||||||
|
expect(roomingListSection).toBeTruthy()
|
||||||
|
await roomingListSection!.find('button.primary-button').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-rooming-list', {
|
||||||
|
version: 12,
|
||||||
|
})
|
||||||
|
const submittedRequest = vi.mocked(service.confirmReservationV4OrderTaskCard).mock.calls[0]?.[2]
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('confirmed_payload')
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('rows')
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('attachment_ids')
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
||||||
|
expect(wrapper.text()).toContain(zhCN.status.CONFIRMED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves Rooming List review cards without submitting row or attachment overrides', async () => {
|
||||||
|
const detail = createOrderTaskDetail()
|
||||||
|
useRoomingListBusinessCard(detail, 'REVIEW_REQUIRED')
|
||||||
|
detail.order_task.order_id = 'order-2001'
|
||||||
|
detail.order_task.target_resolution_status = 'RESOLVED'
|
||||||
|
const resolvedDetail = createOrderTaskDetail()
|
||||||
|
useRoomingListBusinessCard(resolvedDetail, 'PENDING_CONFIRM')
|
||||||
|
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||||
|
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockResolvedValue(resolvedDetail)
|
||||||
|
|
||||||
|
const wrapper = await mountWithPlugins(
|
||||||
|
ReservationV4OrderTaskDetailView,
|
||||||
|
'/reservation/order-tasks/9001',
|
||||||
|
)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const roomingListSection = wrapper
|
||||||
|
.findAll('.task-card-section')
|
||||||
|
.find((section) => section.find('[data-testid="rooming-list-card"]').exists())
|
||||||
|
expect(roomingListSection).toBeTruthy()
|
||||||
|
expect(roomingListSection!.find('.review-box').exists()).toBe(true)
|
||||||
|
expect(roomingListSection!.find('.task-card-section__fields').exists()).toBe(false)
|
||||||
|
await roomingListSection!.find('textarea').setValue('handled rooming list')
|
||||||
|
await roomingListSection!.find('button.primary-button').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
|
||||||
|
expect(service.resolveReservationV4OrderTaskCardReview).toHaveBeenCalledWith('9001', 'card-rooming-list', {
|
||||||
|
version: 12,
|
||||||
|
confirmed_order_id: 'order-2001',
|
||||||
|
reason: 'handled rooming list',
|
||||||
|
field_overrides: [],
|
||||||
|
})
|
||||||
|
const submittedRequest = vi.mocked(service.resolveReservationV4OrderTaskCardReview).mock.calls[0]?.[2]
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('rows')
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('attachment_ids')
|
||||||
|
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
||||||
|
})
|
||||||
|
|
||||||
it('renders New Booking Room Information as a business form and confirms stable final values', async () => {
|
it('renders New Booking Room Information as a business form and confirms stable final values', async () => {
|
||||||
const detail = createOrderTaskDetail({
|
const detail = createOrderTaskDetail({
|
||||||
businessEventType: 'NEW_BOOKING',
|
businessEventType: 'NEW_BOOKING',
|
||||||
@@ -1360,6 +1488,75 @@ function usePaymentBusinessCard(detail: ReservationV4OrderTaskDetailResult): voi
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useRoomingListBusinessCard(
|
||||||
|
detail: ReservationV4OrderTaskDetailResult,
|
||||||
|
cardStatus = 'PENDING_CONFIRM',
|
||||||
|
): void {
|
||||||
|
const isPendingConfirm = cardStatus === 'PENDING_CONFIRM'
|
||||||
|
const isReviewRequired = cardStatus === 'REVIEW_REQUIRED'
|
||||||
|
detail.business_cards = [
|
||||||
|
createCard('card-rooming-list', 'ROOMING_LIST', cardStatus, {
|
||||||
|
version: 12,
|
||||||
|
availability: createAvailability({
|
||||||
|
confirmable: isPendingConfirm,
|
||||||
|
reviewable: isReviewRequired,
|
||||||
|
editable: false,
|
||||||
|
read_only: !isPendingConfirm,
|
||||||
|
}),
|
||||||
|
display_payload: {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
guest_name: 'ROW_SHOULD_NOT_RENDER',
|
||||||
|
room_no: '1001',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attachment_ids: ['rooming-list-att-1'],
|
||||||
|
attachment_url: 'https://oss.example/private/rooming-list.xlsx',
|
||||||
|
ai_payload_json: 'AI_PAYLOAD_SHOULD_NOT_RENDER',
|
||||||
|
pms_import_plan: 'PMS_IMPORT_SHOULD_NOT_RENDER',
|
||||||
|
ohip_payload: 'OHIP_SHOULD_NOT_RENDER',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
createField('/rooming_list/rows/0/guest_name', {
|
||||||
|
display_name: 'Rooming list row',
|
||||||
|
value: 'ROW_SHOULD_NOT_RENDER',
|
||||||
|
editable: true,
|
||||||
|
edit_scope: 'CONFIRM',
|
||||||
|
write_target: 'CONFIRMED_PAYLOAD_JSON',
|
||||||
|
}),
|
||||||
|
createField('/rooming_list/attachment_ids', {
|
||||||
|
display_name: 'attachment_ids',
|
||||||
|
value: ['rooming-list-att-1'],
|
||||||
|
editable: true,
|
||||||
|
edit_scope: 'CONFIRM',
|
||||||
|
write_target: 'CONFIRMED_PAYLOAD_JSON',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
detail.card_counts = {
|
||||||
|
...detail.card_counts,
|
||||||
|
pending_confirm_count: cardStatus === 'PENDING_CONFIRM' ? 2 : 1,
|
||||||
|
confirmed_count: cardStatus === 'CONFIRMED' ? 1 : 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBoundOrder(detail: ReservationV4OrderTaskDetailResult): void {
|
||||||
|
detail.bound_order = {
|
||||||
|
order_id: 'order-2001',
|
||||||
|
hotel_id: 'HOTEL-TEST',
|
||||||
|
order_status: 'ACTIVE',
|
||||||
|
temporary_order_no: null,
|
||||||
|
confirmation_number: null,
|
||||||
|
group_code: 'GRP-001',
|
||||||
|
block_code: null,
|
||||||
|
allotment_code: null,
|
||||||
|
display_name: 'GRP-001',
|
||||||
|
created_at: '2026-07-08T03:00:00Z',
|
||||||
|
updated_at: '2026-07-08T03:00:00Z',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createPaymentConversationMedia(): SourceMessageOriginalMedia[] {
|
function createPaymentConversationMedia(): SourceMessageOriginalMedia[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,6 +89,8 @@
|
|||||||
:hotel-id="detail.order_task.hotel_id"
|
:hotel-id="detail.order_task.hotel_id"
|
||||||
:order-task-id="detail.order_task.order_task_id"
|
:order-task-id="detail.order_task.order_task_id"
|
||||||
:source-message-id="detail.order_task.source_message_id"
|
:source-message-id="detail.order_task.source_message_id"
|
||||||
|
:order-reference="displayOrderReference"
|
||||||
|
:target-locator="targetLocator"
|
||||||
:action-errors="cardActionErrors[detail.basic_information_card.card_id] ?? []"
|
:action-errors="cardActionErrors[detail.basic_information_card.card_id] ?? []"
|
||||||
:success-message="cardSuccessMessages[detail.basic_information_card.card_id] ?? ''"
|
:success-message="cardSuccessMessages[detail.basic_information_card.card_id] ?? ''"
|
||||||
:review-form="reviewForms[detail.basic_information_card.card_id]"
|
:review-form="reviewForms[detail.basic_information_card.card_id]"
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
:can-confirm="canConfirmCard(detail.basic_information_card)"
|
:can-confirm="canConfirmCard(detail.basic_information_card)"
|
||||||
:can-review="canReviewCard(detail.basic_information_card)"
|
:can-review="canReviewCard(detail.basic_information_card)"
|
||||||
:bound-order-id="detail.bound_order?.order_id"
|
:bound-order-id="detail.bound_order?.order_id"
|
||||||
|
:bound-order-display-name="detail.bound_order?.display_name"
|
||||||
@update:model-value="setCardValues(detail.basic_information_card, $event)"
|
@update:model-value="setCardValues(detail.basic_information_card, $event)"
|
||||||
@update:review-form="setReviewForm(detail.basic_information_card.card_id, $event)"
|
@update:review-form="setReviewForm(detail.basic_information_card.card_id, $event)"
|
||||||
@confirm="submitConfirm(detail.basic_information_card)"
|
@confirm="submitConfirm(detail.basic_information_card)"
|
||||||
@@ -132,6 +135,8 @@
|
|||||||
:hotel-id="detail.order_task.hotel_id"
|
:hotel-id="detail.order_task.hotel_id"
|
||||||
:order-task-id="detail.order_task.order_task_id"
|
:order-task-id="detail.order_task.order_task_id"
|
||||||
:source-message-id="detail.order_task.source_message_id"
|
:source-message-id="detail.order_task.source_message_id"
|
||||||
|
:order-reference="displayOrderReference"
|
||||||
|
:target-locator="targetLocator"
|
||||||
:action-errors="cardActionErrors[businessCard.card_id] ?? []"
|
:action-errors="cardActionErrors[businessCard.card_id] ?? []"
|
||||||
:success-message="cardSuccessMessages[businessCard.card_id] ?? ''"
|
:success-message="cardSuccessMessages[businessCard.card_id] ?? ''"
|
||||||
:review-form="reviewForms[businessCard.card_id]"
|
:review-form="reviewForms[businessCard.card_id]"
|
||||||
@@ -139,6 +144,7 @@
|
|||||||
:can-confirm="canConfirmCard(businessCard)"
|
:can-confirm="canConfirmCard(businessCard)"
|
||||||
:can-review="canReviewCard(businessCard)"
|
:can-review="canReviewCard(businessCard)"
|
||||||
:bound-order-id="detail.bound_order?.order_id"
|
:bound-order-id="detail.bound_order?.order_id"
|
||||||
|
:bound-order-display-name="detail.bound_order?.display_name"
|
||||||
@update:model-value="setCardValues(businessCard, $event)"
|
@update:model-value="setCardValues(businessCard, $event)"
|
||||||
@update:review-form="setReviewForm(businessCard.card_id, $event)"
|
@update:review-form="setReviewForm(businessCard.card_id, $event)"
|
||||||
@confirm="submitConfirm(businessCard)"
|
@confirm="submitConfirm(businessCard)"
|
||||||
@@ -459,7 +465,7 @@ function editableFieldKeys(card: ReservationV4TaskCardResult): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function submissionFieldsForCard(card: ReservationV4TaskCardResult): ReservationV4TaskCardResult['fields'] {
|
function submissionFieldsForCard(card: ReservationV4TaskCardResult): ReservationV4TaskCardResult['fields'] {
|
||||||
if (isPaymentCard(card)) {
|
if (isVersionOnlyConfirmCard(card)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return card.card_type === 'ROOM_INFORMATION'
|
return card.card_type === 'ROOM_INFORMATION'
|
||||||
@@ -473,7 +479,7 @@ function buildConfirmRequest(
|
|||||||
card: ReservationV4TaskCardResult,
|
card: ReservationV4TaskCardResult,
|
||||||
submissionFields: ReservationV4TaskCardResult['fields'],
|
submissionFields: ReservationV4TaskCardResult['fields'],
|
||||||
): ReservationV4CardConfirmRequest {
|
): ReservationV4CardConfirmRequest {
|
||||||
if (isPaymentCard(card)) {
|
if (isVersionOnlyConfirmCard(card)) {
|
||||||
return {
|
return {
|
||||||
version: card.version,
|
version: card.version,
|
||||||
}
|
}
|
||||||
@@ -488,6 +494,14 @@ function isPaymentCard(card: ReservationV4TaskCardResult): boolean {
|
|||||||
return card.card_type === 'PAYMENT'
|
return card.card_type === 'PAYMENT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRoomingListCard(card: ReservationV4TaskCardResult): boolean {
|
||||||
|
return card.card_type === 'ROOMING_LIST'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVersionOnlyConfirmCard(card: ReservationV4TaskCardResult): boolean {
|
||||||
|
return isPaymentCard(card) || isRoomingListCard(card)
|
||||||
|
}
|
||||||
|
|
||||||
function isGroupRoomInformationBreakfastField(
|
function isGroupRoomInformationBreakfastField(
|
||||||
card: ReservationV4TaskCardResult,
|
card: ReservationV4TaskCardResult,
|
||||||
field: ReservationV4TaskCardResult['fields'][number],
|
field: ReservationV4TaskCardResult['fields'][number],
|
||||||
@@ -521,6 +535,9 @@ function applyCardActionError(card: ReservationV4TaskCardResult, error: unknown,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cardTitle(card: ReservationV4TaskCardResult): string {
|
function cardTitle(card: ReservationV4TaskCardResult): string {
|
||||||
|
if (isRoomingListCard(card)) {
|
||||||
|
return t('taskV4.roomingList.title')
|
||||||
|
}
|
||||||
return formatReservationTaskCard(t, card.card_type)
|
return formatReservationTaskCard(t, card.card_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user