Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion vueManager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"format": "prettier --write \"src/**/*.{js,vue,scss,css}\"",
"format:check": "prettier --check \"src/**/*.{js,vue,scss,css}\"",
"lint:all": "npm run lint && npm run format:check && npm run lint:scss",
"lint:scss": "stylelint \"src/**/*.scss\" --fix"
"lint:scss": "stylelint \"src/**/*.scss\" --fix",
"test:utils": "node src/utils/formatLocalDateYmd.test.mjs"
},
"dependencies": {
"@primeuix/themes": "^2.0.3",
Expand Down
16 changes: 4 additions & 12 deletions vueManager/src/components/OrdersGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { computed, onMounted, ref } from 'vue'
import { useGridFilterParams } from '../composables/useGridFilterParams.js'
import { useSelection } from '../composables/useSelection.js'
import request from '../request.js'
import { formatLocalDateYmd } from '../utils/formatLocalDateYmd.js'
import ActionsColumn from './ActionsColumn.vue'

const toast = useToast()
Expand Down Expand Up @@ -106,18 +107,18 @@ async function loadOrders() {
addFilterParam(
params,
filterConfig.fields?.from || `${key}_from`,
formatDateForApi(value[0])
formatLocalDateYmd(value[0])
)
}
if (value[1]) {
addFilterParam(
params,
filterConfig.fields?.to || `${key}_to`,
formatDateForApi(value[1])
formatLocalDateYmd(value[1])
)
}
} else if (filterConfig?.type === 'datepicker' && value) {
addFilterParam(params, key, formatDateForApi(value))
addFilterParam(params, key, formatLocalDateYmd(value))
} else {
addFilterParam(params, key, value)
}
Expand Down Expand Up @@ -169,15 +170,6 @@ function onSort(event) {
loadOrders()
}

/**
* Format date for API (YYYY-MM-DD)
*/
function formatDateForApi(date) {
if (!date) return null
const d = new Date(date)
return d.toISOString().split('T')[0]
}

/**
* Open order for editing (redirect to order page)
*/
Expand Down
11 changes: 2 additions & 9 deletions vueManager/src/components/product/ProductOptionField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Textarea from 'primevue/textarea'
import { computed, ref, watch } from 'vue'

import request from '../../request.js'
import { formatLocalDateYmd } from '../../utils/formatLocalDateYmd.js'

const props = defineProps({
option: { type: Object, required: true },
Expand Down Expand Up @@ -80,17 +81,9 @@ function onChange() {
emit('change', { key: props.option.key, value: value.value })
}

/**
* Format a Date in local timezone as YYYY-MM-DD.
* Never use Date.toISOString() here — it converts to UTC and shifts the date east of UTC
* (user picks 2026-04-20, toISOString() returns 2026-04-19).
*/
function formatDateForPost(raw) {
if (raw instanceof Date) {
const y = raw.getFullYear()
const m = String(raw.getMonth() + 1).padStart(2, '0')
const d = String(raw.getDate()).padStart(2, '0')
return `${y}-${m}-${d}`
return formatLocalDateYmd(raw) || ''
}
return raw || ''
}
Expand Down
24 changes: 24 additions & 0 deletions vueManager/src/utils/formatLocalDateYmd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Format a date as YYYY-MM-DD in the browser's local calendar.
*
* Do not use Date.toISOString().split('T')[0] — ISO is UTC and shifts the
* calendar day east/west of UTC (issue #386).
*
* @param {string|number|Date|null|undefined} date
* @returns {string|null}
*/
export function formatLocalDateYmd(date) {
if (date === null || date === undefined || date === '') {
return null
}

const d = date instanceof Date ? date : new Date(date)
if (Number.isNaN(d.getTime())) {
return null
}

const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
40 changes: 40 additions & 0 deletions vueManager/src/utils/formatLocalDateYmd.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Regression: local YYYY-MM-DD must not use UTC from toISOString (#386).
*
* Run: node src/utils/formatLocalDateYmd.test.mjs
*/

import assert from 'node:assert/strict'
import { stdout } from 'node:process'

import { formatLocalDateYmd } from './formatLocalDateYmd.js'

assert.equal(formatLocalDateYmd(null), null)
assert.equal(formatLocalDateYmd(''), null)
assert.equal(formatLocalDateYmd('invalid'), null)

// Local calendar components (month is 0-based)
const localMidnight = new Date(2026, 3, 20, 0, 0, 0)
assert.equal(formatLocalDateYmd(localMidnight), '2026-04-20')

// Early local morning east of UTC: toISOString often yields previous UTC day
const earlyLocalMorning = new Date(2026, 3, 21, 1, 0, 0)
assert.equal(formatLocalDateYmd(earlyLocalMorning), '2026-04-21')

const utcDay = earlyLocalMorning.toISOString().split('T')[0]
const offsetMinutes = earlyLocalMorning.getTimezoneOffset()
if (offsetMinutes < 0 && utcDay !== '2026-04-21') {
// Negative getTimezoneOffset => local is ahead of UTC (e.g. Asia/Almaty)
assert.notEqual(
utcDay,
formatLocalDateYmd(earlyLocalMorning),
'toISOString must differ from local YMD east of UTC (documents #386 bug)'
)
}

assert.equal(
formatLocalDateYmd('2026-04-20T12:00:00'),
formatLocalDateYmd(new Date('2026-04-20T12:00:00'))
)

stdout.write('OK formatLocalDateYmd.test.mjs\n')