diff --git a/website/client/src/components/ui/datepicker.vue b/website/client/src/components/ui/datepicker.vue index 1e41a3d46b..8b36316e1b 100644 --- a/website/client/src/components/ui/datepicker.vue +++ b/website/client/src/components/ui/datepicker.vue @@ -68,8 +68,12 @@ export default { }, methods: { upDate (after) { - this.value = after; - this.$emit('update:date', after); + // zero out the time so the server doesn't shift the day across a DST boundary on save + const normalized = after + ? new Date(after.getFullYear(), after.getMonth(), after.getDate()) + : null; + this.value = normalized; + this.$emit('update:date', normalized); }, setToday () { this.upDate(moment().toDate()); diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 1963bc7174..9e92cd3f90 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -27,6 +27,7 @@ import { moveTask, setNextDue, requiredGroupFields, + normalizeDailyStartDate, } from '../../libs/tasks/utils'; import common from '../../../common'; import { apiError } from '../../libs/apiError'; @@ -648,13 +649,10 @@ api.updateTask = { task.group.managerNotes = sanitizedObj.managerNotes; } - // For daily tasks, update start date based on timezone to maintain consistency if (task.type === 'daily' && task.startDate ) { - task.startDate = moment(task.startDate).utcOffset( - -user.preferences.timezoneOffset, - ).startOf('day').toDate(); + task.startDate = normalizeDailyStartDate(task.startDate, user); // If the daily task was set to repeat monthly on a day of the month, and the start date was // updated, the task will then need to be updated to repeat on the same day of the month as diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index c2ef1f4888..386740dd68 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -1,4 +1,3 @@ -import moment from 'moment'; import cloneDeep from 'lodash/cloneDeep'; import compact from 'lodash/compact'; import forEach from 'lodash/forEach'; @@ -9,6 +8,7 @@ import { setNextDue, validateTaskAlias, requiredGroupFields, + normalizeDailyStartDate, } from './utils'; import { model as Challenge } from '../../models/challenge'; import { model as Group } from '../../models/group'; @@ -80,13 +80,8 @@ async function createTasks (req, res, options = {}) { } } - // set startDate to midnight in the user's timezone if (taskType === 'daily') { - const awareStartDate = moment(newTask.startDate).utcOffset(-user.preferences.timezoneOffset); - if (awareStartDate.format('HMsS') !== '0000') { - awareStartDate.startOf('day'); - newTask.startDate = awareStartDate.toDate(); - } + newTask.startDate = normalizeDailyStartDate(newTask.startDate, user); } setNextDue(newTask, user); diff --git a/website/server/libs/tasks/utils.js b/website/server/libs/tasks/utils.js index 5a0c465e6a..5b8c257f29 100644 --- a/website/server/libs/tasks/utils.js +++ b/website/server/libs/tasks/utils.js @@ -59,6 +59,21 @@ export function moveTask (order, taskId, to) { } } +export function normalizeDailyStartDate (date, user) { + if (!date) return date; + const utcView = moment.utc(date); + const looksLikeMidnightLocal = utcView.second() === 0 + && utcView.millisecond() === 0 + && [0, 15, 30, 45].includes(utcView.minute()); + if (looksLikeMidnightLocal) { + return new Date(date); + } + return moment(date) + .utcOffset(-(user.preferences.timezoneOffset || 0)) + .startOf('day') + .toDate(); +} + export function setNextDue (task, user, dueDateOption) { if (task.type !== 'daily') return;