diff --git a/frontend/src/components/common/DateRangePicker.vue b/frontend/src/components/common/DateRangePicker.vue index b116dff2e3..a2af52de9f 100644 --- a/frontend/src/components/common/DateRangePicker.vue +++ b/frontend/src/components/common/DateRangePicker.vue @@ -43,7 +43,7 @@ @@ -57,7 +57,7 @@ type="date" v-model="localEndDate" :min="localStartDate" - :max="tomorrow" + :max="tomorrow()" class="date-picker-input" @change="onDateChange" /> @@ -108,22 +108,15 @@ const localStartDate = ref(props.startDate) const localEndDate = ref(props.endDate) const activePreset = ref('last24Hours') -const today = computed(() => { - // Use local timezone to avoid UTC timezone issues - const now = new Date() - const year = now.getFullYear() - const month = String(now.getMonth() + 1).padStart(2, '0') - const day = String(now.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` -}) +const today = () => formatDateToString(new Date()) // Tomorrow's date - used for max date to handle timezone differences // When user is in a timezone behind the server, "today" on server might be "tomorrow" locally -const tomorrow = computed(() => { +const tomorrow = () => { const d = new Date() d.setDate(d.getDate() + 1) return formatDateToString(d) -}) +} // Helper function to format date to YYYY-MM-DD using local timezone const formatDateToString = (date: Date): string => { @@ -138,7 +131,7 @@ const presets: DatePreset[] = [ labelKey: 'dates.today', value: 'today', getRange: () => { - const t = today.value + const t = today() return { start: t, end: t } } }, @@ -168,7 +161,7 @@ const presets: DatePreset[] = [ labelKey: 'dates.last7Days', value: '7days', getRange: () => { - const end = today.value + const end = today() const d = new Date() d.setDate(d.getDate() - 6) const start = formatDateToString(d) @@ -179,7 +172,7 @@ const presets: DatePreset[] = [ labelKey: 'dates.last14Days', value: '14days', getRange: () => { - const end = today.value + const end = today() const d = new Date() d.setDate(d.getDate() - 13) const start = formatDateToString(d) @@ -190,7 +183,7 @@ const presets: DatePreset[] = [ labelKey: 'dates.last30Days', value: '30days', getRange: () => { - const end = today.value + const end = today() const d = new Date() d.setDate(d.getDate() - 29) const start = formatDateToString(d) @@ -203,7 +196,7 @@ const presets: DatePreset[] = [ getRange: () => { const now = new Date() const start = formatDateToString(new Date(now.getFullYear(), now.getMonth(), 1)) - return { start, end: today.value } + return { start, end: today() } } }, { diff --git a/frontend/src/components/common/__tests__/DateRangePicker.midnight.spec.ts b/frontend/src/components/common/__tests__/DateRangePicker.midnight.spec.ts new file mode 100644 index 0000000000..79e2404564 --- /dev/null +++ b/frontend/src/components/common/__tests__/DateRangePicker.midnight.spec.ts @@ -0,0 +1,33 @@ +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' +import DateRangePicker from '../DateRangePicker.vue' +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key, locale: ref('en') }) })) +enableAutoUnmount(afterEach) +beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date(2026, 8, 30, 23, 59)) }) +afterEach(() => vi.useRealTimers()) +async function reopenNextDay() { + const w = mount(DateRangePicker, { props: { startDate: '2026-09-30', endDate: '2026-09-30' }, global: { stubs: { Icon: true } } }) + await w.get('.date-picker-trigger').trigger('click') + expect(w.findAll('input[type="date"]')[1].attributes('max')).toBe('2026-10-01') + await w.get('.date-picker-trigger').trigger('click') + vi.setSystemTime(new Date(2026, 9, 1, 0, 1)) + await w.get('.date-picker-trigger').trigger('click') + return w +} +describe('date presets after midnight', () => { + it.each([ + ['dates.today', '2026-10-01'], + ['dates.last7Days', '2026-09-25'], + ['dates.thisMonth', '2026-10-01'], + ])('refreshes %s when the page stays mounted overnight', async (label, startDate) => { + const w = await reopenNextDay() + await w.findAll('.date-picker-preset').find(b => b.text() === label)!.trigger('click') + await w.get('.date-picker-apply').trigger('click') + expect(w.emitted('change')?.[0]?.[0]).toMatchObject({ startDate, endDate: '2026-10-01' }) + }) + it('updates the maximum selectable date when reopened', async () => { + const w = await reopenNextDay() + expect(w.findAll('input[type="date"]')[1].attributes('max')).toBe('2026-10-02') + }) +})