Compare commits

...

1 Commits

Author SHA1 Message Date
Gregor Vostrak
cb4d3ec061 add set end time functionality to timetracker component 2025-10-21 17:10:13 +02:00
7 changed files with 72 additions and 15 deletions

View File

@@ -10,7 +10,8 @@ defineProps<{
<div class="px-4 py-2 2xl:py-3 border-b border-b-background-separator">
<div class="col-span-2">
<div class="flex justify-between">
<p class="font-semibold text-sm text-text-primary">
<p
class="font-semibold text-sm min-w-0 overflow-ellipsis overflow-hidden flex-1 text-text-primary">
{{ name }}
</p>
<div v-if="working" class="flex space-x-1.5 items-center justify-end">

View File

@@ -117,6 +117,12 @@ async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>
showManualTimeEntryModal.value = false;
}
async function createTimeEntryFromCurrentEntry() {
const { start, end, description, project_id, task_id, billable, tags } = currentTimeEntry.value;
await createTimeEntry({ start, end, description, project_id, task_id, billable, tags });
currentTimeEntryStore.$reset();
}
const { handleApiRequestNotifications } = useNotificationsStore();
const queryClient = useQueryClient();
@@ -195,7 +201,8 @@ const { tags } = storeToRefs(useTagsStore());
@stop-live-timer="stopLiveTimer"
@start-timer="setActiveState(true)"
@stop-timer="setActiveState(false)"
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
@update-time-entry="updateTimeEntry"
@create-time-entry="createTimeEntryFromCurrentEntry"></TimeTrackerControls>
</div>
<TimeTrackerMoreOptionsDropdown
:has-active-timer="isActive"

View File

