mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
add support for currently running time entry
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user