Skip to content

Commit f8f708a

Browse files
committed
add set end time functionality to timetracker component
1 parent c359259 commit f8f708a

File tree

7 files changed

+72
-15
lines changed

7 files changed

+72
-15
lines changed

resources/js/Components/Dashboard/TeamActivityCardEntry.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defineProps<{
1010
<div class="px-4 py-2 2xl:py-3 border-b border-b-background-separator">
1111
<div class="col-span-2">
1212
<div class="flex justify-between">
13-
<p class="font-semibold text-sm text-text-primary">
13+
<p
14+
class="font-semibold text-sm min-w-0 overflow-ellipsis overflow-hidden flex-1 text-text-primary">
1415
{{ name }}
1516
</p>
1617
<div v-if="working" class="flex space-x-1.5 items-center justify-end">

resources/js/Components/TimeTracker.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>
117117
showManualTimeEntryModal.value = false;
118118
}
119119
120+
async function createTimeEntryFromCurrentEntry() {
121+
const { start, end, description, project_id, task_id, billable, tags } = currentTimeEntry.value;
122+
await createTimeEntry({ start, end, description, project_id, task_id, billable, tags });
123+
currentTimeEntryStore.$reset();
124+
}
125+
120126
const { handleApiRequestNotifications } = useNotificationsStore();
121127
const queryClient = useQueryClient();
122128
@@ -195,7 +201,8 @@ const { tags } = storeToRefs(useTagsStore());
195201
@stop-live-timer="stopLiveTimer"
196202
@start-timer="setActiveState(true)"
197203
@stop-timer="setActiveState(false)"
198-
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
204+
@update-time-entry="updateTimeEntry"
205+
@create-time-entry="createTimeEntryFromCurrentEntry"></TimeTrackerControls>
199206
</div>
200207
<TimeTrackerMoreOptionsDropdown
201208
:has-active-timer="isActive"

