mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
add set end time functionality to timetracker component
This commit is contained in:
@@ -10,7 +10,8 @@ defineProps<{
|
|||||||
<div class="px-4 py-2 2xl:py-3 border-b border-b-background-separator">
|
<div class="px-4 py-2 2xl:py-3 border-b border-b-background-separator">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<div class="flex justify-between">
|
<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 }}
|
{{ name }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="working" class="flex space-x-1.5 items-center justify-end">
|
<div v-if="working" class="flex space-x-1.5 items-center justify-end">
|
||||||
|
|||||||
@@ -117,6 +117,12 @@ async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>
|
|||||||
showManualTimeEntryModal.value = false;
|
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 { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -195,7 +201,8 @@ const { tags } = storeToRefs(useTagsStore());
|
|||||||
@stop-live-timer="stopLiveTimer"
|
@stop-live-timer="stopLiveTimer"
|
||||||
@start-timer="setActiveState(true)"
|
@start-timer="setActiveState(true)"
|
||||||
@stop-timer="setActiveState(false)"
|
@stop-timer="setActiveState(false)"
|
||||||
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
|
@update-time-entry="updateTimeEntry"
|
||||||
|
@create-time-entry="createTimeEntryFromCurrentEntry"></TimeTrackerControls>
|
||||||
</div>
|
</div>
|
||||||
<TimeTrackerMoreOptionsDropdown
|
<TimeTrackerMoreOptionsDropdown
|
||||||
:has-active-timer="isActive"
|
:has-active-timer="isActive"
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const inputValue = ref(model.value ? getLocalizedDayJs(model.value).format('HH:m
|
|||||||
data-testid="time_picker_input"
|
data-testid="time_picker_input"
|
||||||
type="text"
|
type="text"
|
||||||
@blur="updateTime"
|
@blur="updateTime"
|
||||||
|
@keydown.enter.prevent="updateTime"
|
||||||
@focus="($event.target as HTMLInputElement).select()"
|
@focus="($event.target as HTMLInputElement).select()"
|
||||||
@mouseup="($event.target as HTMLInputElement).select()"
|
@mouseup="($event.target as HTMLInputElement).select()"
|
||||||
@click="($event.target as HTMLInputElement).select()"
|
@click="($event.target as HTMLInputElement).select()"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, nextTick, ref, watch } from 'vue';
|
import { defineProps, nextTick, ref, watch } from 'vue';
|
||||||
import { useFocusWithin } from '@vueuse/core';
|
|
||||||
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
|
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
|
||||||
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import TimePickerSimple from '@/packages/ui/src/Input/TimePickerSimple.vue';
|
import TimePickerSimple from '@/packages/ui/src/Input/TimePickerSimple.vue';
|
||||||
|
import { Button } from '@/Components/ui/button';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
start: string;
|
start: string;
|
||||||
@@ -17,31 +17,42 @@ const emit = defineEmits(['changed', 'close']);
|
|||||||
|
|
||||||
const tempStart = ref(props.start ? getLocalizedDayJs(props.start).format() : dayjs().format());
|
const tempStart = ref(props.start ? getLocalizedDayJs(props.start).format() : dayjs().format());
|
||||||
const tempEnd = ref(props.end ? getLocalizedDayJs(props.end).format() : null);
|
const tempEnd = ref(props.end ? getLocalizedDayJs(props.end).format() : null);
|
||||||
|
const showEndTimePicker = ref(false);
|
||||||
|
|
||||||
watch(props, () => {
|
watch(props, () => {
|
||||||
tempStart.value = getLocalizedDayJs(props.start).format();
|
tempStart.value = getLocalizedDayJs(props.start).format();
|
||||||
tempEnd.value = props.end ? getLocalizedDayJs(props.end).format() : null;
|
tempEnd.value = props.end ? getLocalizedDayJs(props.end).format() : null;
|
||||||
|
showEndTimePicker.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateTimeEntry() {
|
function updateTimeEntry() {
|
||||||
const tempStartUtc = getDayJsInstance()(tempStart.value).utc().format();
|
const tempStartUtc = getDayJsInstance()(tempStart.value).utc().format();
|
||||||
const tempEndUtc = tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null;
|
const tempEndUtc = tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null;
|
||||||
|
|
||||||
if (tempStartUtc !== props.start || tempEndUtc !== props.end) {
|
if (tempStartUtc !== props.start || tempEndUtc !== props.end) {
|
||||||
emit(
|
emit(
|
||||||
'changed',
|
'changed',
|
||||||
getDayJsInstance()(tempStart.value).utc().format(),
|
getDayJsInstance()(tempStart.value).utc().format(),
|
||||||
getDayJsInstance()(tempEnd.value).utc().format()
|
tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdownContent = ref();
|
function setEndTime() {
|
||||||
const { focused } = useFocusWithin(dropdownContent);
|
showEndTimePicker.value = true;
|
||||||
|
tempEnd.value = getDayJsInstance()().format();
|
||||||
|
}
|
||||||
|
|
||||||
watch(focused, (newValue, oldValue) => {
|
function confirmEndTime() {
|
||||||
if (oldValue === true && newValue === false) {
|
// wait for the v-model for the end time to update
|
||||||
|
nextTick(() => {
|
||||||
updateTimeEntry();
|
updateTimeEntry();
|
||||||
}
|
showEndTimePicker.value = false;
|
||||||
});
|
emit('close');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownContent = ref();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -67,7 +78,7 @@ watch(focused, (newValue, oldValue) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<div class="font-semibold text-text-primary text-sm pb-2">End</div>
|
<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
|
<TimePickerSimple
|
||||||
v-model="tempEnd"
|
v-model="tempEnd"
|
||||||
data-testid="time_entry_range_end"
|
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"
|
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
|
||||||
@changed="updateTimeEntry"></DatePicker>
|
@changed="updateTimeEntry"></DatePicker>
|
||||||
</div>
|
</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 v-else class="text-text-secondary">-- : --</div>
|
||||||
<div tabindex="0" @focusin="emit('close')"></div>
|
<div tabindex="0" @focusin="emit('close')"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const emit = defineEmits<{
|
|||||||
updateTimeEntry: [];
|
updateTimeEntry: [];
|
||||||
startLiveTimer: [];
|
startLiveTimer: [];
|
||||||
stopLiveTimer: [];
|
stopLiveTimer: [];
|
||||||
|
createTimeEntry: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function updateProject() {
|
function updateProject() {
|
||||||
@@ -280,6 +281,7 @@ useSelectEvents(
|
|||||||
@stop-live-timer="emit('stopLiveTimer')"
|
@stop-live-timer="emit('stopLiveTimer')"
|
||||||
@update-timer="emit('updateTimeEntry')"
|
@update-timer="emit('updateTimeEntry')"
|
||||||
@start-timer="emit('startTimer')"
|
@start-timer="emit('startTimer')"
|
||||||
|
@create-time-entry="emit('createTimeEntry')"
|
||||||
@keydown.enter="startTimerIfNotActive"></TimeTrackerRangeSelector>
|
@keydown.enter="startTimerIfNotActive"></TimeTrackerRangeSelector>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const emit = defineEmits<{
|
|||||||
stopLiveTimer: [];
|
stopLiveTimer: [];
|
||||||
updateTimer: [];
|
updateTimer: [];
|
||||||
startTimer: [];
|
startTimer: [];
|
||||||
|
createTimeEntry: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
@@ -73,12 +74,16 @@ function updateTimerAndStartLiveTimerUpdate() {
|
|||||||
|
|
||||||
const temporaryCustomTimerEntry = ref<string>('');
|
const temporaryCustomTimerEntry = ref<string>('');
|
||||||
|
|
||||||
async function updateTimeRange(newStart: string) {
|
async function updateTimeRange(newStart: string, newEnd: string | null) {
|
||||||
// prohibit updates in the future
|
// prohibit updates in the future
|
||||||
if (getDayJsInstance()(newStart).isBefore(getDayJsInstance()())) {
|
if (getDayJsInstance()(newStart).isBefore(getDayJsInstance()())) {
|
||||||
currentTimeEntry.value.start = newStart;
|
currentTimeEntry.value.start = newStart;
|
||||||
|
currentTimeEntry.value.end = newEnd;
|
||||||
if (currentTimeEntry.value.id) {
|
if (currentTimeEntry.value.id) {
|
||||||
emit('updateTimer');
|
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 {
|
} else {
|
||||||
emit('startTimer');
|
emit('startTimer');
|
||||||
}
|
}
|
||||||
@@ -91,6 +96,14 @@ const startTime = computed(() => {
|
|||||||
}
|
}
|
||||||
return dayjs().utc().format();
|
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 inputField = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const timeRangeSelector = ref<HTMLElement | null>(null);
|
const timeRangeSelector = ref<HTMLElement | null>(null);
|
||||||
@@ -154,7 +167,7 @@ function closeAndFocusInput() {
|
|||||||
<div ref="timeRangeSelector">
|
<div ref="timeRangeSelector">
|
||||||
<TimeRangeSelector
|
<TimeRangeSelector
|
||||||
:start="startTime"
|
:start="startTime"
|
||||||
:end="null"
|
:end="endTime"
|
||||||
@changed="updateTimeRange"
|
@changed="updateTimeRange"
|
||||||
@close="closeAndFocusInput">
|
@close="closeAndFocusInput">
|
||||||
</TimeRangeSelector>
|
</TimeRangeSelector>
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
|||||||
task_id: currentTimeEntry.value.task_id,
|
task_id: currentTimeEntry.value.task_id,
|
||||||
start: currentTimeEntry.value.start,
|
start: currentTimeEntry.value.start,
|
||||||
billable: currentTimeEntry.value.billable,
|
billable: currentTimeEntry.value.billable,
|
||||||
end: null,
|
end: currentTimeEntry.value.end,
|
||||||
tags: currentTimeEntry.value.tags,
|
tags: currentTimeEntry.value.tags,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -175,7 +175,12 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
|||||||
'Time entry updated!'
|
'Time entry updated!'
|
||||||
);
|
);
|
||||||
if (response?.data) {
|
if (response?.data) {
|
||||||
currentTimeEntry.value = response.data;
|
if (response.data.end === null) {
|
||||||
|
currentTimeEntry.value = response.data;
|
||||||
|
} else {
|
||||||
|
$reset();
|
||||||
|
stopLiveTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -215,5 +220,6 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
|||||||
stopLiveTimer,
|
stopLiveTimer,
|
||||||
now,
|
now,
|
||||||
setActiveState,
|
setActiveState,
|
||||||
|
$reset,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user