add break time entries and simplified time tracker ui

This commit is contained in:
Gregor Vostrak
2026-07-21 16:48:37 +02:00
parent c07c62bfab
commit 64ca1e9115
128 changed files with 6252 additions and 437 deletions

View File

@@ -26,9 +26,33 @@ const emptyTimeEntry = {
project_id: null,
tags: [],
billable: false,
type: 'work',
organization_id: '',
} as TimeEntry;
export type ResumeTimeEntryContext = {
description: string | null;
project_id: string | null;
task_id: string | null;
tags: string[];
billable: boolean;
};
/**
* Time entries are loaded newest-first. Ignore scheduled entries so resuming after a break
* always uses the latest work entry that has actually started.
*/
export function getLastWorkTimeEntry(
timeEntries: TimeEntry[],
currentTime: Dayjs = dayjs().utc()
): TimeEntry | null {
return (
timeEntries.find(
(entry) => entry.type === 'work' && !dayjs(entry.start).utc().isAfter(currentTime)
) ?? null
);
}
export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
const currentTimeEntry = ref<TimeEntry>({ ...emptyTimeEntry });
const { handleApiRequestNotifications } = useNotificationsStore();
@@ -117,6 +141,7 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
project_id: currentTimeEntry.value?.project_id,
task_id: currentTimeEntry.value?.task_id,
billable: currentTimeEntry.value.billable,
type: currentTimeEntry.value?.type ?? 'work',
tags: currentTimeEntry.value?.tags,
},
{ params: { organization: organization } }
@@ -133,11 +158,11 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
}
}
async function stopTimer() {
async function stopTimer(endTime?: string) {
const user = getCurrentUserId();
const organization = getCurrentOrganizationId();
if (organization) {
const currentDateTime = dayjs().utc().format();
const currentDateTime = endTime ?? dayjs().utc().format();
await handleApiRequestNotifications(
() =>
api.updateTimeEntry(
@@ -161,6 +186,58 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
}
}
async function startBreak() {
const organization = getCurrentOrganizationId();
const membership = getCurrentMembershipId();
if (!organization || !membership) {
throw new Error('Failed to start break because organization ID is missing.');
}
// One timestamp for both the work end and the break start, so the entries touch exactly
const switchTime = dayjs().utc().format();
if (isActive.value && currentTimeEntry.value.type !== 'break') {
await stopTimer(switchTime);
}
startLiveTimer();
const response = await handleApiRequestNotifications(
() =>
api.createTimeEntry(
{
member_id: membership,
start: switchTime,
billable: false,
type: 'break',
},
{ params: { organization: organization } }
),
'Break started!',
'Your timer was stopped, but the break could not be started.'
);
if (response?.data) {
currentTimeEntry.value = response.data;
} else {
stopLiveTimer();
}
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
}
async function resumeWorkAfterBreak(context: ResumeTimeEntryContext) {
if (isActive.value && currentTimeEntry.value.type === 'break') {
stopLiveTimer();
await stopTimer();
}
currentTimeEntry.value = {
...emptyTimeEntry,
description: context.description ?? '',
project_id: context.project_id,
task_id: context.task_id,
tags: context.tags ?? [],
billable: context.billable,
};
startLiveTimer();
await startTimer();
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
}
async function updateTimer() {
const user = getCurrentUserId();
const organization = getCurrentOrganizationId();
@@ -213,6 +290,10 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
return false;
});
const isOnBreak = computed(() => {
return isActive.value && currentTimeEntry.value.type === 'break';
});
async function setActiveState(newState: boolean) {
if (newState) {
startLiveTimer();
@@ -229,6 +310,9 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
fetchCurrentTimeEntry,
updateTimer,
isActive,
isOnBreak,
startBreak,
resumeWorkAfterBreak,
startLiveTimer,
stopLiveTimer,
now,