add support for currently running time entry

This commit is contained in:
Gregor Vostrak
2025-11-19 16:08:32 +01:00
parent 249ab67ac8
commit 5b491b0da2

View File

@@ -13,6 +13,7 @@ import {
nextTick, nextTick,
onMounted, onMounted,
onActivated, onActivated,
onUnmounted,
} from 'vue'; } from 'vue';
import chroma from 'chroma-js'; import chroma from 'chroma-js';
import { useCssVariable } from '@/utils/useCssVariable'; import { useCssVariable } from '@/utils/useCssVariable';
@@ -37,7 +38,10 @@ import type {
} from '@/packages/api/src'; } from '@/packages/api/src';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
type CalendarExtendedProps = { timeEntry: TimeEntry } & Record<string, unknown>; type CalendarExtendedProps = { timeEntry: TimeEntry; isRunning?: boolean } & Record<
string,
unknown
>;
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'dates-change', payload: { start: Date; end: Date }): void; (e: 'dates-change', payload: { start: Date; end: Date }): void;
@@ -77,6 +81,10 @@ const selectedTimeEntry = ref<TimeEntry | null>(null);
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null); const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null);
// Reactive "now" for running time entry - updates every minute
const currentTime = ref(getDayJsInstance()());
let currentTimeInterval: ReturnType<typeof setInterval> | null = null;
// Inject organization data for settings // Inject organization data for settings
const organization = inject<ComputedRef<Organization>>('organization'); const organization = inject<ComputedRef<Organization>>('organization');
@@ -118,47 +126,52 @@ const events = computed(() => {
const themeBackground = (() => { const themeBackground = (() => {
return cssBackground.value?.trim(); return cssBackground.value?.trim();
})(); })();
return props.timeEntries return props.timeEntries?.map((timeEntry) => {
?.filter((timeEntry) => timeEntry.end !== null) const isRunning = timeEntry.end === null;
?.map((timeEntry) => { const project = props.projects.find((p) => p.id === timeEntry.project_id);
const project = props.projects.find((p) => p.id === timeEntry.project_id); const client = props.clients.find((c) => c.id === project?.client_id);
const client = props.clients.find((c) => c.id === project?.client_id); const task = props.tasks.find((t) => t.id === timeEntry.task_id);
const task = props.tasks.find((t) => t.id === timeEntry.task_id);
const duration = getDayJsInstance()(timeEntry.end!).diff(
getDayJsInstance()(timeEntry.start),
'minutes'
);
const title = timeEntry.description || 'No description'; // For running entries, use current time as end
const effectiveEnd = isRunning ? currentTime.value : getDayJsInstance()(timeEntry.end!);
const duration = effectiveEnd.diff(getDayJsInstance()(timeEntry.start), 'minutes');
const baseColor = project?.color || '#6B7280'; const title = timeEntry.description || 'No description';
const backgroundColor = chroma.mix(baseColor, themeBackground, 0.65, 'lab').hex();
const borderColor = chroma.mix(baseColor, themeBackground, 0.5, 'lab').hex();
// For 0-duration events, display them with minimum visual duration but preserve actual duration const baseColor = project?.color || '#6B7280';
const startTime = getLocalizedDayJs(timeEntry.start); const backgroundColor = chroma.mix(baseColor, themeBackground, 0.65, 'lab').hex();
const endTime = const borderColor = chroma.mix(baseColor, themeBackground, 0.5, 'lab').hex();
duration === 0
? startTime.add(1, 'second') // Show as 1 second for minimal visibility
: getLocalizedDayJs(timeEntry.end!);
return { // For 0-duration events, display them with minimum visual duration but preserve actual duration
id: timeEntry.id, const startTime = getLocalizedDayJs(timeEntry.start);
start: startTime.format(), const endTime =
end: endTime.format(), duration === 0
title, ? startTime.add(1, 'second') // Show as 1 second for minimal visibility
backgroundColor, : isRunning
borderColor, ? getLocalizedDayJs(currentTime.value.toISOString())
textColor: 'var(--foreground)', : getLocalizedDayJs(timeEntry.end!);
extendedProps: {
timeEntry, return {
project, id: timeEntry.id,
client, start: startTime.format(),
task, end: endTime.format(),
duration, title,
}, backgroundColor,
}; borderColor,
}); textColor: 'var(--foreground)',
// For running entries: disable dragging and resizing
startEditable: !isRunning,
classNames: isRunning ? ['running-entry'] : [],
extendedProps: {
timeEntry,
project,
client,
task,
duration,
isRunning,
},
};
});
}); });
// Daily totals used in day header // Daily totals used in day header
@@ -199,6 +212,10 @@ function handleDateSelect(arg: { start: Date; end: Date }) {
function handleEventClick(arg: EventClickArg) { function handleEventClick(arg: EventClickArg) {
const ext = arg.event.extendedProps as CalendarExtendedProps; const ext = arg.event.extendedProps as CalendarExtendedProps;
// Don't open edit modal for running time entries
if (ext.isRunning) {
return;
}
selectedTimeEntry.value = ext.timeEntry; selectedTimeEntry.value = ext.timeEntry;
showEditTimeEntryModal.value = true; showEditTimeEntryModal.value = true;
} }
@@ -238,12 +255,15 @@ async function handleEventResize(arg: EventChangeArg) {
.second(0) .second(0)
.utc() .utc()
.format(), .format(),
end: getDayJsInstance()(arg.event.end.toISOString()) // Preserve null end for running entries
.utc() end: ext.isRunning
.tz(getUserTimezone(), true) ? null
.second(0) : getDayJsInstance()(arg.event.end.toISOString())
.utc() .utc()
.format(), .tz(getUserTimezone(), true)
.second(0)
.utc()
.format(),
} as TimeEntry; } as TimeEntry;
await props.updateTimeEntry(updatedTimeEntry); await props.updateTimeEntry(updatedTimeEntry);
emit('refresh'); emit('refresh');
@@ -337,11 +357,23 @@ const scrollToCurrentTime = () => {
onMounted(() => { onMounted(() => {
scrollToCurrentTime(); scrollToCurrentTime();
// Start interval to update running time entry
currentTimeInterval = setInterval(() => {
currentTime.value = getDayJsInstance()();
}, 60000); // Update every minute
}); });
onActivated(() => { onActivated(() => {
scrollToCurrentTime(); scrollToCurrentTime();
}); });
onUnmounted(() => {
// Clean up interval
if (currentTimeInterval) {
clearInterval(currentTimeInterval);
currentTimeInterval = null;
}
});
</script> </script>
<template> <template>
@@ -699,4 +731,14 @@ onActivated(() => {
.fullcalendar :deep(.fc-timegrid-event) { .fullcalendar :deep(.fc-timegrid-event) {
margin-left: 0 !important; margin-left: 0 !important;
} }
/* Hide end resizer for running time entries */
.fullcalendar :deep(.running-entry .fc-event-resizer-end) {
display: none;
}
.fullcalendar :deep(.running-entry) {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
</style> </style>