实现房表生成第二种名单样式

This commit is contained in:
andy committed 2026-07-24 11:55:29 +07:00
1 parent 14eda89b0b
commit eaea8787d9
22 files changed
+645 -64

No files matched your search

@@ -116,6 +116,56 @@
{{ fieldError('roomType') }}
</span>
</label>
<div class="field">
<label
for="rooming-list-arrival-input"
class="field-label"
>
{{ t('roomingList.arrival') }}
</label>
<LocalizedDatePicker
v-model="form.arrival"
input-id="rooming-list-arrival-input"
test-id="rooming-list-arrival"
:placeholder="t('roomingList.datePlaceholder')"
:input-class="validationClass('arrival')"
:invalid="Boolean(fieldError('arrival'))"
:described-by="fieldError('arrival') ? fieldErrorId('arrival') : undefined"
/>
<span
v-if="fieldError('arrival')"
:id="fieldErrorId('arrival')"
class="field-error"
>
{{ fieldError('arrival') }}
</span>
</div>
<div class="field">
<label
for="rooming-list-departure-input"
class="field-label"
>
{{ t('roomingList.departure') }}
</label>
<LocalizedDatePicker
v-model="form.departure"
input-id="rooming-list-departure-input"
test-id="rooming-list-departure"
:placeholder="t('roomingList.datePlaceholder')"
:input-class="validationClass('departure')"
:invalid="Boolean(fieldError('departure'))"
:described-by="fieldError('departure') ? fieldErrorId('departure') : undefined"
/>
<span
v-if="fieldError('departure')"
:id="fieldErrorId('departure')"
class="field-error"
>
{{ fieldError('departure') }}
</span>
</div>
</div>
</section>
@@ -252,6 +302,7 @@
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import LocalizedDatePicker from '@/components/common/LocalizedDatePicker.vue'
import {
generateReservationRoomingListExcel,
RoomingListGenerationError,
@@ -264,6 +315,8 @@ type AlertTone = 'success' | 'error'
type RoomingListFieldKey =
| 'file'
| 'peoplePerRoom'
| 'arrival'
| 'departure'
| 'roomType'
| 'paymentType'
| 'nationality'
@@ -277,6 +330,8 @@ const authStore = useAuthStore()
const form = reactive({
file: null as File | null,
peoplePerRoom: '',
arrival: '',
departure: '',
roomType: '',
paymentType: 'BTQR',
nationality: '',
@@ -340,6 +395,8 @@ function buildGenerationInput(file: File): RoomingListGenerationInput {
file,
hotelId: authStore.selectedHotelId,
peoplePerRoom: Number(form.peoplePerRoom),
arrival: form.arrival,
departure: form.departure,
roomType: form.roomType,
paymentType: form.paymentType,
nationality: form.nationality,
@@ -387,6 +444,21 @@ function collectFieldErrors(): FieldErrors {
if (!isPositiveInteger(form.peoplePerRoom)) {
errors.peoplePerRoom = t('roomingList.fieldErrors.positiveInteger')
}
const arrival = form.arrival.trim()
const departure = form.departure.trim()
if (arrival && !isDateInputValue(arrival)) {
errors.arrival = t('roomingList.fieldErrors.dateFormat')
}
if (departure && !isDateInputValue(departure)) {
errors.departure = t('roomingList.fieldErrors.dateFormat')
}
if (arrival && !departure) {
errors.departure = t('roomingList.fieldErrors.departureRequired')
} else if (!arrival && departure) {
errors.arrival = t('roomingList.fieldErrors.arrivalRequired')
} else if (arrival && departure && isDateInputValue(arrival) && isDateInputValue(departure) && departure <= arrival) {
errors.departure = t('roomingList.fieldErrors.departureAfterArrival')
}
if (!form.roomType.trim()) {
errors.roomType = t('roomingList.fieldErrors.roomTypeRequired')
}
@@ -438,6 +510,14 @@ function isPositiveInteger(value: unknown): boolean {
return /^[1-9]\d*$/.test(normalizeInputText(value))
}
function isDateInputValue(value: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return false
}
const date = new Date(`${value}T00:00:00Z`)
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
}
function normalizeInputText(value: unknown): string {
if (typeof value === 'string') {
return value.trim()