fix timezone issues related to time entries spanning over a day

This commit is contained in:
Gregor Vostrak
2024-06-12 17:06:10 +02:00
parent 91cb6ab087
commit 92dde6a701
5 changed files with 55 additions and 23 deletions

View File

@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { getLocalizedDayJs } from '@/utils/time'; import { getDayJsInstance, getLocalizedDayJs } from '@/utils/time';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
const props = defineProps<{ const props = defineProps<{
class?: string; class?: string;
}>(); }>();
// This has to be a localized timestamp, not UTC
const model = defineModel<string | null>({ const model = defineModel<string | null>({
default: null, default: null,
}); });
@@ -15,13 +17,12 @@ const tempDate = ref(getLocalizedDayJs(model.value).format('YYYY-MM-DD'));
function updateDate(event: Event) { function updateDate(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const newValue = target.value; const newValue = target.value;
const newDate = getLocalizedDayJs(newValue); const newDate = getDayJsInstance()(newValue);
if (newDate) { if (newDate) {
model.value = getLocalizedDayJs(model.value) model.value = getLocalizedDayJs(model.value)
.set('year', newDate.year()) .set('year', newDate.year())
.set('month', newDate.month()) .set('month', newDate.month())
.set('date', newDate.date()) .set('date', newDate.date())
.utc()
.format(); .format();
emit('changed', model.value); emit('changed', model.value);
} }

View File

@@ -2,7 +2,7 @@
import TextInput from '@/Components/TextInput.vue'; import TextInput from '@/Components/TextInput.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue'; import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue'; import DialogModal from '@/Components/DialogModal.vue';
import { ref } from 'vue'; import { ref, watch } from 'vue';
import PrimaryButton from '@/Components/PrimaryButton.vue'; import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useFocus } from '@vueuse/core'; import { useFocus } from '@vueuse/core';
import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue'; import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue';
@@ -13,7 +13,7 @@ import { useTimeEntriesStore } from '@/utils/useTimeEntries';
import InputLabel from '@/Components/InputLabel.vue'; import InputLabel from '@/Components/InputLabel.vue';
import TimePicker from '@/Components/Common/TimePicker.vue'; import TimePicker from '@/Components/Common/TimePicker.vue';
import DatePicker from '@/Components/Common/DatePicker.vue'; import DatePicker from '@/Components/Common/DatePicker.vue';
import { getDayJsInstance } from '@/utils/time'; import { getDayJsInstance, getLocalizedDayJs } from '@/utils/time';
const { createTimeEntry } = useTimeEntriesStore(); const { createTimeEntry } = useTimeEntriesStore();
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
@@ -32,9 +32,25 @@ const timeEntryDefaultValues = {
const timeEntry = ref({ ...timeEntryDefaultValues }); const timeEntry = ref({ ...timeEntryDefaultValues });
const localStart = ref(
getLocalizedDayJs(timeEntryDefaultValues.start).format()
);
watch(localStart, (value) => {
timeEntry.value.start = getLocalizedDayJs(value).utc().format();
});
const localEnd = ref(getLocalizedDayJs(timeEntryDefaultValues.end).format());
watch(localEnd, (value) => {
timeEntry.value.end = getLocalizedDayJs(value).utc().format();
});
async function submit() { async function submit() {
await createTimeEntry(timeEntry.value); await createTimeEntry(timeEntry.value);
timeEntry.value = { ...timeEntryDefaultValues }; timeEntry.value = { ...timeEntryDefaultValues };
localStart.value = getLocalizedDayJs(timeEntryDefaultValues.start).format();
localEnd.value = getLocalizedDayJs(timeEntryDefaultValues.end).format();
show.value = false; show.value = false;
} }
@@ -85,19 +101,19 @@ useFocus(projectNameInput, { initialValue: true });
<div class="flex-1"> <div class="flex-1">
<InputLabel>Start</InputLabel> <InputLabel>Start</InputLabel>
<div class="flex items-center space-x-4 mt-1"> <div class="flex items-center space-x-4 mt-1">
<DatePicker v-model="timeEntry.start"></DatePicker> <DatePicker v-model="localStart"></DatePicker>
<TimePicker <TimePicker
size="large" size="large"
v-model="timeEntry.start"></TimePicker> v-model="localStart"></TimePicker>
</div> </div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<InputLabel>End</InputLabel> <InputLabel>End</InputLabel>
<div class="flex items-center space-x-4 mt-1"> <div class="flex items-center space-x-4 mt-1">
<DatePicker v-model="timeEntry.end"></DatePicker> <DatePicker v-model="localEnd"></DatePicker>
<TimePicker <TimePicker
size="large" size="large"
v-model="timeEntry.end"></TimePicker> v-model="localEnd"></TimePicker>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { getLocalizedDayJs } from '@/utils/time'; import { getDayJsInstance, getLocalizedDayJs } from '@/utils/time';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
// This has to be a localized timestamp, not UTC
const model = defineModel<string | null>({ const model = defineModel<string | null>({
default: null, default: null,
}); });
@@ -28,9 +29,8 @@ function updateMinutes(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const newValue = target.value; const newValue = target.value;
if (!isNaN(parseInt(newValue))) { if (!isNaN(parseInt(newValue))) {
model.value = getLocalizedDayJs(model.value) model.value = getDayJsInstance()(model.value)
.set('minutes', Math.min(parseInt(newValue), 59)) .set('minutes', Math.min(parseInt(newValue), 59))
.utc()
.format(); .format();
} }
} }
@@ -41,7 +41,6 @@ function updateHours(event: Event) {
if (!isNaN(parseInt(newValue))) { if (!isNaN(parseInt(newValue))) {
model.value = getLocalizedDayJs(model.value) model.value = getLocalizedDayJs(model.value)
.set('hours', Math.min(parseInt(newValue), 23)) .set('hours', Math.min(parseInt(newValue), 23))
.utc()
.format(); .format();
} }
} }

