diff --git a/client/src/components/common/LocalizedDatePicker.vue b/client/src/components/common/LocalizedDatePicker.vue new file mode 100644 index 0000000..b00e863 --- /dev/null +++ b/client/src/components/common/LocalizedDatePicker.vue @@ -0,0 +1,124 @@ + + + + + + + diff --git a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue index 8ef772b..f665dd6 100644 --- a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue +++ b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue @@ -31,15 +31,16 @@ - + :test-id="fieldKey(field)" + :placeholder="t('taskV4.field.datePlaceholder')" + :invalid="Boolean(fieldError(field))" + @update:model-value="updateFieldValue(field, $event)" + /> option.value === value) @@ -309,10 +320,14 @@ function checkboxValue(field: ReservationV4TaskCardFieldResult): boolean { function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void { const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + updateFieldValue(field, target.value) +} + +function updateFieldValue(field: ReservationV4TaskCardFieldResult, value: string): void { delete invalidLookupValues[reservationV4FieldKey(field)] emit('update:modelValue', { ...props.modelValue, - [reservationV4FieldKey(field)]: target.value, + [reservationV4FieldKey(field)]: value, }) } diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts index 345ec69..2a98e4e 100644 --- a/client/src/i18n/locales/en-US.ts +++ b/client/src/i18n/locales/en-US.ts @@ -698,6 +698,7 @@ export default { empty: 'Empty', hidden: 'Sensitive content hidden', selectPlaceholder: 'Select', + datePlaceholder: 'yyyy-mm-dd', }, error: { loadFailed: 'Failed to load the reservation item handling page.', diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts index 8f05cbb..f27087c 100644 --- a/client/src/i18n/locales/th-TH.ts +++ b/client/src/i18n/locales/th-TH.ts @@ -698,6 +698,7 @@ export default { empty: 'ยังไม่กรอก', hidden: 'ซ่อนข้อมูลที่อ่อนไหวแล้ว', selectPlaceholder: 'เลือก', + datePlaceholder: 'yyyy-mm-dd', }, error: { loadFailed: 'โหลดหน้าจัดการรายการจองไม่สำเร็จ', diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index 73b2ea3..19a2ece 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -698,6 +698,7 @@ export default { empty: '未填写', hidden: '敏感内容已隐藏', selectPlaceholder: '请选择', + datePlaceholder: '年-月-日', }, error: { loadFailed: '订单事项办理页加载失败。', diff --git a/client/src/tests/reservationV4Views.spec.ts b/client/src/tests/reservationV4Views.spec.ts index b1d3170..9ef2b36 100644 --- a/client/src/tests/reservationV4Views.spec.ts +++ b/client/src/tests/reservationV4Views.spec.ts @@ -1,5 +1,6 @@ import { flushPromises, mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' +import PrimeVue from 'primevue/config' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import { createMemoryHistory, createRouter } from 'vue-router' @@ -40,6 +41,20 @@ const service = await import('@/services/reservationService') describe('reservation V4 pages', () => { beforeEach(() => { sessionStorage.clear() + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }) vi.mocked(service.ackReservationV4SourceNotification).mockReset() vi.mocked(service.confirmReservationV4OrderTaskCard).mockReset() vi.mocked(service.fetchReservationV4AccountLookups).mockReset() @@ -780,8 +795,10 @@ describe('reservation V4 pages', () => { expect((breakfast.element as HTMLInputElement).disabled).toBe(true) expect((roomCard.find('input[name="/room_information/final_values/group_block_name"]').element as HTMLInputElement).value) .toBe('GRP-V4-RI-GROUP-001') + expect(roomCard.find('input[name="/room_information/final_values/arrival_date"]').attributes('type')).toBe('text') expect((roomCard.find('input[name="/room_information/final_values/arrival_date"]').element as HTMLInputElement).value) .toBe('2026-07-26') + expect(roomCard.find('input[name="/room_information/final_values/departure_date"]').attributes('type')).toBe('text') expect((roomCard.find('input[name="/room_information/final_values/departure_date"]').element as HTMLInputElement).value) .toBe('2026-07-29') @@ -1405,7 +1422,7 @@ async function mountWithPlugins(component: object, initialPath: string) { return mount(component, { global: { - plugins: [pinia, i18n, router], + plugins: [pinia, i18n, router, PrimeVue], stubs: { RouterLink: { template: '', diff --git a/client/src/utils/localizedDatePicker.ts b/client/src/utils/localizedDatePicker.ts new file mode 100644 index 0000000..ba72c1f --- /dev/null +++ b/client/src/utils/localizedDatePicker.ts @@ -0,0 +1,130 @@ +import type { PrimeVueLocaleOptions } from 'primevue/config' + +export type SupportedDatePickerLocale = 'zh-CN' | 'en-US' | 'th-TH' + +export type LocalizedDatePickerLocaleOptions = Pick< + PrimeVueLocaleOptions, + 'fileSizeTypes' | 'dayNames' | 'dayNamesShort' | 'dayNamesMin' | 'monthNames' | 'monthNamesShort' +> & + Partial + +export const primeVueDatePickerLocales: Record = { + 'zh-CN': { + fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'], + dayNamesShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'], + dayNamesMin: ['日', '一', '二', '三', '四', '五', '六'], + monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], + monthNamesShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + chooseDate: '选择日期', + chooseMonth: '选择月份', + chooseYear: '选择年份', + prevMonth: '上个月', + nextMonth: '下个月', + prevYear: '上一年', + nextYear: '下一年', + today: '今天', + clear: '清除', + firstDayOfWeek: 1, + showMonthAfterYear: true, + dateFormat: 'yy-mm-dd', + }, + 'en-US': { + fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + monthNames: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ], + monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + chooseDate: 'Choose date', + chooseMonth: 'Choose month', + chooseYear: 'Choose year', + prevMonth: 'Previous month', + nextMonth: 'Next month', + prevYear: 'Previous year', + nextYear: 'Next year', + today: 'Today', + clear: 'Clear', + firstDayOfWeek: 0, + showMonthAfterYear: false, + dateFormat: 'yy-mm-dd', + }, + 'th-TH': { + fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + dayNames: ['วันอาทิตย์', 'วันจันทร์', 'วันอังคาร', 'วันพุธ', 'วันพฤหัสบดี', 'วันศุกร์', 'วันเสาร์'], + dayNamesShort: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'], + dayNamesMin: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'], + monthNames: [ + 'มกราคม', + 'กุมภาพันธ์', + 'มีนาคม', + 'เมษายน', + 'พฤษภาคม', + 'มิถุนายน', + 'กรกฎาคม', + 'สิงหาคม', + 'กันยายน', + 'ตุลาคม', + 'พฤศจิกายน', + 'ธันวาคม', + ], + monthNamesShort: ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'], + chooseDate: 'เลือกวันที่', + chooseMonth: 'เลือกเดือน', + chooseYear: 'เลือกปี', + prevMonth: 'เดือนก่อนหน้า', + nextMonth: 'เดือนถัดไป', + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + today: 'วันนี้', + clear: 'ล้าง', + firstDayOfWeek: 1, + showMonthAfterYear: false, + dateFormat: 'yy-mm-dd', + }, +} + +export function normalizeDatePickerLocale(nextLocale: string): SupportedDatePickerLocale { + if (nextLocale === 'en-US' || nextLocale === 'th-TH') { + return nextLocale + } + return 'zh-CN' +} + +export function parseDatePickerValue(value: string): Date | null { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) + if (!match) { + return null + } + const year = Number(match[1]) + const month = Number(match[2]) + const day = Number(match[3]) + const date = new Date(year, month - 1, day) + if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) { + return null + } + return date +} + +export function formatDatePickerValue(value: Date | null | undefined): string { + if (!(value instanceof Date) || Number.isNaN(value.getTime())) { + return '' + } + const year = value.getFullYear() + const month = String(value.getMonth() + 1).padStart(2, '0') + const day = String(value.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} diff --git a/client/src/views/reservation/ReservationManualInvoiceView.vue b/client/src/views/reservation/ReservationManualInvoiceView.vue index 8578657..02f3104 100644 --- a/client/src/views/reservation/ReservationManualInvoiceView.vue +++ b/client/src/views/reservation/ReservationManualInvoiceView.vue @@ -201,20 +201,14 @@ >Booking Date
- @@ -274,20 +268,14 @@ >Arrival Date
-
@@ -309,20 +297,14 @@ >Departure Date
-
@@ -344,20 +326,14 @@ >Due Date
- @@ -812,11 +788,10 @@