@@ -93,6 +93,7 @@ const inputValue = ref(model.value ? getLocalizedDayJs(model.value).format('HH:m
data-testid="time_picker_input"
type="text"
@blur="updateTime"
@keydown.enter.prevent="updateTime"
@focus="($event.target as HTMLInputElement).select()"
@mouseup="($event.target as HTMLInputElement).select()"
@click="($event.target as HTMLInputElement).select()"

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { defineProps, nextTick, ref, watch } from 'vue';
import { useFocusWithin } from '@vueuse/core';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import dayjs from 'dayjs';
import TimePickerSimple from '@/packages/ui/src/Input/TimePickerSimple.vue';
import { Button } from '@/Components/ui/button';
const props = defineProps<{
start: string;
@@ -17,31 +17,42 @@ const emit = defineEmits(['changed', 'close']);
const tempStart = ref(props.start ? getLocalizedDayJs(props.start).format() : dayjs().format());
const tempEnd = ref(props.end ? getLocalizedDayJs(props.end).format() : null);
const showEndTimePicker = ref(false);
watch(props, () => {
tempStart.value = getLocalizedDayJs(props.start).format();
tempEnd.value = props.end ? getLocalizedDayJs(props.end).format() : null;
showEndTimePicker.value = false;
});
function updateTimeEntry() {
const tempStartUtc = getDayJsInstance()(tempStart.value).utc().format();
const tempEndUtc = tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null;
if (tempStartUtc !== props.start || tempEndUtc !== props.end) {
emit(
'changed',
getDayJsInstance()(tempStart.value).utc().format(),
getDayJsInstance()(tempEnd.value).utc().format()
tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null
);
}
}
const dropdownContent = ref();
const { focused } = useFocusWithin(dropdownContent);
function setEndTime() {
showEndTimePicker.value = true;
tempEnd.value = getDayJsInstance()().format();
}
watch(focused, (newValue, oldValue) => {
if (oldValue === true && newValue === false) {
function confirmEndTime() {
// wait for the v-model for the end time to update
nextTick(() => {
updateTimeEntry();
}
});
showEndTimePicker.value = false;
emit('close');
});
}
const dropdownContent = ref();
</script>
<template>
@@ -67,7 +78,7 @@ watch(focused, (newValue, oldValue) => {
</div>
<div class="px-2">
<div class="font-semibold text-text-primary text-sm pb-2">End</div>
<div v-if="tempEnd !== null" class="space-y-2">
<div v-if="end !== null && tempEnd !== null" class="space-y-2">
<TimePickerSimple
v-model="tempEnd"
data-testid="time_entry_range_end"
@@ -77,6 +88,22 @@ watch(focused, (newValue, oldValue) => {
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
@changed="updateTimeEntry"></DatePicker>
</div>
<div v-else-if="end === null && !showEndTimePicker">
<Button variant="outline" size="sm" @click="setEndTime"> Set End Time </Button>
</div>
<div v-else-if="showEndTimePicker && tempEnd !== null" class="space-y-2">
<TimePickerSimple
v-model="tempEnd"
data-testid="time_entry_range_end"
@keydown.enter.prevent.stop="confirmEndTime"></TimePickerSimple>
<DatePicker
v-model="tempEnd"
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
@keydown.enter.prevent="confirmEndTime"></DatePicker>
<Button variant="outline" size="sm" class="w-full" @click="confirmEndTime">
Confirm
</Button>
</div>
<div v-else class="text-text-secondary">-- : --</div>
<div tabindex="0" @focusin="emit('close')"></div>
</div>

View File

@@ -49,6 +49,7 @@ const emit = defineEmits<{
updateTimeEntry: [];
startLiveTimer: [];
stopLiveTimer: [];
createTimeEntry: [];
}>();
function updateProject() {
@@ -280,6 +281,7 @@ useSelectEvents(
@stop-live-timer="emit('stopLiveTimer')"
@update-timer="emit('updateTimeEntry')"
@start-timer="emit('startTimer')"
@create-time-entry="emit('createTimeEntry')"
@keydown.enter="startTimerIfNotActive"></TimeTrackerRangeSelector>
</div>
</div>

View File

@@ -16,6 +16,7 @@ const emit = defineEmits<{
stopLiveTimer: [];
updateTimer: [];
startTimer: [];
createTimeEntry: [];
}>();
const open = ref(false);
@@ -73,12 +74,16 @@ function updateTimerAndStartLiveTimerUpdate() {
const temporaryCustomTimerEntry = ref<string>('');
async function updateTimeRange(newStart: string) {
async function updateTimeRange(newStart: string, newEnd: string | null) {
// prohibit updates in the future
if (getDayJsInstance()(newStart).isBefore(getDayJsInstance()())) {
currentTimeEntry.value.start = newStart;
currentTimeEntry.value.end = newEnd;
if (currentTimeEntry.value.id) {
emit('updateTimer');
} else if (newEnd !== null) {
// If there's no ID but we have both start and end, create a new time entry
emit('createTimeEntry');
} else {
emit('startTimer');
}
@@ -91,6 +96,14 @@ const startTime = computed(() => {
}
return dayjs().utc().format();
});
const endTime = computed(() => {
if (currentTimeEntry.value.end && currentTimeEntry.value.end !== '') {
return currentTimeEntry.value.end;
}
return null;
});
const inputField = ref<HTMLInputElement | null>(null);
const timeRangeSelector = ref<HTMLElement | null>(null);
@@ -154,7 +167,7 @@ function closeAndFocusInput() {
<div ref="timeRangeSelector">
<TimeRangeSelector
:start="startTime"
:end="null"
:end="endTime"
@changed="updateTimeRange"
@close="closeAndFocusInput">
</TimeRangeSelector>

View File

@@ -162,7 +162,7 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
task_id: currentTimeEntry.value.task_id,
start: currentTimeEntry.value.start,
billable: currentTimeEntry.value.billable,
end: null,
end: currentTimeEntry.value.end,
tags: currentTimeEntry.value.tags,
},
{
@@ -175,7 +175,12 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
'Time entry updated!'
);
if (response?.data) {
currentTimeEntry.value = response.data;
if (response.data.end === null) {
currentTimeEntry.value = response.data;
} else {
$reset();
stopLiveTimer();
}
}
} else {
throw new Error(
@@ -215,5 +220,6 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
stopLiveTimer,
now,
setActiveState,
$reset,
};
});