View File

@@ -3,23 +3,33 @@ import { defineProps, ref, watch } from 'vue';
import TimePicker from '@/Components/Common/TimePicker.vue'; import TimePicker from '@/Components/Common/TimePicker.vue';
import { useFocusWithin } from '@vueuse/core'; import { useFocusWithin } from '@vueuse/core';
import DatePicker from '@/Components/Common/DatePicker.vue'; import DatePicker from '@/Components/Common/DatePicker.vue';
import { getDayJsInstance, getLocalizedDayJs } from '@/utils/time';
import dayjs from 'dayjs';
const props = defineProps<{ const props = defineProps<{
start: string; start: string;
end: string | null; end: string | null;
}>(); }>();
// The timestamps for the changed event are UTC
const emit = defineEmits(['changed']); const emit = defineEmits(['changed']);
const tempStart = ref(props.start);
const tempEnd = ref(props.end || null); const tempStart = ref(
props.start ? getLocalizedDayJs(props.start).format() : dayjs().format()
);
const tempEnd = ref(props.end ? getLocalizedDayJs(props.end).format() : null);
watch(props, () => { watch(props, () => {
tempStart.value = props.start; tempStart.value = getLocalizedDayJs(props.start).format();
tempEnd.value = props.end; tempEnd.value = getLocalizedDayJs(props.end).format();
}); });
function updateTimeEntry() { function updateTimeEntry() {
if (tempStart.value !== props.start || tempEnd.value !== props.end) { if (tempStart.value !== props.start || tempEnd.value !== props.end) {
emit('changed', tempStart.value, tempEnd.value); emit(
'changed',
getDayJsInstance()(tempStart.value).utc().format(),
getDayJsInstance()(tempEnd.value).utc().format()
);
} }
} }

View File

@@ -13,7 +13,11 @@ import DateRangePicker from '@/Components/Common/DateRangePicker.vue';
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue'; import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
import BillableIcon from '@/Components/Common/Icons/BillableIcon.vue'; import BillableIcon from '@/Components/Common/Icons/BillableIcon.vue';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { formatHumanReadableDuration, getDayJsInstance } from '@/utils/time'; import {
formatHumanReadableDuration,
getDayJsInstance,
getLocalizedDayJs,
} from '@/utils/time';
import { type GroupingOption, useReportingStore } from '@/utils/useReporting'; import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import TagDropdown from '@/Components/Common/Tag/TagDropdown.vue'; import TagDropdown from '@/Components/Common/Tag/TagDropdown.vue';
@@ -31,9 +35,11 @@ import { getCurrentMembershipId, getCurrentRole } from '@/utils/useUser';
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue'; import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
const startDate = ref<string | null>( const startDate = ref<string | null>(
getDayJsInstance()().subtract(14, 'd').format('YYYY-MM-DD') getLocalizedDayJs(getDayJsInstance()().format()).subtract(2, 'd').format()
);
const endDate = ref<string | null>(
getLocalizedDayJs(getDayJsInstance()().format()).format()
); );
const endDate = ref<string | null>(getDayJsInstance()().format('YYYY-MM-DD'));
const selectedTags = ref<string[]>([]); const selectedTags = ref<string[]>([]);
const selectedProjects = ref<string[]>([]); const selectedProjects = ref<string[]>([]);
const selectedMembers = ref<string[]>([]); const selectedMembers = ref<string[]>([]);
@@ -54,8 +60,8 @@ const { groupByOptions } = reportingStore;
function getFilterAttributes() { function getFilterAttributes() {
let params: AggregatedTimeEntriesQueryParams = { let params: AggregatedTimeEntriesQueryParams = {
start: getDayJsInstance()(startDate.value).utc().format(), start: getLocalizedDayJs(startDate.value).startOf('day').utc().format(),
end: getDayJsInstance()(endDate.value).endOf('day').utc().format(), end: getLocalizedDayJs(endDate.value).endOf('day').utc().format(),
}; };
params = { params = {
...params, ...params,