Skip to content

Commit 0791a68

Browse files
committed
add support for currently running time entry
1 parent e666792 commit 0791a68

File tree

1 file changed

+89
-47
lines changed

1 file changed

+89
-47
lines changed

resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
nextTick,
1414
onMounted,
1515
onActivated,
16+
onUnmounted,
1617
} from 'vue';
1718
import chroma from 'chroma-js';
1819
import { useCssVariable } from '@/utils/useCssVariable';
@@ -37,7 +38,10 @@ import type {
3738
} from '@/packages/api/src';
3839
import type { Dayjs } from 'dayjs';
3940
40-
type CalendarExtendedProps = { timeEntry: TimeEntry } & Record<string, unknown>;
41+
type CalendarExtendedProps = { timeEntry: TimeEntry; isRunning?: boolean } & Record<
42+
string,
43+
unknown
44+
>;
4145
4246
const emit = defineEmits<{
4347
(e: 'dates-change', payload: { start: Date; end: Date }): void;
@@ -77,6 +81,10 @@ const selectedTimeEntry = ref<TimeEntry | null>(null);
7781
7882
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null);
7983
84+
// Reactive "now" for running time entry - updates every minute
85+
const currentTime = ref(getDayJsInstance()());
86+
let currentTimeInterval: ReturnType<typeof setInterval> | null = null;
87+
8088
// Inject organization data for settings
8189
const organization = inject<ComputedRef<Organization>>('organization');
8290
@@ -118,47 +126,52 @@ const events = computed(() => {
118126
const themeBackground = (() => {
119127
return cssBackground.value?.trim();
120128
})();
121-
return props.timeEntries
122-
?.filter((timeEntry) => timeEntry.end !== null)
123-
?.map((timeEntry) => {
124-
const project = props.projects.find((p) => p.id === timeEntry.project_id);
125-
const client = props.clients.find((c) => c.id === project?.client_id);
126-
const task = props.tasks.find((t) => t.id === timeEntry.task_id);
127-
const duration = getDayJsInstance()(timeEntry.end!).diff(
128-
getDayJsInstance()(timeEntry.start),
129-
'minutes'
130-
);
129+
return props.timeEntries?.map((timeEntry) => {
130+
const isRunning = timeEntry.end === null;
131+
const project = props.projects.find((p) => p.id === timeEntry.project_id);
132+
const client = props.clients.find((c) => c.id === project?.client_id);
133+
const task = props.tasks.find((t) => t.id === timeEntry.task_id);
134+
135+
// For running entries, use current time as end
136+
const effectiveEnd = isRunning ? currentTime.value : getDayJsInstance()(timeEntry.end!);
137+
const duration = effectiveEnd.diff(getDayJsInstance()(timeEntry.start), 'minutes');
138+
139+
const title = timeEntry.description || 'No description';
140+
141+
const baseColor = project?.color || '#6B7280';
142+
const backgroundColor = chroma.mix(baseColor, themeBackground, 0.65, 'lab').hex();
143+
const borderColor = chroma.mix(baseColor, themeBackground, 0.5, 'lab').hex();
144+
145+
// For 0-duration events, display them with minimum visual duration but preserve actual duration
146+
const startTime = getLocalizedDayJs(timeEntry.start);
147+
const endTime =
148+
duration === 0
149+
? startTime.add(1, 'second') // Show as 1 second for minimal visibility
150+
: isRunning
151+
? getLocalizedDayJs(currentTime.value.toISOString())
152+
: getLocalizedDayJs(timeEntry.end!);
131153
132-
const title = timeEntry.description || 'No description';
133-
134-
const baseColor = project?.color || '#6B7280';
135-
const backgroundColor = chroma.mix(baseColor, themeBackground, 0.65, 'lab').hex();
136-
const borderColor = chroma.mix(baseColor, themeBackground, 0.5, 'lab').hex();
137-
138-
// For 0-duration events, display them with minimum visual duration but preserve actual duration
139-
const startTime = getLocalizedDayJs(timeEntry.start);
140-
const endTime =
141-
duration === 0
142-
? startTime.add(1, 'second') // Show as 1 second for minimal visibility
143-
: getLocalizedDayJs(timeEntry.end!);
144-
145-
return {
146-
id: timeEntry.id,
147-
start: startTime.format(),
148-
end: endTime.format(),
149-
title,
150-
backgroundColor,
151-
borderColor,
152-
textColor: 'var(--foreground)',
153-
extendedProps: {
154-
timeEntry,
155-
project,
156-
client,
157-
task,
158-
duration,
159-
},
160-
};
161-
});
154+
return {
155+
id: timeEntry.id,
156+
start: startTime.format(),
157+
end: endTime.format(),
158+
title,
159+
backgroundColor,
160+
borderColor,
161+
textColor: 'var(--foreground)',
162+
// For running entries: disable dragging and resizing
163+
startEditable: !isRunning,
164+
classNames: isRunning ? ['running-entry'] : [],
165+
extendedProps: {
166+
timeEntry,
167+
project,
168+
client,
169+
task,
170+
duration,
171+
isRunning,
172+
},
173+
};
174+
});
162175
});
163176
164177
// Daily totals used in day header
@@ -199,6 +212,10 @@ function handleDateSelect(arg: { start: Date; end: Date }) {
199212
200213
function handleEventClick(arg: EventClickArg) {
201214
const ext = arg.event.extendedProps as CalendarExtendedProps;
215+
// Don't open edit modal for running time entries
216+
if (ext.isRunning) {
217+
return;
218+
}
202219
selectedTimeEntry.value = ext.timeEntry;
203220
showEditTimeEntryModal.value = true;
204221
}
@@ -238,12 +255,15 @@ async function handleEventResize(arg: EventChangeArg) {
238255
.second(0)
239256
.utc()
240257
.format(),
241-
end: getDayJsInstance()(arg.event.end.toISOString())
242-
.utc()
243-
.tz(getUserTimezone(), true)
244-
.second(0)
245-
.utc()
246-
.format(),
258+
// Preserve null end for running entries
259+
end: ext.isRunning
260+
? null
261+
: getDayJsInstance()(arg.event.end.toISOString())
262+
.utc()
263+
.tz(getUserTimezone(), true)
264+
.second(0)
265+
.utc()
266+
.format(),
247267
} as TimeEntry;
248268
await props.updateTimeEntry(updatedTimeEntry);
249269
emit('refresh');
@@ -337,11 +357,23 @@ const scrollToCurrentTime = () => {
337357
338358
onMounted(() => {
339359
scrollToCurrentTime();
360+
// Start interval to update running time entry
361+
currentTimeInterval = setInterval(() => {
362+
currentTime.value = getDayJsInstance()();
363+
}, 60000); // Update every minute
340364
});
341365
342366
onActivated(() => {
343367
scrollToCurrentTime();
344368
});
369+
370+
onUnmounted(() => {
371+
// Clean up interval
372+
if (currentTimeInterval) {
373+
clearInterval(currentTimeInterval);
374+
currentTimeInterval = null;
375+
}
376+
});
345377
</script>
346378

347379
<template>
@@ -699,4 +731,14 @@ onActivated(() => {
699731
.fullcalendar :deep(.fc-timegrid-event) {
700732
margin-left: 0 !important;
701733
}
734+
735+
/* Hide end resizer for running time entries */
736+
.fullcalendar :deep(.running-entry .fc-event-resizer-end) {
737+
display: none;
738+
}
739+
740+
.fullcalendar :deep(.running-entry) {
741+
border-bottom-left-radius: 0px;
742+
border-bottom-right-radius: 0px;
743+
}
702744
</style>

0 commit comments

Comments
 (0)