统一前端时间按酒店时区显示 #1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
45
client/src/tests/reservationFormat.spec.ts
Normal file
45
client/src/tests/reservationFormat.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user