diff --git a/client/src/config/reservationConfig.ts b/client/src/config/reservationConfig.ts index 9bf9b01..cc566fe 100644 --- a/client/src/config/reservationConfig.ts +++ b/client/src/config/reservationConfig.ts @@ -1,14 +1,25 @@ const configuredReservationHotelId = import.meta.env.VITE_RESERVATION_HOTEL_ID?.trim() export const reservationHotelId = configuredReservationHotelId || null +export const reservationTimeZone = 'Asia/Bangkok' let reservationHotelIdProvider: (() => string | null | undefined) | null = null +let reservationTimeZoneProvider: (() => string | null | undefined) | null = null export function setReservationHotelIdProvider(provider: (() => string | null | undefined) | null): void { reservationHotelIdProvider = provider } +export function setReservationTimeZoneProvider(provider: (() => string | null | undefined) | null): void { + reservationTimeZoneProvider = provider +} + export function getReservationHotelId(): string | null { const providedHotelId = reservationHotelIdProvider?.()?.trim() return providedHotelId || reservationHotelId } + +export function getReservationTimeZone(): string { + const providedTimeZone = reservationTimeZoneProvider?.()?.trim() + return providedTimeZone || reservationTimeZone +} diff --git a/client/src/main.ts b/client/src/main.ts index 84a5ef4..83e68d8 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -6,7 +6,10 @@ import { VueQueryPlugin } from '@tanstack/vue-query' import 'primeicons/primeicons.css' import App from './App.vue' -import { setReservationHotelIdProvider } from './config/reservationConfig' +import { + setReservationHotelIdProvider, + setReservationTimeZoneProvider, +} from './config/reservationConfig' import { i18n } from './i18n' import { router } from './router' import { setUnauthorizedHandler } from './services/httpClient' @@ -18,6 +21,7 @@ const pinia = createPinia() const authStore = useAuthStore(pinia) setReservationHotelIdProvider(() => authStore.selectedHotelId) +setReservationTimeZoneProvider(() => authStore.selectedHotel?.time_zone) setUnauthorizedHandler(() => { const currentPath = router.currentRoute.value.fullPath authStore.clearAuth() diff --git a/client/src/tests/reservationFormat.spec.ts b/client/src/tests/reservationFormat.spec.ts new file mode 100644 index 0000000..f6b7429 --- /dev/null +++ b/client/src/tests/reservationFormat.spec.ts @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, it } from 'vitest' + +import { + setReservationTimeZoneProvider, +} from '@/config/reservationConfig' +import { + formatReservationDateRange, + formatReservationDateTime, +} from '@/utils/reservationFormat' + +describe('reservationFormat', () => { + afterEach(() => { + setReservationTimeZoneProvider(null) + }) + + it('formats UTC API timestamps in the default hotel time zone', () => { + expect(formatReservationDateTime('2026-07-09T05:56:56Z')).toBe('2026-07-09 12:56:56') + }) + + it('formats timestamps in the selected hotel time zone', () => { + setReservationTimeZoneProvider(() => 'Asia/Tokyo') + + expect(formatReservationDateTime('2026-07-09T05:56:56Z')).toBe('2026-07-09 14:56:56') + }) + + it('honors offsets from source timestamps before converting to hotel time', () => { + expect(formatReservationDateTime('2026-07-08T09:18:00+08:00')).toBe('2026-07-08 08:18:00') + }) + + it('falls back to the default hotel time zone when the selected hotel timezone is invalid', () => { + setReservationTimeZoneProvider(() => 'Invalid/Timezone') + + expect(formatReservationDateTime('2026-07-09T05:56:56Z')).toBe('2026-07-09 12:56:56') + }) + + it('falls back safely for empty or invalid timestamps', () => { + expect(formatReservationDateTime(null)).toBe('-') + expect(formatReservationDateTime(undefined)).toBe('-') + expect(formatReservationDateTime('not-a-date')).toBe('not-a-date') + }) + + it('keeps hotel business date ranges as local dates without timezone conversion', () => { + expect(formatReservationDateRange('2026-07-09', '2026-07-10')).toBe('2026-07-09 / 2026-07-10') + }) +}) diff --git a/client/src/utils/reservationFormat.ts b/client/src/utils/reservationFormat.ts index 230d106..f962888 100644 --- a/client/src/utils/reservationFormat.ts +++ b/client/src/utils/reservationFormat.ts @@ -1,8 +1,14 @@ +import { getReservationTimeZone, reservationTimeZone } from '@/config/reservationConfig' + export function formatReservationDateTime(value: string | null | undefined): string { if (!value) { return '-' } - return value.replace('T', ' ').replace(/\+.*$/, '') + const date = new Date(value) + if (Number.isNaN(date.getTime())) { + return value + } + return formatDateTimeInTimeZone(date, getReservationTimeZone()) } export function formatReservationDateRange(arrivalDate: string, departureDate: string): string { @@ -16,3 +22,26 @@ export function formatReservationCurrency(amount: number, currencyCode: string): maximumFractionDigits: 0, }).format(amount) } + +function formatDateTimeInTimeZone(date: Date, timeZone: string): string { + try { + return formatDateTimeParts(date, timeZone) + } catch { + return formatDateTimeParts(date, reservationTimeZone) + } +} + +function formatDateTimeParts(date: Date, timeZone: string): string { + const parts = new Intl.DateTimeFormat('en-GB', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23', + }).formatToParts(date) + const valueByType = Object.fromEntries(parts.map((part) => [part.type, part.value])) + return `${valueByType.year}-${valueByType.month}-${valueByType.day} ${valueByType.hour}:${valueByType.minute}:${valueByType.second}` +} diff --git a/client/src/views/debug/DebugEmlSuperAgentRunView.vue b/client/src/views/debug/DebugEmlSuperAgentRunView.vue index 2d32d37..3000747 100644 --- a/client/src/views/debug/DebugEmlSuperAgentRunView.vue +++ b/client/src/views/debug/DebugEmlSuperAgentRunView.vue @@ -217,23 +217,33 @@
{{ t('debugEml.traceToolName') }}
-
{{ trace.tool_name }}
+
+ {{ trace.tool_name }} +
{{ t('debugEml.traceInputSummary') }}
-
{{ trace.input_summary }}
+
+ {{ trace.input_summary }} +
{{ t('debugEml.traceOutputSummary') }}
-
{{ trace.output_summary }}
+
+ {{ trace.output_summary }} +
{{ t('debugEml.traceStatus') }}
-
{{ trace.status }}
+
+ {{ trace.status }} +
{{ t('debugEml.traceTime') }}
-
{{ trace.ts }}
+
+ {{ formatReservationDateTime(trace.ts) }} +
@@ -437,6 +447,7 @@ import type { DebugEmlSuperAgentRunResult, DebugEmlSuperAgentTraceEvent, } from '@/types/debugEml' +import { formatReservationDateTime } from '@/utils/reservationFormat' type DebugEmlStatus = 'idle' | 'uploading' | 'succeeded' | 'failed'