develop #2

Merged
huangting merged 2 commits from develop into master 2026-07-13 11:43:30 +08:00
5 changed files with 107 additions and 7 deletions
Showing only changes of commit 51ebc8e767 - Show all commits

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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')
})
})

View File

@@ -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}`
}

View File

@@ -217,23 +217,33 @@
<dl class="debug-eml-trace-fields">
<div v-if="trace.tool_name">
<dt>{{ t('debugEml.traceToolName') }}</dt>
<dd class="th-code">{{ trace.tool_name }}</dd>
<dd class="th-code">
{{ trace.tool_name }}
</dd>
</div>
<div v-if="trace.input_summary">
<dt>{{ t('debugEml.traceInputSummary') }}</dt>
<dd class="th-code">{{ trace.input_summary }}</dd>
<dd class="th-code">
{{ trace.input_summary }}
</dd>
</div>
<div v-if="trace.output_summary">
<dt>{{ t('debugEml.traceOutputSummary') }}</dt>
<dd class="th-code">{{ trace.output_summary }}</dd>
<dd class="th-code">
{{ trace.output_summary }}
</dd>
</div>
<div v-if="trace.status">
<dt>{{ t('debugEml.traceStatus') }}</dt>
<dd class="th-code">{{ trace.status }}</dd>
<dd class="th-code">
{{ trace.status }}
</dd>
</div>
<div v-if="trace.ts">
<dt>{{ t('debugEml.traceTime') }}</dt>
<dd class="th-code">{{ trace.ts }}</dd>
<dd class="th-code">
{{ formatReservationDateTime(trace.ts) }}
</dd>
</div>
</dl>
</li>
@@ -437,6 +447,7 @@ import type {
DebugEmlSuperAgentRunResult,
DebugEmlSuperAgentTraceEvent,
} from '@/types/debugEml'
import { formatReservationDateTime } from '@/utils/reservationFormat'
type DebugEmlStatus = 'idle' | 'uploading' | 'succeeded' | 'failed'