resources/js/packages/ui/src/Input/TimePickerSimple.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const inputValue = ref(model.value ? getLocalizedDayJs(model.value).format('HH:m
9393
data-testid="time_picker_input"
9494
type="text"
9595
@blur="updateTime"
96+
@keydown.enter.prevent="updateTime"
9697
@focus="($event.target as HTMLInputElement).select()"
9798
@mouseup="($event.target as HTMLInputElement).select()"
9899
@click="($event.target as HTMLInputElement).select()"

resources/js/packages/ui/src/Input/TimeRangeSelector.vue

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script setup lang="ts">
22
import { defineProps, nextTick, ref, watch } from 'vue';
3-
import { useFocusWithin } from '@vueuse/core';
43
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
54
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
65
import dayjs from 'dayjs';
76
import TimePickerSimple from '@/packages/ui/src/Input/TimePickerSimple.vue';
7+
import { Button } from '@/Components/ui/button';
88
99
const props = defineProps<{
1010
start: string;
@@ -17,31 +17,42 @@ const emit = defineEmits(['changed', 'close']);
1717
1818
const tempStart = ref(props.start ? getLocalizedDayJs(props.start).format() : dayjs().format());
1919
const tempEnd = ref(props.end ? getLocalizedDayJs(props.end).format() : null);
20+
const showEndTimePicker = ref(false);
2021
2122
watch(props, () => {
2223
tempStart.value = getLocalizedDayJs(props.start).format();
2324
tempEnd.value = props.end ? getLocalizedDayJs(props.end).format() : null;
25+
showEndTimePicker.value = false;
2426
});
27+
2528
function updateTimeEntry() {
2629
const tempStartUtc = getDayJsInstance()(tempStart.value).utc().format();
2730
const tempEndUtc = tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null;
31+
2832
if (tempStartUtc !== props.start || tempEndUtc !== props.end) {
2933
emit(
3034
'changed',
3135
getDayJsInstance()(tempStart.value).utc().format(),
32-
getDayJsInstance()(tempEnd.value).utc().format()
36+
tempEnd.value ? getDayJsInstance()(tempEnd.value).utc().format() : null
3337
);
3438
}
3539
}
3640
37-
const dropdownContent = ref();
38-
const { focused } = useFocusWithin(dropdownContent);
41+
function setEndTime() {
42+
showEndTimePicker.value = true;
43+
tempEnd.value = getDayJsInstance()().format();
44+
}
3945
40-
watch(focused, (newValue, oldValue) => {
41-
if (oldValue === true && newValue === false) {
46+
function confirmEndTime() {
47+
// wait for the v-model for the end time to update
48+
nextTick(() => {
4249
updateTimeEntry();
43-
}
44-
});
50+
showEndTimePicker.value = false;
51+
emit('close');
52+
});
53+
}
54+
55+
const dropdownContent = ref();
4556
</script>
4657

4758
<template>
@@ -67,7 +78,7 @@ watch(focused, (newValue, oldValue) => {
6778
</div>
6879
<div class="px-2">
6980
<div class="font-semibold text-text-primary text-sm pb-2">End</div>
70-
<div v-if="tempEnd !== null" class="space-y-2">
81+
<div v-if="end !== null && tempEnd !== null" class="space-y-2">
7182
<TimePickerSimple
7283
v-model="tempEnd"
7384
data-testid="time_entry_range_end"
@@ -77,6 +88,22 @@ watch(focused, (newValue, oldValue) => {
7788
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
7889
@changed="updateTimeEntry"></DatePicker>
7990
</div>
91+
<div v-else-if="end === null && !showEndTimePicker">
92+
<Button variant="outline" size="sm" @click="setEndTime"> Set End Time </Button>
93+
</div>
94+
<div v-else-if="showEndTimePicker && tempEnd !== null" class="space-y-2">
95+
<TimePickerSimple
96+
v-model="tempEnd"
97+
data-testid="time_entry_range_end"
98+
@keydown.enter.prevent.stop="confirmEndTime"></TimePickerSimple>
99+
<DatePicker
100+
v-model="tempEnd"
101+
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
102+
@keydown.enter.prevent="confirmEndTime"></DatePicker>
103+
<Button variant="outline" size="sm" class="w-full" @click="confirmEndTime">
104+
Confirm
105+
</Button>
106+
</div>
80107
<div v-else class="text-text-secondary">-- : --</div>
81108
<div tabindex="0" @focusin="emit('close')"></div>
82109
</div>

resources/js/packages/ui/src/TimeTracker/TimeTrackerControls.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const emit = defineEmits<{
4949
updateTimeEntry: [];
5050
startLiveTimer: [];
5151
stopLiveTimer: [];
52+
createTimeEntry: [];
5253
}>();
5354
5455
function updateProject() {
@@ -280,6 +281,7 @@ useSelectEvents(
280281
@stop-live-timer="emit('stopLiveTimer')"
281282
@update-timer="emit('updateTimeEntry')"
282283
@start-timer="emit('startTimer')"
284+
@create-time-entry="emit('createTimeEntry')"
283285
@keydown.enter="startTimerIfNotActive"></TimeTrackerRangeSelector>
284286
</div>
285287
</div>

resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const emit = defineEmits<{
1616
stopLiveTimer: [];
1717
updateTimer: [];
1818
startTimer: [];
19+
createTimeEntry: [];
1920
}>();
2021
2122
const open = ref(false);
@@ -73,12 +74,16 @@ function updateTimerAndStartLiveTimerUpdate() {
7374
7475
const temporaryCustomTimerEntry = ref<string>('');
7576
76-
async function updateTimeRange(newStart: string) {
77+
async function updateTimeRange(newStart: string, newEnd: string | null) {
7778
// prohibit updates in the future
7879
if (getDayJsInstance()(newStart).isBefore(getDayJsInstance()())) {
7980
currentTimeEntry.value.start = newStart;
81+
currentTimeEntry.value.end = newEnd;
8082
if (currentTimeEntry.value.id) {
8183
emit('updateTimer');
84+
} else if (newEnd !== null) {
85+
// If there's no ID but we have both start and end, create a new time entry
86+
emit('createTimeEntry');
8287
} else {
8388
emit('startTimer');
8489
}
@@ -91,6 +96,14 @@ const startTime = computed(() => {
9196
}
9297
return dayjs().utc().format();
9398
});
99+
100+
const endTime = computed(() => {
101+
if (currentTimeEntry.value.end && currentTimeEntry.value.end !== '') {
102+
return currentTimeEntry.value.end;
103+
}
104+
return null;
105+
});
106+
94107
const inputField = ref<HTMLInputElement | null>(null);
95108
96109
const timeRangeSelector = ref<HTMLElement | null>(null);
@@ -154,7 +167,7 @@ function closeAndFocusInput() {
154167
<div ref="timeRangeSelector">
155168
<TimeRangeSelector
156169
:start="startTime"
157-
:end="null"
170+
:end="endTime"
158171
@changed="updateTimeRange"
159172
@close="closeAndFocusInput">
160173
</TimeRangeSelector>

resources/js/utils/useCurrentTimeEntry.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
162162
task_id: currentTimeEntry.value.task_id,
163163
start: currentTimeEntry.value.start,
164164
billable: currentTimeEntry.value.billable,
165-
end: null,
165+
end: currentTimeEntry.value.end,
166166
tags: currentTimeEntry.value.tags,
167167
},
168168
{
@@ -175,7 +175,12 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
175175
'Time entry updated!'
176176
);
177177
if (response?.data) {
178-
currentTimeEntry.value = response.data;
178+
if (response.data.end === null) {
179+
currentTimeEntry.value = response.data;
180+
} else {
181+
$reset();
182+
stopLiveTimer();
183+
}
179184
}
180185
} else {
181186
throw new Error(
@@ -215,5 +220,6 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
215220
stopLiveTimer,
216221
now,
217222
setActiveState,
223+
$reset,
218224
};
219225
});

0 commit comments

Comments
 (0)