实现V4 Trace事项卡前端展示
This commit is contained in:
@@ -439,6 +439,13 @@ function isSelectControl(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
}
|
||||
|
||||
function fixedFieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
const backendOptions = (field.fixed_options ?? []).map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label || option.value,
|
||||
}))
|
||||
if (backendOptions.length) {
|
||||
return backendOptions
|
||||
}
|
||||
const optionsSource = normalizeControlType(field.options_source)
|
||||
if (optionsSource !== 'RESERVATION_V4_GROUP_BOOKING_STATUS_FIXED') {
|
||||
return []
|
||||
|
||||
@@ -58,6 +58,16 @@
|
||||
:card="card"
|
||||
:source-message-id="sourceMessageId"
|
||||
/>
|
||||
<ReservationV4TraceCard
|
||||
v-else-if="isTraceCard"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
v-else
|
||||
class="task-card-section__fields"
|
||||
@@ -72,7 +82,7 @@
|
||||
</template>
|
||||
|
||||
<details
|
||||
v-if="!isRoomInformationCard && !isRoomingListCard && safePayloadRows.length"
|
||||
v-if="!isRoomInformationCard && !isRoomingListCard && !isTraceCard && safePayloadRows.length"
|
||||
class="safe-payload"
|
||||
>
|
||||
<summary>
|
||||
@@ -124,6 +134,7 @@
|
||||
<label>
|
||||
<span>{{ t('taskV4.reviewReason') }}</span>
|
||||
<textarea
|
||||
name="v4_review_reason"
|
||||
:value="reviewForm.reason"
|
||||
:placeholder="t('taskV4.reviewReasonPlaceholder')"
|
||||
rows="3"
|
||||
@@ -182,6 +193,7 @@ import ReservationV4PaymentAttachmentPreview from '@/components/reservation/Rese
|
||||
import ReservationV4RoomInformationCard from '@/components/reservation/ReservationV4RoomInformationCard.vue'
|
||||
import ReservationV4RoomingListCard from '@/components/reservation/ReservationV4RoomingListCard.vue'
|
||||
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
||||
import ReservationV4TraceCard from '@/components/reservation/ReservationV4TraceCard.vue'
|
||||
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
|
||||
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
|
||||
import {
|
||||
@@ -242,6 +254,7 @@ const { t } = useI18n()
|
||||
const isRoomInformationCard = computed(() => props.card.card_type === 'ROOM_INFORMATION')
|
||||
const isPaymentCard = computed(() => props.card.card_type === 'PAYMENT')
|
||||
const isRoomingListCard = computed(() => props.card.card_type === 'ROOMING_LIST')
|
||||
const isTraceCard = computed(() => props.card.card_type === 'TRACE_RESERVATION_NOTES')
|
||||
const visibleFields = computed(() =>
|
||||
isPaymentCard.value
|
||||
? props.card.fields.filter((field) => !isPaymentAttachmentIdField(field))
|
||||
|
||||
543
client/src/components/reservation/ReservationV4TraceCard.vue
Normal file
543
client/src/components/reservation/ReservationV4TraceCard.vue
Normal file
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<section
|
||||
class="trace-card"
|
||||
data-testid="trace-card"
|
||||
>
|
||||
<article
|
||||
v-for="item in traceItems"
|
||||
:key="item.index"
|
||||
class="trace-item"
|
||||
>
|
||||
<header class="trace-item__header">
|
||||
<span>{{ t('taskV4.trace.itemLabel', { index: item.index + 1 }) }}</span>
|
||||
<strong>{{ item.type === 'EXTRA_BED' ? t('taskV4.trace.extraBedTitle') : t('taskV4.trace.generalTitle') }}</strong>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="item.type === 'EXTRA_BED'"
|
||||
class="trace-item__action"
|
||||
>
|
||||
<span>{{ t('taskV4.trace.action') }}</span>
|
||||
<strong>SET EXTRA BED</strong>
|
||||
</div>
|
||||
|
||||
<div class="trace-item__fields">
|
||||
<label
|
||||
v-for="field in fieldsForItem(item)"
|
||||
:key="fieldKey(field)"
|
||||
class="trace-field"
|
||||
:class="{ 'trace-field--readonly': !isEditable(field) }"
|
||||
>
|
||||
<span class="trace-field__label">
|
||||
{{ field.display_name }}
|
||||
<sup v-if="field.required">*</sup>
|
||||
<em v-if="!isEditable(field)">{{ t('taskV4.field.readonly') }}</em>
|
||||
</span>
|
||||
|
||||
<select
|
||||
v-if="isEditable(field) && isSelectField(field)"
|
||||
class="trace-field__control"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:disabled="selectDisabled(field)"
|
||||
:value="fieldValue(field)"
|
||||
@change="updateField(field, $event)"
|
||||
>
|
||||
<option value="">{{ t('taskV4.field.selectPlaceholder') }}</option>
|
||||
<option
|
||||
v-for="option in fieldOptions(field)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<textarea
|
||||
v-else-if="isEditable(field) && isTextField(field)"
|
||||
class="trace-field__control trace-field__control--textarea"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
rows="3"
|
||||
@input="updateField(field, $event)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field) && isPositiveIntegerField(field)"
|
||||
class="trace-field__control"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field)"
|
||||
class="trace-field__control"
|
||||
type="text"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="trace-field__static"
|
||||
>
|
||||
{{ staticFieldValue(field) }}
|
||||
</span>
|
||||
|
||||
<small
|
||||
v-if="field.control_hint"
|
||||
class="trace-field__hint"
|
||||
>{{ field.control_hint }}</small>
|
||||
<small
|
||||
v-for="hint in lookupHints(field)"
|
||||
:key="hint"
|
||||
class="trace-field__hint"
|
||||
>{{ hint }}</small>
|
||||
<small
|
||||
v-for="error in visibleFieldErrors(field)"
|
||||
:key="error"
|
||||
class="trace-field__error"
|
||||
>{{ error }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { fetchReservationV4RoomTypeLookups } from '@/services/reservationService'
|
||||
import type {
|
||||
ReservationRecord,
|
||||
ReservationV4CatalogLookupItem,
|
||||
ReservationV4CatalogLookupResult,
|
||||
ReservationV4TaskCardFieldResult,
|
||||
ReservationV4TaskCardResult,
|
||||
} from '@/types/reservation'
|
||||
import {
|
||||
isReservationV4EditableField,
|
||||
isReservationV4TraceSafeField,
|
||||
fieldPathToPointer,
|
||||
readV4FieldValue,
|
||||
reservationV4FieldKey,
|
||||
reservationV4LookupKindForOptionsSource,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
type TraceItemType = 'GENERAL' | 'EXTRA_BED'
|
||||
type TraceItem = {
|
||||
index: number
|
||||
type: TraceItemType
|
||||
}
|
||||
type FieldOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
card: ReservationV4TaskCardResult
|
||||
modelValue: ReservationRecord
|
||||
readOnly: boolean
|
||||
editableKeys: string[]
|
||||
validationErrors?: Record<string, string>
|
||||
hotelId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ReservationRecord]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const roomTypeLookup = reactive<{
|
||||
loading: boolean
|
||||
error: string
|
||||
result: ReservationV4CatalogLookupResult | null
|
||||
}>({
|
||||
loading: false,
|
||||
error: '',
|
||||
result: null,
|
||||
})
|
||||
let roomTypeLookupRequestVersion = 0
|
||||
|
||||
const traceFields = computed(() => props.card.fields.filter(isReservationV4TraceSafeField))
|
||||
const hasRoomTypeLookupField = computed(() =>
|
||||
traceFields.value.some((field) => reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE'),
|
||||
)
|
||||
const traceItems = computed<TraceItem[]>(() => {
|
||||
const itemIndexes = [...new Set(traceFields.value.map(fieldIndex).filter((index) => index >= 0))]
|
||||
return itemIndexes.sort((left, right) => left - right).map((index) => ({
|
||||
index,
|
||||
type: traceItemType(index),
|
||||
}))
|
||||
})
|
||||
|
||||
watch(
|
||||
[hasRoomTypeLookupField, () => props.hotelId],
|
||||
() => {
|
||||
if (hasRoomTypeLookupField.value) {
|
||||
void loadRoomTypeLookup()
|
||||
} else {
|
||||
roomTypeLookupRequestVersion += 1
|
||||
roomTypeLookup.loading = false
|
||||
roomTypeLookup.error = ''
|
||||
roomTypeLookup.result = null
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function fieldsForItem(item: TraceItem): ReservationV4TaskCardFieldResult[] {
|
||||
const preferredOrder = item.type === 'EXTRA_BED'
|
||||
? ['target_room_type_code', 'extra_bed_room_count', 'department_code']
|
||||
: ['text', 'department_code']
|
||||
return preferredOrder
|
||||
.map((suffix) => traceFields.value.find((field) => fieldPointer(field) === `/trace_items/${item.index}/${suffix}`))
|
||||
.filter((field): field is ReservationV4TaskCardFieldResult => Boolean(field))
|
||||
}
|
||||
|
||||
function isEditable(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const key = fieldKey(field)
|
||||
return !props.readOnly &&
|
||||
props.editableKeys.includes(key) &&
|
||||
isReservationV4EditableField(field)
|
||||
}
|
||||
|
||||
function isSelectField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fixedFieldOptions(field).length > 0 ||
|
||||
isDepartmentField(field) ||
|
||||
reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE'
|
||||
}
|
||||
|
||||
function isTextField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fieldPointer(field).endsWith('/text')
|
||||
}
|
||||
|
||||
function isPositiveIntegerField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fieldPointer(field).endsWith('/extra_bed_room_count')
|
||||
}
|
||||
|
||||
function isDepartmentField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fieldPointer(field).endsWith('/department_code')
|
||||
}
|
||||
|
||||
function fieldKey(field: ReservationV4TaskCardFieldResult): string {
|
||||
return reservationV4FieldKey(field)
|
||||
}
|
||||
|
||||
function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
||||
const value = readV4FieldValue(field, props.modelValue)
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return stringifyValue(value)
|
||||
}
|
||||
|
||||
function staticFieldValue(field: ReservationV4TaskCardFieldResult): string {
|
||||
const value = String(fieldValue(field))
|
||||
return fieldOptions(field).find((option) => option.value === value)?.label ?? stringifyValue(value)
|
||||
}
|
||||
|
||||
function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
if (reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE') {
|
||||
return roomTypeOptions(field)
|
||||
}
|
||||
const options = fixedFieldOptions(field)
|
||||
if (!options.length && isDepartmentField(field)) {
|
||||
const currentValue = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
return currentValue
|
||||
? [{
|
||||
value: currentValue,
|
||||
label: currentValue,
|
||||
}]
|
||||
: []
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function fixedFieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
return (field.fixed_options ?? []).map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label || option.value,
|
||||
}))
|
||||
}
|
||||
|
||||
function roomTypeOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
const options = (roomTypeLookup.result?.items ?? []).map((item) => ({
|
||||
value: item.code,
|
||||
label: lookupItemLabel(item),
|
||||
}))
|
||||
const currentValue = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
if (currentValue && !options.some((option) => option.value === currentValue)) {
|
||||
options.push({
|
||||
value: currentValue,
|
||||
label: currentValue,
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function selectDisabled(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
if (isDepartmentField(field)) {
|
||||
return fixedFieldOptions(field).length === 0
|
||||
}
|
||||
return reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE' &&
|
||||
(roomTypeLookup.loading || Boolean(roomTypeLookup.error))
|
||||
}
|
||||
|
||||
function lookupHints(field: ReservationV4TaskCardFieldResult): string[] {
|
||||
if (reservationV4LookupKindForOptionsSource(field.options_source) !== 'ROOM_TYPE') {
|
||||
return []
|
||||
}
|
||||
if (roomTypeLookup.loading) {
|
||||
return [t('taskV4.lookup.loading')]
|
||||
}
|
||||
if (roomTypeLookup.error) {
|
||||
return [roomTypeLookup.error]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function visibleFieldErrors(field: ReservationV4TaskCardFieldResult): string[] {
|
||||
return [
|
||||
...(field.validation_errors ?? []),
|
||||
props.validationErrors?.[fieldKey(field)] ?? '',
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
||||
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[fieldKey(field)]: inputValue(field, target.value),
|
||||
})
|
||||
}
|
||||
|
||||
function inputValue(field: ReservationV4TaskCardFieldResult, value: string): string | number {
|
||||
if (!isPositiveIntegerField(field)) {
|
||||
return value
|
||||
}
|
||||
if (value.trim() === '') {
|
||||
return ''
|
||||
}
|
||||
const numericValue = Number(value)
|
||||
return Number.isFinite(numericValue) ? numericValue : value
|
||||
}
|
||||
|
||||
async function loadRoomTypeLookup(): Promise<void> {
|
||||
const requestVersion = ++roomTypeLookupRequestVersion
|
||||
roomTypeLookup.loading = true
|
||||
roomTypeLookup.error = ''
|
||||
try {
|
||||
const result = await fetchReservationV4RoomTypeLookups({
|
||||
...(props.hotelId ? { hotel_id: props.hotelId } : {}),
|
||||
page_num: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
if (requestVersion === roomTypeLookupRequestVersion) {
|
||||
roomTypeLookup.result = {
|
||||
...result,
|
||||
items: result.items ?? [],
|
||||
warnings: result.warnings ?? [],
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (requestVersion === roomTypeLookupRequestVersion) {
|
||||
roomTypeLookup.error = t('taskV4.lookup.error')
|
||||
}
|
||||
} finally {
|
||||
if (requestVersion === roomTypeLookupRequestVersion) {
|
||||
roomTypeLookup.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function traceItemType(index: number): TraceItemType {
|
||||
const payloadItem = payloadTraceItem(index)
|
||||
if (readString(payloadItem?.item_type).toUpperCase() === 'EXTRA_BED') {
|
||||
return 'EXTRA_BED'
|
||||
}
|
||||
return traceFields.value.some((field) =>
|
||||
fieldPointer(field) === `/trace_items/${index}/target_room_type_code` ||
|
||||
fieldPointer(field) === `/trace_items/${index}/extra_bed_room_count`,
|
||||
)
|
||||
? 'EXTRA_BED'
|
||||
: 'GENERAL'
|
||||
}
|
||||
|
||||
function payloadTraceItem(index: number): ReservationRecord | null {
|
||||
const traceItemsValue = props.card.display_payload?.trace_items
|
||||
if (!Array.isArray(traceItemsValue)) {
|
||||
return null
|
||||
}
|
||||
const item = traceItemsValue[index]
|
||||
return isRecord(item) ? item : null
|
||||
}
|
||||
|
||||
function fieldIndex(field: ReservationV4TaskCardFieldResult): number {
|
||||
const match = fieldPointer(field).match(/^\/trace_items\/(\d+)\//)
|
||||
return match ? Number(match[1]) : -1
|
||||
}
|
||||
|
||||
function fieldPointer(field: ReservationV4TaskCardFieldResult): string {
|
||||
return field.field_pointer || fieldPathToPointer(field.field_path)
|
||||
}
|
||||
|
||||
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
||||
if (item.display_name && item.display_name !== item.code) {
|
||||
return `${item.code} - ${item.display_name}`
|
||||
}
|
||||
return item.code
|
||||
}
|
||||
|
||||
function stringifyValue(value: unknown): string {
|
||||
return stringifyReservationV4SafeDisplayValue(
|
||||
value,
|
||||
t('taskV4.field.empty'),
|
||||
t('taskV4.field.hidden'),
|
||||
)
|
||||
}
|
||||
|
||||
function readString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is ReservationRecord {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trace-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin: 0 20px 18px;
|
||||
}
|
||||
|
||||
.trace-item {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.trace-item__header,
|
||||
.trace-item__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trace-item__header span,
|
||||
.trace-item__action span {
|
||||
color: var(--th-color-blue-600);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.trace-item__header strong,
|
||||
.trace-item__action strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.trace-item__action {
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.trace-item__fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.trace-field {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.trace-field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.trace-field__label sup {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.trace-field__label em {
|
||||
border-radius: 999px;
|
||||
background: var(--th-color-slate-100);
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.trace-field__control,
|
||||
.trace-field__static {
|
||||
min-height: 38px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
font: inherit;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.trace-field__control--textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.trace-field__control[aria-invalid='true'] {
|
||||
border-color: var(--th-color-danger);
|
||||
box-shadow: 0 0 0 3px var(--th-color-danger-bg);
|
||||
}
|
||||
|
||||
.trace-field__static {
|
||||
display: block;
|
||||
background: var(--th-color-white);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.trace-field__hint {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trace-field__error {
|
||||
color: var(--th-color-danger);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.trace-item__fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -630,6 +630,13 @@ export default {
|
||||
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.',
|
||||
},
|
||||
trace: {
|
||||
title: 'Trace notes',
|
||||
itemLabel: 'Item {index}',
|
||||
generalTitle: 'General note',
|
||||
extraBedTitle: 'Extra bed note',
|
||||
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.',
|
||||
|
||||
@@ -630,6 +630,13 @@ export default {
|
||||
description: 'ข้อความต้นทางปัจจุบันมีรายการ Rooming List ที่ต้องให้เจ้าหน้าที่จัดการ',
|
||||
confirmHint: 'เวอร์ชันแรกนี้ใช้ยืนยันว่ารายการได้รับการจัดการด้วยคนแล้วเท่านั้น ไม่แสดงหรือประมวลผลรายชื่อแขกและเนื้อหาไฟล์แนบในหน้านี้',
|
||||
},
|
||||
trace: {
|
||||
title: 'หมายเหตุ Trace',
|
||||
itemLabel: 'รายการ {index}',
|
||||
generalTitle: 'หมายเหตุทั่วไป',
|
||||
extraBedTitle: 'รายการเตียงเสริม',
|
||||
action: 'การดำเนินการคงที่',
|
||||
},
|
||||
lookup: {
|
||||
loading: 'กำลังโหลดแค็ตตาล็อก',
|
||||
empty: 'ไม่มีตัวเลือกในแค็ตตาล็อกปัจจุบัน หากค้นหาไม่พบไม่ได้หมายความว่าแค็ตตาล็อกยังไม่เริ่มต้น',
|
||||
|
||||
@@ -630,6 +630,13 @@ export default {
|
||||
description: '当前来源消息包含需要人工处理的 Rooming List 事项,请在酒店内部流程处理后确认该卡片。',
|
||||
confirmHint: '第一版仅确认该事项已由人工处理,不展示或处理名单明细和附件内容。',
|
||||
},
|
||||
trace: {
|
||||
title: 'Trace / 追踪备注事项',
|
||||
itemLabel: '事项 {index}',
|
||||
generalTitle: '普通事项',
|
||||
extraBedTitle: '加床事项',
|
||||
action: '固定动作',
|
||||
},
|
||||
lookup: {
|
||||
loading: '目录加载中',
|
||||
empty: '当前目录没有可选项;如果是搜索无结果,不代表目录未初始化。',
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('reservation V4 pages', () => {
|
||||
|
||||
it('keeps V4 task card layout classes for styled card metadata and payload summary', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
detail.business_cards[0]!.card_type = 'TRACE_RESERVATION_NOTES'
|
||||
detail.business_cards[0]!.card_type = 'VOUCHER'
|
||||
detail.business_cards[0]!.display_payload = {
|
||||
booking_scenario: 'STANDARD',
|
||||
relevant_message_excerpt: 'Please keep this visible.',
|
||||
@@ -514,6 +514,178 @@ describe('reservation V4 pages', () => {
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
||||
})
|
||||
|
||||
it('renders GENERAL Trace cards with fixed department options and confirms safe trace fields only', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
useTraceBusinessCard(detail, 'GENERAL')
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue({
|
||||
...detail,
|
||||
business_cards: [
|
||||
{
|
||||
...detail.business_cards[0]!,
|
||||
card_status: 'CONFIRMED',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
const traceSection = wrapper
|
||||
.findAll('.task-card-section')
|
||||
.find((section) => section.find('[data-testid="trace-card"]').exists())
|
||||
expect(traceSection).toBeTruthy()
|
||||
expect((traceSection!.find('textarea[name="/trace_items/0/text"]').element as HTMLTextAreaElement).value)
|
||||
.toBe('Late arrival note')
|
||||
expect(traceSection!.text()).not.toContain('CONTENT_SHOULD_NOT_RENDER')
|
||||
expect(traceSection!.text()).not.toContain('TARGET_ORDER_SHOULD_NOT_RENDER')
|
||||
expect(traceSection!.text()).not.toContain('AI_PAYLOAD_SHOULD_NOT_RENDER')
|
||||
expect(traceSection!.text()).not.toContain('https://oss.example/private/trace.pdf')
|
||||
expect(traceSection!.find('details.safe-payload').exists()).toBe(false)
|
||||
|
||||
const departmentOptions = traceSection!
|
||||
.find('select[name="/trace_items/0/department_code"]')
|
||||
.findAll('option')
|
||||
.map((option) => option.attributes('value'))
|
||||
expect(departmentOptions).toEqual(['', 'FO', 'HSK', 'FO+HSK'])
|
||||
|
||||
await traceSection!.find('textarea[name="/trace_items/0/text"]').setValue('Arrange late arrival follow-up')
|
||||
await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('HSK')
|
||||
await traceSection!.find('button.primary-button').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-trace', {
|
||||
version: 10,
|
||||
confirmed_payload: {
|
||||
trace_items: [
|
||||
{
|
||||
text: 'Arrange late arrival follow-up',
|
||||
department_code: 'HSK',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const submittedRequest = vi.mocked(service.confirmReservationV4OrderTaskCard).mock.calls[0]?.[2]
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('content')
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('target_order')
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('AI_PAYLOAD_SHOULD_NOT_RENDER')
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
||||
})
|
||||
|
||||
it('renders EXTRA_BED Trace cards with room type lookup, positive count input and confirms safe fields only', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
useTraceBusinessCard(detail, 'EXTRA_BED')
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue(detail)
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
const traceSection = wrapper
|
||||
.findAll('.task-card-section')
|
||||
.find((section) => section.find('[data-testid="trace-card"]').exists())
|
||||
expect(traceSection).toBeTruthy()
|
||||
expect(traceSection!.text()).toContain('SET EXTRA BED')
|
||||
expect(traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').text()).toContain('TWN - Twin')
|
||||
const roomCountInput = traceSection!.find('input[name="/trace_items/0/extra_bed_room_count"]')
|
||||
expect(roomCountInput.attributes('type')).toBe('number')
|
||||
expect(roomCountInput.attributes('min')).toBe('1')
|
||||
expect(roomCountInput.attributes('step')).toBe('1')
|
||||
|
||||
await traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').setValue('TWN')
|
||||
await roomCountInput.setValue('0')
|
||||
await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('FO+HSK')
|
||||
await traceSection!.find('button.primary-button').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
|
||||
expect(traceSection!.text()).toContain('Extra Bed Room Count must be a positive integer')
|
||||
|
||||
await roomCountInput.setValue('2')
|
||||
await traceSection!.find('button.primary-button').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-trace', {
|
||||
version: 10,
|
||||
confirmed_payload: {
|
||||
trace_items: [
|
||||
{
|
||||
target_room_type_code: 'TWN',
|
||||
extra_bed_room_count: 2,
|
||||
department_code: 'FO+HSK',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const submittedRequest = vi.mocked(service.confirmReservationV4OrderTaskCard).mock.calls[0]?.[2]
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('content')
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('target_order')
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
|
||||
})
|
||||
|
||||
it('resolves REVIEW_REQUIRED Trace cards with field errors and safe review overrides', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
useTraceBusinessCard(detail, 'GENERAL', 'REVIEW_REQUIRED')
|
||||
detail.order_task.order_id = 'order-2001'
|
||||
detail.order_task.target_resolution_status = 'RESOLVED'
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockResolvedValue({
|
||||
...detail,
|
||||
business_cards: [
|
||||
{
|
||||
...detail.business_cards[0]!,
|
||||
card_status: 'PENDING_CONFIRM',
|
||||
review_status: 'RESOLVED',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
const traceSection = wrapper
|
||||
.findAll('.task-card-section')
|
||||
.find((section) => section.find('[data-testid="trace-card"]').exists())
|
||||
expect(traceSection).toBeTruthy()
|
||||
expect(traceSection!.find('.trace-field__error').text()).toContain('Department is required')
|
||||
|
||||
await traceSection!.find('textarea[name="/trace_items/0/text"]').setValue('Confirm trace review text')
|
||||
await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('FO')
|
||||
await traceSection!.find('textarea[name="v4_review_reason"]').setValue('confirmed trace note')
|
||||
await traceSection!.find('button.primary-button').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
|
||||
expect(service.resolveReservationV4OrderTaskCardReview).toHaveBeenCalledWith('9001', 'card-trace', {
|
||||
version: 10,
|
||||
confirmed_order_id: 'order-2001',
|
||||
reason: 'confirmed trace note',
|
||||
field_overrides: [
|
||||
{
|
||||
field_pointer: '/trace_items/0/text',
|
||||
value: 'Confirm trace review text',
|
||||
},
|
||||
{
|
||||
field_pointer: '/trace_items/0/department_code',
|
||||
value: 'FO',
|
||||
},
|
||||
],
|
||||
})
|
||||
const submittedRequest = vi.mocked(service.resolveReservationV4OrderTaskCardReview).mock.calls[0]?.[2]
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('content')
|
||||
expect(JSON.stringify(submittedRequest)).not.toContain('target_order')
|
||||
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 () => {
|
||||
const detail = createOrderTaskDetail({
|
||||
businessEventType: 'NEW_BOOKING',
|
||||
@@ -1557,6 +1729,124 @@ function useBoundOrder(detail: ReservationV4OrderTaskDetailResult): void {
|
||||
}
|
||||
}
|
||||
|
||||
function useTraceBusinessCard(
|
||||
detail: ReservationV4OrderTaskDetailResult,
|
||||
itemType: 'GENERAL' | 'EXTRA_BED',
|
||||
cardStatus = 'PENDING_CONFIRM',
|
||||
): void {
|
||||
const isReviewRequired = cardStatus === 'REVIEW_REQUIRED'
|
||||
detail.business_cards = [
|
||||
createCard('card-trace', 'TRACE_RESERVATION_NOTES', cardStatus, {
|
||||
event_type: 'TRACE_RESERVATION_NOTES',
|
||||
version: 10,
|
||||
availability: createAvailability({
|
||||
confirmable: cardStatus === 'PENDING_CONFIRM',
|
||||
reviewable: isReviewRequired,
|
||||
editable: true,
|
||||
read_only: false,
|
||||
}),
|
||||
display_payload: {
|
||||
trace_items: [
|
||||
itemType === 'EXTRA_BED'
|
||||
? {
|
||||
item_type: 'EXTRA_BED',
|
||||
target_room_type_code: 'TWN',
|
||||
extra_bed_room_count: 1,
|
||||
department_code: 'FO',
|
||||
content: 'CONTENT_SHOULD_NOT_RENDER',
|
||||
}
|
||||
: {
|
||||
item_type: 'GENERAL',
|
||||
text: 'Late arrival note',
|
||||
department_code: 'FO',
|
||||
content: 'CONTENT_SHOULD_NOT_RENDER',
|
||||
},
|
||||
],
|
||||
target_order: 'TARGET_ORDER_SHOULD_NOT_RENDER',
|
||||
ai_payload_json: 'AI_PAYLOAD_SHOULD_NOT_RENDER',
|
||||
attachment_url: 'https://oss.example/private/trace.pdf',
|
||||
},
|
||||
fields: itemType === 'EXTRA_BED'
|
||||
? [
|
||||
createField('/trace_items/0/target_room_type_code', {
|
||||
display_name: 'Target Room Type',
|
||||
value: 'TWN',
|
||||
options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG',
|
||||
control_type: 'SELECT',
|
||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
|
||||
}),
|
||||
createField('/trace_items/0/extra_bed_room_count', {
|
||||
display_name: 'Extra Bed Room Count',
|
||||
value: 1,
|
||||
control_type: 'NUMBER',
|
||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
|
||||
}),
|
||||
createTraceDepartmentField(isReviewRequired),
|
||||
createField('/trace_items/0/content', {
|
||||
display_name: 'Legacy content',
|
||||
value: 'CONTENT_SHOULD_NOT_RENDER',
|
||||
editable: true,
|
||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
|
||||
}),
|
||||
]
|
||||
: [
|
||||
createField('/trace_items/0/text', {
|
||||
display_name: 'Trace Text',
|
||||
value: 'Late arrival note',
|
||||
control_type: 'TEXTAREA',
|
||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
|
||||
}),
|
||||
createTraceDepartmentField(isReviewRequired, isReviewRequired ? ['Department is required'] : []),
|
||||
createField('/trace_items/0/content', {
|
||||
display_name: 'Legacy content',
|
||||
value: 'CONTENT_SHOULD_NOT_RENDER',
|
||||
editable: true,
|
||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]
|
||||
detail.card_counts = {
|
||||
...detail.card_counts,
|
||||
pending_confirm_count: cardStatus === 'PENDING_CONFIRM' ? 2 : 1,
|
||||
review_required_count: isReviewRequired ? 1 : 0,
|
||||
}
|
||||
}
|
||||
|
||||
function createTraceDepartmentField(
|
||||
isReviewRequired: boolean,
|
||||
validationErrors: string[] = [],
|
||||
): ReservationV4TaskCardResult['fields'][number] {
|
||||
return createField('/trace_items/0/department_code', {
|
||||
display_name: 'Department',
|
||||
value: 'FO',
|
||||
control_type: 'SELECT',
|
||||
options_source: 'reservation_v4_trace_department_fixed',
|
||||
fixed_options: [
|
||||
{
|
||||
value: 'FO',
|
||||
label: 'FO',
|
||||
},
|
||||
{
|
||||
value: 'HSK',
|
||||
label: 'HSK',
|
||||
},
|
||||
{
|
||||
value: 'FO+HSK',
|
||||
label: 'FO+HSK',
|
||||
},
|
||||
],
|
||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
|
||||
validation_errors: validationErrors,
|
||||
})
|
||||
}
|
||||
|
||||
function createPaymentConversationMedia(): SourceMessageOriginalMedia[] {
|
||||
return [
|
||||
{
|
||||
@@ -1830,6 +2120,7 @@ function createField(
|
||||
edit_scope: overrides.edit_scope ?? 'NORMAL_TASK',
|
||||
write_target: overrides.write_target ?? 'CONFIRMED_PAYLOAD_JSON',
|
||||
options_source: overrides.options_source ?? null,
|
||||
fixed_options: overrides.fixed_options ?? null,
|
||||
raw_readonly: overrides.raw_readonly ?? false,
|
||||
validation_errors: overrides.validation_errors ?? [],
|
||||
control_hint: overrides.control_hint ?? null,
|
||||
|
||||
@@ -358,11 +358,17 @@ export interface ReservationV4TaskCardFieldResult {
|
||||
edit_scope: string | null
|
||||
write_target: string | null
|
||||
options_source: string | null
|
||||
fixed_options?: ReservationV4TaskCardFieldOptionResult[] | null
|
||||
raw_readonly: boolean | null
|
||||
validation_errors: string[]
|
||||
control_hint: string | null
|
||||
}
|
||||
|
||||
export interface ReservationV4TaskCardFieldOptionResult {
|
||||
value: string
|
||||
label: string | null
|
||||
}
|
||||
|
||||
export interface ReservationV4RoomInformationRoomItem extends ReservationRecord {
|
||||
room_type_code?: string | null
|
||||
room_count?: number | string | null
|
||||
|
||||
@@ -18,8 +18,14 @@ const reviewWriteTargets = new Set([
|
||||
'FIELD_OVERRIDES',
|
||||
])
|
||||
const readonlyControlTypes = new Set(['READONLY', 'FILE', 'WORKFLOW_STATE', 'STRUCTURED_TABLE'])
|
||||
const reviewWritablePointerPrefixes = ['/basic_information/', '/room_information/final_values/', '/business_fields/']
|
||||
const reviewWritablePointerPrefixes = [
|
||||
'/basic_information/',
|
||||
'/room_information/final_values/',
|
||||
'/business_fields/',
|
||||
'/trace_items/',
|
||||
]
|
||||
const roomInformationFieldPrefix = '/room_information/final_values/'
|
||||
const traceFieldPattern = /^\/trace_items\/\d+\/(?:text|department_code|target_room_type_code|extra_bed_room_count)$/
|
||||
const hiddenDisplayValue = Symbol('reservation-v4-hidden-display-value')
|
||||
|
||||
export type ReservationV4FieldErrorMap = Record<string, string>
|
||||
@@ -43,6 +49,11 @@ export function isReservationV4RoomInformationSafeField(field: ReservationV4Task
|
||||
return pointer.startsWith(roomInformationFieldPrefix) && !isForbiddenRoomInformationPointer(pointer)
|
||||
}
|
||||
|
||||
export function isReservationV4TraceSafeField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
|
||||
return traceFieldPattern.test(pointer)
|
||||
}
|
||||
|
||||
export function buildReservationV4ConfirmedPayload(
|
||||
fields: ReservationV4TaskCardFieldResult[],
|
||||
values: ReservationRecord,
|
||||
@@ -73,8 +84,13 @@ export function validateReservationV4Fields(
|
||||
): ReservationV4FieldErrorMap {
|
||||
return fields.filter(isReservationV4EditableField).reduce<ReservationV4FieldErrorMap>((errors, field) => {
|
||||
const key = reservationV4FieldKey(field)
|
||||
if (field.required && isEmptyV4Value(readV4FieldValue(field, values))) {
|
||||
const value = readV4FieldValue(field, values)
|
||||
if (field.required && isEmptyV4Value(value)) {
|
||||
errors[key] = `${field.display_name} is required`
|
||||
return errors
|
||||
}
|
||||
if (isTracePositiveIntegerField(field) && !isEmptyV4Value(value) && !isPositiveIntegerValue(value)) {
|
||||
errors[key] = `${field.display_name} must be a positive integer`
|
||||
}
|
||||
return errors
|
||||
}, {})
|
||||
@@ -383,6 +399,21 @@ function isForbiddenRoomInformationPointer(pointer: string): boolean {
|
||||
normalizedPointer.includes('/locator_value')
|
||||
}
|
||||
|
||||
function isTracePositiveIntegerField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
|
||||
return /^\/trace_items\/\d+\/extra_bed_room_count$/.test(pointer)
|
||||
}
|
||||
|
||||
function isPositiveIntegerValue(value: unknown): boolean {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isInteger(value) && value > 0
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return false
|
||||
}
|
||||
return /^[1-9]\d*$/.test(value.trim())
|
||||
}
|
||||
|
||||
function isUnsafeUrlLikeValue(value: string): boolean {
|
||||
const trimmedValue = value.trim()
|
||||
return /^(https?:\/\/|oss:\/\/|s3:\/\/)/i.test(trimmedValue)
|
||||
|
||||
@@ -224,6 +224,7 @@ import {
|
||||
isReservationV4ConfirmWritableField,
|
||||
isReservationV4RoomInformationSafeField,
|
||||
isReservationV4ReviewWritableField,
|
||||
isReservationV4TraceSafeField,
|
||||
mapReservationV4BackendDetailsToFields,
|
||||
reservationV4FieldKey,
|
||||
validateReservationV4Fields,
|
||||
@@ -468,11 +469,15 @@ function submissionFieldsForCard(card: ReservationV4TaskCardResult): Reservation
|
||||
if (isVersionOnlyConfirmCard(card)) {
|
||||
return []
|
||||
}
|
||||
return card.card_type === 'ROOM_INFORMATION'
|
||||
? card.fields.filter((field) =>
|
||||
if (card.card_type === 'ROOM_INFORMATION') {
|
||||
return card.fields.filter((field) =>
|
||||
isReservationV4RoomInformationSafeField(field) && !isGroupRoomInformationBreakfastField(card, field),
|
||||
)
|
||||
: card.fields
|
||||
}
|
||||
if (isTraceCard(card)) {
|
||||
return card.fields.filter(isReservationV4TraceSafeField)
|
||||
}
|
||||
return card.fields
|
||||
}
|
||||
|
||||
function buildConfirmRequest(
|
||||
@@ -498,6 +503,10 @@ function isRoomingListCard(card: ReservationV4TaskCardResult): boolean {
|
||||
return card.card_type === 'ROOMING_LIST'
|
||||
}
|
||||
|
||||
function isTraceCard(card: ReservationV4TaskCardResult): boolean {
|
||||
return card.card_type === 'TRACE_RESERVATION_NOTES'
|
||||
}
|
||||
|
||||
function isVersionOnlyConfirmCard(card: ReservationV4TaskCardResult): boolean {
|
||||
return isPaymentCard(card) || isRoomingListCard(card)
|
||||
}
|
||||
@@ -538,6 +547,9 @@ function cardTitle(card: ReservationV4TaskCardResult): string {
|
||||
if (isRoomingListCard(card)) {
|
||||
return t('taskV4.roomingList.title')
|
||||
}
|
||||
if (isTraceCard(card)) {
|
||||
return t('taskV4.trace.title')
|
||||
}
|
||||
return formatReservationTaskCard(t, card.card_type)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user