@@ -13,6 +13,7 @@ import {
1313 nextTick ,
1414 onMounted ,
1515 onActivated ,
16+ onUnmounted ,
1617} from ' vue' ;
1718import chroma from ' chroma-js' ;
1819import { useCssVariable } from ' @/utils/useCssVariable' ;
@@ -37,7 +38,10 @@ import type {
3738} from ' @/packages/api/src' ;
3839import 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
4246const emit = defineEmits <{
4347 (e : ' dates-change' , payload : { start: Date ; end: Date }): void ;
@@ -77,6 +81,10 @@ const selectedTimeEntry = ref<TimeEntry | null>(null);
7781
7882const 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
8189const 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
200213function 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
338358onMounted (() => {
339359 scrollToCurrentTime ();
360+ // Start interval to update running time entry
361+ currentTimeInterval = setInterval (() => {
362+ currentTime .value = getDayJsInstance ()();
363+ }, 60000 ); // Update every minute
340364});
341365
342366onActivated (() => {
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