接入V4房型信息展示模型
This commit is contained in:
1 parent
1d593f05fd
commit
8d181ee8ad
21 files changed
+1390
-77
No files matched your search
@@ -0,0 +1,536 @@
|
||||
<template>
|
||||
<div class="room-information">
|
||||
<div
|
||||
v-if="!roomInformation"
|
||||
class="room-information__empty"
|
||||
>
|
||||
{{ t('taskV4.roomInformation.noDisplayModel') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="room-information__chips">
|
||||
<span>{{ eventType || '-' }}</span>
|
||||
<span>{{ bookingType || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="showChangeSummary"
|
||||
class="room-information__panel room-information__panel--changes"
|
||||
>
|
||||
<h3>{{ t('taskV4.roomInformation.changeSummary') }}</h3>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(change, index) in visibleChangeSummary"
|
||||
:key="`${change.field}-${index}`"
|
||||
>
|
||||
<span>{{ roomInformationFieldLabel(change.field) }}</span>
|
||||
<strong>{{ formatRoomInformationValue(change.field, change.before, currentValues) }}</strong>
|
||||
<em aria-hidden="true">→</em>
|
||||
<strong>{{ formatRoomInformationValue(change.field, change.after, finalValues) }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="showCurrentValues"
|
||||
class="room-information__panel"
|
||||
>
|
||||
<h3>{{ t('taskV4.roomInformation.currentValues') }}</h3>
|
||||
<dl
|
||||
v-if="scalarEntries(currentValues).length"
|
||||
class="room-information__grid"
|
||||
>
|
||||
<template
|
||||
v-for="entry in scalarEntries(currentValues)"
|
||||
:key="entry.key"
|
||||
>
|
||||
<dt>{{ roomInformationFieldLabel(entry.key) }}</dt>
|
||||
<dd>{{ entry.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p
|
||||
v-else-if="!roomItems(currentValues).length"
|
||||
class="room-information__empty"
|
||||
>
|
||||
{{ t('taskV4.roomInformation.emptyValues') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="roomItems(currentValues).length"
|
||||
class="room-information__room-items"
|
||||
>
|
||||
<h4>{{ t('taskV4.roomInformation.roomItems') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('taskV4.roomInformation.roomTypeCode') }}</th>
|
||||
<th>{{ t('taskV4.roomInformation.roomCount') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in roomItems(currentValues)"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ formatRoomInformationValue('room_type_code', item.room_type_code, item) }}</td>
|
||||
<td>{{ formatRoomInformationValue('room_count', item.room_count, item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="room-information__panel">
|
||||
<h3>{{ t('taskV4.roomInformation.finalValues') }}</h3>
|
||||
<div class="room-information__derived">
|
||||
<div
|
||||
v-if="hasValue(finalValues.nights)"
|
||||
class="room-information__derived-item"
|
||||
>
|
||||
<span>{{ t('taskV4.roomInformation.nights') }}</span>
|
||||
<strong>{{ formatRoomInformationValue('nights', finalValues.nights, finalValues) }}</strong>
|
||||
</div>
|
||||
<label
|
||||
v-if="showReadonlyBreakfast"
|
||||
class="room-information__derived-item room-information__derived-item--checkbox"
|
||||
>
|
||||
<span>{{ t('taskV4.roomInformation.breakfastIncluded') }}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="/room_information/final_values/breakfast_included"
|
||||
:checked="finalValues.breakfast_included === true"
|
||||
disabled
|
||||
>
|
||||
</label>
|
||||
<div
|
||||
v-if="hasValue(finalValues.group_booking_status) && !hasEditableField('/room_information/final_values/group_booking_status')"
|
||||
class="room-information__derived-item"
|
||||
>
|
||||
<span>{{ t('taskV4.roomInformation.groupBookingStatus') }}</span>
|
||||
<strong>{{ formatRoomInformationValue('group_booking_status', finalValues.group_booking_status, finalValues) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
v-if="showEditableFields"
|
||||
:fields="visibleFields"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<template v-else>
|
||||
<dl
|
||||
v-if="scalarEntries(finalValues).length"
|
||||
class="room-information__grid"
|
||||
>
|
||||
<template
|
||||
v-for="entry in scalarEntries(finalValues)"
|
||||
:key="entry.key"
|
||||
>
|
||||
<dt>{{ roomInformationFieldLabel(entry.key) }}</dt>
|
||||
<dd>{{ entry.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p
|
||||
v-else-if="!roomItems(finalValues).length"
|
||||
class="room-information__empty"
|
||||
>
|
||||
{{ t('taskV4.roomInformation.emptyValues') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="roomItems(finalValues).length"
|
||||
class="room-information__room-items"
|
||||
>
|
||||
<h4>{{ t('taskV4.roomInformation.roomItems') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('taskV4.roomInformation.roomTypeCode') }}</th>
|
||||
<th>{{ t('taskV4.roomInformation.roomCount') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in roomItems(finalValues)"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ formatRoomInformationValue('room_type_code', item.room_type_code, item) }}</td>
|
||||
<td>{{ formatRoomInformationValue('room_count', item.room_count, item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
||||
import type {
|
||||
ReservationRecord,
|
||||
ReservationV4RoomInformationDisplayModel,
|
||||
ReservationV4RoomInformationValues,
|
||||
ReservationV4TaskCardResult,
|
||||
} from '@/types/reservation'
|
||||
import {
|
||||
reservationV4FieldKey,
|
||||
isReservationV4RoomInformationSafeField,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
card: ReservationV4TaskCardResult
|
||||
modelValue: ReservationRecord
|
||||
readOnly: boolean
|
||||
editableKeys: string[]
|
||||
validationErrors?: Record<string, string>
|
||||
hotelId?: string
|
||||
}>(), {
|
||||
validationErrors: () => ({}),
|
||||
hotelId: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ReservationRecord]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const roomInformation = computed<ReservationV4RoomInformationDisplayModel | null>(() => {
|
||||
const payload = props.card.display_payload
|
||||
const value = isRecord(payload?.room_information) ? payload.room_information : null
|
||||
return value as ReservationV4RoomInformationDisplayModel | null
|
||||
})
|
||||
const eventType = computed(() => roomInformation.value?.event_type ?? props.card.event_type ?? '')
|
||||
const bookingType = computed(() => roomInformation.value?.booking_type ?? '')
|
||||
const currentValues = computed(() => normalizeValues(roomInformation.value?.current_values))
|
||||
const finalValues = computed(() => normalizeValues(roomInformation.value?.final_values))
|
||||
const changeSummary = computed(() => roomInformation.value?.change_summary ?? [])
|
||||
const visibleChangeSummary = computed(() => changeSummary.value.filter((change) =>
|
||||
isVisibleRoomInformationChangeField(change.field),
|
||||
))
|
||||
const visibleFields = computed(() => props.card.fields.filter((field) =>
|
||||
isReservationV4RoomInformationSafeField(field) && isVisibleRoomInformationField(field),
|
||||
))
|
||||
const showChangeSummary = computed(() => eventType.value === 'UPDATE_BOOKING' && visibleChangeSummary.value.length > 0)
|
||||
const showCurrentValues = computed(() =>
|
||||
['CANCEL_BOOKING'].includes(eventType.value) && hasVisibleValues(currentValues.value),
|
||||
)
|
||||
const showEditableFields = computed(() => eventType.value !== 'CANCEL_BOOKING' && visibleFields.value.length > 0)
|
||||
const showReadonlyBreakfast = computed(() =>
|
||||
hasValue(finalValues.value.breakfast_included) &&
|
||||
(bookingType.value === 'GROUP' || !hasEditableField('/room_information/final_values/breakfast_included')),
|
||||
)
|
||||
|
||||
const scalarFieldOrder = [
|
||||
'group_block_name',
|
||||
'fit_name',
|
||||
'arrival_date',
|
||||
'departure_date',
|
||||
'nights',
|
||||
'rate_code',
|
||||
'breakfast_included',
|
||||
'group_booking_status',
|
||||
'block_id',
|
||||
'confirmation_number',
|
||||
]
|
||||
|
||||
function scalarEntries(values: ReservationV4RoomInformationValues): Array<{ key: string; value: string }> {
|
||||
return scalarFieldOrder
|
||||
.filter((key) => key in values && hasValue(values[key]))
|
||||
.filter((key) => key !== 'group_booking_status_label')
|
||||
.filter((key) => key !== 'breakfast_included' || !hasEditableField('/room_information/final_values/breakfast_included'))
|
||||
.map((key) => ({
|
||||
key,
|
||||
value: formatRoomInformationValue(key, values[key], values),
|
||||
}))
|
||||
}
|
||||
|
||||
function roomItems(values: ReservationV4RoomInformationValues): ReservationRecord[] {
|
||||
return Array.isArray(values.room_items)
|
||||
? values.room_items.filter((item): item is ReservationRecord => isRecord(item))
|
||||
: []
|
||||
}
|
||||
|
||||
function hasEditableField(pointer: string): boolean {
|
||||
return visibleFields.value.some((field) => reservationV4FieldKey(field) === pointer)
|
||||
}
|
||||
|
||||
function isVisibleRoomInformationField(field: ReservationV4TaskCardResult['fields'][number]): boolean {
|
||||
const key = reservationV4FieldKey(field)
|
||||
return !(bookingType.value === 'GROUP' && key === '/room_information/final_values/breakfast_included')
|
||||
}
|
||||
|
||||
function isVisibleRoomInformationChangeField(field: unknown): boolean {
|
||||
const key = typeof field === 'string' ? field.toLowerCase() : ''
|
||||
return key !== 'adult' &&
|
||||
key !== 'adults' &&
|
||||
!key.endsWith('.adult') &&
|
||||
!key.endsWith('.adults') &&
|
||||
!key.endsWith('/adult') &&
|
||||
!key.endsWith('/adults') &&
|
||||
!key.includes('target_order') &&
|
||||
!key.includes('locator_value')
|
||||
}
|
||||
|
||||
function hasVisibleValues(values: ReservationV4RoomInformationValues): boolean {
|
||||
return scalarEntries(values).length > 0 || roomItems(values).length > 0
|
||||
}
|
||||
|
||||
function normalizeValues(value: unknown): ReservationV4RoomInformationValues {
|
||||
return isRecord(value) ? value : {}
|
||||
}
|
||||
|
||||
function roomInformationFieldLabel(field: unknown): string {
|
||||
const key = typeof field === 'string' ? field : ''
|
||||
const labelKey = roomInformationLabelKeys[key]
|
||||
return labelKey ? t(labelKey) : key || '-'
|
||||
}
|
||||
|
||||
function formatRoomInformationValue(
|
||||
field: string,
|
||||
value: unknown,
|
||||
values: ReservationRecord,
|
||||
): string {
|
||||
if (field === 'group_booking_status') {
|
||||
const statusLabel = values.group_booking_status_label
|
||||
if (typeof statusLabel === 'string' && statusLabel.trim()) {
|
||||
return statusLabel
|
||||
}
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? t('taskV4.roomInformation.yes') : t('taskV4.roomInformation.no')
|
||||
}
|
||||
return stringifyReservationV4SafeDisplayValue(
|
||||
value,
|
||||
t('taskV4.field.empty'),
|
||||
t('taskV4.field.hidden'),
|
||||
)
|
||||
}
|
||||
|
||||
function hasValue(value: unknown): boolean {
|
||||
if (value === null || value === undefined) {
|
||||
return false
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length > 0
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is ReservationRecord {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||
}
|
||||
|
||||
const roomInformationLabelKeys: Record<string, string> = {
|
||||
group_block_name: 'taskV4.roomInformation.groupBlockName',
|
||||
fit_name: 'taskV4.roomInformation.fitName',
|
||||
arrival_date: 'taskV4.roomInformation.arrivalDate',
|
||||
departure_date: 'taskV4.roomInformation.departureDate',
|
||||
nights: 'taskV4.roomInformation.nights',
|
||||
rate_code: 'taskV4.roomInformation.rateCode',
|
||||
breakfast_included: 'taskV4.roomInformation.breakfastIncluded',
|
||||
group_booking_status: 'taskV4.roomInformation.groupBookingStatus',
|
||||
block_id: 'taskV4.roomInformation.blockId',
|
||||
confirmation_number: 'taskV4.roomInformation.confirmationNumber',
|
||||
room_items: 'taskV4.roomInformation.roomItems',
|
||||
room_type_code: 'taskV4.roomInformation.roomTypeCode',
|
||||
room_count: 'taskV4.roomInformation.roomCount',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.room-information {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.room-information__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.room-information__chips span {
|
||||
border-radius: 999px;
|
||||
background: var(--th-color-info-bg);
|
||||
color: var(--th-color-blue-700);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
padding: 4px 9px;
|
||||
}
|
||||
|
||||
.room-information__panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.room-information__panel h3,
|
||||
.room-information__room-items h4 {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.room-information__panel--changes {
|
||||
border-color: color-mix(in srgb, var(--th-color-blue-600) 28%, var(--th-color-slate-200));
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-info-bg));
|
||||
}
|
||||
|
||||
.room-information__panel--changes ul {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.room-information__panel--changes li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 13px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.room-information__panel--changes strong {
|
||||
color: var(--th-color-slate-900);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.room-information__panel--changes em {
|
||||
color: var(--th-color-blue-600);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.room-information__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.room-information__grid dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.room-information__grid dd {
|
||||
margin: 4px 0 0;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.room-information__derived {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.room-information__derived-item {
|
||||
display: inline-grid;
|
||||
gap: 4px;
|
||||
min-width: 120px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 9px 11px;
|
||||
}
|
||||
|
||||
.room-information__derived-item span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.room-information__derived-item strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.room-information__derived-item--checkbox {
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.room-information__derived-item--checkbox span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.room-information__derived-item--checkbox input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
.room-information__room-items {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.room-information__room-items table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.room-information__room-items th,
|
||||
.room-information__room-items td {
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
padding: 9px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.room-information__room-items th {
|
||||
background: var(--th-color-slate-50);
|
||||
color: var(--th-color-slate-500);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.room-information__room-items td {
|
||||
color: var(--th-color-slate-900);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.room-information__room-items tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.room-information__empty {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.room-information__grid,
|
||||
.room-information__panel--changes li {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@
|
||||
<select
|
||||
v-if="isEditable(field) && isSelectField(field)"
|
||||
class="v4-field__control"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:disabled="selectDisabled(field)"
|
||||
:value="fieldValue(field)"
|
||||
@@ -34,15 +35,27 @@
|
||||
v-else-if="isEditable(field) && isDateField(field)"
|
||||
class="v4-field__control"
|
||||
type="date"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field) && isCheckboxField(field)"
|
||||
class="v4-field__control v4-field__control--checkbox"
|
||||
type="checkbox"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:checked="checkboxValue(field)"
|
||||
@change="updateCheckboxField(field, $event)"
|
||||
>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field) && isNumberField(field)"
|
||||
class="v4-field__control"
|
||||
type="number"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
@@ -51,6 +64,7 @@
|
||||
<textarea
|
||||
v-else-if="isEditable(field) && isTextAreaField(field)"
|
||||
class="v4-field__control v4-field__control--textarea"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
rows="3"
|
||||
@@ -61,16 +75,26 @@
|
||||
v-else-if="isEditable(field)"
|
||||
class="v4-field__control"
|
||||
type="text"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<input
|
||||
v-else-if="isCheckboxField(field)"
|
||||
class="v4-field__control v4-field__control--checkbox"
|
||||
type="checkbox"
|
||||
:name="fieldKey(field)"
|
||||
:checked="checkboxValue(field)"
|
||||
disabled
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="v4-field__static"
|
||||
>
|
||||
{{ stringifyV4Value(fieldValue(field)) }}
|
||||
{{ staticFieldValue(field) }}
|
||||
</span>
|
||||
|
||||
<small
|
||||
@@ -209,13 +233,18 @@ function isEditable(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
}
|
||||
|
||||
function isSelectField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return isSelectControl(field) && Boolean(reservationV4LookupKindForOptionsSource(field.options_source))
|
||||
return isSelectControl(field) &&
|
||||
(Boolean(reservationV4LookupKindForOptionsSource(field.options_source)) || fixedFieldOptions(field).length > 0)
|
||||
}
|
||||
|
||||
function isDateField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'DATE'
|
||||
}
|
||||
|
||||
function isCheckboxField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return ['CHECKBOX', 'BOOLEAN'].includes(normalizeControlType(field.control_type))
|
||||
}
|
||||
|
||||
function isNumberField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'NUMBER'
|
||||
}
|
||||
@@ -232,7 +261,7 @@ function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
label: lookupItemLabel(item),
|
||||
item,
|
||||
}))
|
||||
: []
|
||||
: fixedFieldOptions(field)
|
||||
const exactItem = exactLookupItemForField(field)
|
||||
if (exactItem && !options.some((option) => option.value === exactItem.code)) {
|
||||
options.push({
|
||||
@@ -267,6 +296,17 @@ function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
||||
return stringifyV4Value(value)
|
||||
}
|
||||
|
||||
function staticFieldValue(field: ReservationV4TaskCardFieldResult): string {
|
||||
const value = fieldValue(field)
|
||||
const matchedOption = fieldOptions(field).find((option) => option.value === value)
|
||||
return matchedOption?.label ?? stringifyV4Value(value)
|
||||
}
|
||||
|
||||
function checkboxValue(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const value = readV4FieldValue(field, props.modelValue)
|
||||
return value === true || value === 'true' || value === 1 || value === '1'
|
||||
}
|
||||
|
||||
function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
||||
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
delete invalidLookupValues[reservationV4FieldKey(field)]
|
||||
@@ -276,6 +316,15 @@ function updateField(field: ReservationV4TaskCardFieldResult, event: Event): voi
|
||||
})
|
||||
}
|
||||
|
||||
function updateCheckboxField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
||||
const target = event.target as HTMLInputElement
|
||||
delete invalidLookupValues[reservationV4FieldKey(field)]
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[reservationV4FieldKey(field)]: target.checked,
|
||||
})
|
||||
}
|
||||
|
||||
function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] {
|
||||
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
||||
if (!kind) {
|
||||
@@ -389,6 +438,27 @@ function isSelectControl(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return ['SELECT', 'LOOKUP'].includes(normalizeControlType(field.control_type))
|
||||
}
|
||||
|
||||
function fixedFieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
const optionsSource = normalizeControlType(field.options_source)
|
||||
if (optionsSource !== 'RESERVATION_V4_GROUP_BOOKING_STATUS_FIXED') {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
value: 'TEN',
|
||||
label: 'TEN-Tentative',
|
||||
},
|
||||
{
|
||||
value: 'DEF',
|
||||
label: 'DEF-Definite',
|
||||
},
|
||||
{
|
||||
value: 'INQ',
|
||||
label: 'INQ-Inquiry',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
||||
if (item.display_name && item.display_name !== item.code) {
|
||||
return `${item.code} - ${item.display_name}`
|
||||
@@ -629,6 +699,14 @@ function exactLookupKey(kind: ReservationV4LookupKind, code: string): string {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.v4-field__control--checkbox {
|
||||
min-height: 18px;
|
||||
width: 18px;
|
||||
accent-color: var(--th-color-blue-600);
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.v4-field__control[aria-invalid='true'] {
|
||||
border-color: var(--th-color-danger);
|
||||
box-shadow: 0 0 0 3px var(--th-color-danger-bg);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<section class="th-section task-card-section">
|
||||
<section
|
||||
class="th-section task-card-section"
|
||||
:data-testid="isRoomInformationCard ? 'room-information-card' : undefined"
|
||||
>
|
||||
<div class="task-card-section__header">
|
||||
<div class="task-card-section__heading">
|
||||
<h2 class="th-section-title">
|
||||
@@ -31,7 +34,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReservationV4RoomInformationCard
|
||||
v-if="isRoomInformationCard"
|
||||
class="task-card-section__fields"
|
||||
: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"
|
||||
:fields="card.fields"
|
||||
:model-value="modelValue"
|
||||
@@ -43,7 +58,7 @@
|
||||
/>
|
||||
|
||||
<details
|
||||
v-if="safePayloadRows.length"
|
||||
v-if="!isRoomInformationCard && safePayloadRows.length"
|
||||
class="safe-payload"
|
||||
>
|
||||
<summary>
|
||||
@@ -86,6 +101,7 @@
|
||||
<label>
|
||||
<span>{{ t('taskV4.confirmedOrderId') }}</span>
|
||||
<input
|
||||
name="v4_confirmed_order_id"
|
||||
:value="reviewForm.confirmed_order_id"
|
||||
:placeholder="t('taskV4.confirmedOrderIdPlaceholder')"
|
||||
@input="updateReviewForm('confirmed_order_id', $event)"
|
||||
@@ -107,7 +123,7 @@
|
||||
:disabled="!canReview"
|
||||
@click="emit('resolveReview')"
|
||||
>
|
||||
{{ submitting ? t('taskV4.resolvingReview') : t('taskV4.resolveReview') }}
|
||||
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
|
||||
</button>
|
||||
<RouterLink
|
||||
v-if="boundOrderId"
|
||||
@@ -148,6 +164,7 @@ import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import ReservationV4RoomInformationCard from '@/components/reservation/ReservationV4RoomInformationCard.vue'
|
||||
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
||||
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
|
||||
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
|
||||
@@ -198,6 +215,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isRoomInformationCard = computed(() => props.card.card_type === 'ROOM_INFORMATION')
|
||||
|
||||
const availabilityReason = computed(() => {
|
||||
return props.card.availability.readonly_reason_message ??
|
||||
|
||||
Reference in new issue
Block a user