import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import { api } from '@/packages/api/src'; import type { TimeEntry } from '@/packages/api/src'; import dayjs, { Dayjs } from 'dayjs'; import utc from 'dayjs/plugin/utc'; import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentUserId, } from '@/utils/useUser'; import { useLocalStorage } from '@vueuse/core'; import { useNotificationsStore } from '@/utils/notification'; import { useQueryClient } from '@tanstack/vue-query'; dayjs.extend(utc); const emptyTimeEntry = { id: '', description: '', user_id: '', start: '', end: null, duration: null, task_id: null, project_id: null, tags: [], billable: false, organization_id: '', } as TimeEntry; export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => { const currentTimeEntry = ref({ ...emptyTimeEntry }); const { handleApiRequestNotifications } = useNotificationsStore(); const queryClient = useQueryClient(); useLocalStorage('solidtime/current-time-entry', currentTimeEntry, { deep: true, }); function $reset() { currentTimeEntry.value = { ...emptyTimeEntry }; } const now = ref(null); const interval = ref | null>(null); function startLiveTimer() { stopLiveTimer(); now.value = dayjs().utc(); interval.value = setInterval(() => { now.value = dayjs().utc(); }, 1000); } function stopLiveTimer() { if (interval.value !== null) { clearInterval(interval.value); } } async function fetchCurrentTimeEntry() { const organizationId = getCurrentOrganizationId(); if (organizationId) { try { const timeEntriesResponse = await api.getMyActiveTimeEntry({}); if (timeEntriesResponse?.data) { if (timeEntriesResponse.data) { currentTimeEntry.value = timeEntriesResponse.data; if ( currentTimeEntry.value.start !== '' && currentTimeEntry.value.end === null ) { startLiveTimer(); } } else { // No active time entry on server // Only reset if we had a previously started timer (has an ID) // Don't reset if user is preparing a new time entry (no ID yet) if (currentTimeEntry.value.id !== '') { currentTimeEntry.value = { ...emptyTimeEntry }; stopLiveTimer(); } } } } catch { // API error (e.g., 404 when no active time entry) // Only reset if we had a previously started timer (has an ID) // Don't reset if user is preparing a new time entry (no ID yet) if (currentTimeEntry.value.id !== '') { currentTimeEntry.value = { ...emptyTimeEntry }; stopLiveTimer(); } } } else { throw new Error( 'Failed to fetch current time entry because organization ID is missing.' ); } } async function startTimer() { const organization = getCurrentOrganizationId(); const membership = getCurrentMembershipId(); if (organization && membership) { const startTime = currentTimeEntry.value.start !== '' ? currentTimeEntry.value.start : dayjs().utc().format(); const response = await handleApiRequestNotifications( () => api.createTimeEntry( { member_id: membership, start: startTime, description: currentTimeEntry.value?.description, project_id: currentTimeEntry.value?.project_id, task_id: currentTimeEntry.value?.task_id, billable: currentTimeEntry.value.billable, tags: currentTimeEntry.value?.tags, }, { params: { organization: organization } } ), 'Timer started!' ); if (response?.data) { currentTimeEntry.value = response.data; } } else { throw new Error( 'Failed to fetch current time entry because organization ID is missing.' ); } } async function stopTimer() { const user = getCurrentUserId(); const organization = getCurrentOrganizationId(); if (organization) { const currentDateTime = dayjs().utc().format(); await handleApiRequestNotifications( () => api.updateTimeEntry( { user_id: user, start: currentTimeEntry.value.start, end: currentDateTime, }, { params: { organization: organization, timeEntry: currentTimeEntry.value.id, }, } ), 'Timer stopped!' ); $reset(); } else { throw new Error('Failed to stop current timer because organization ID is missing.'); } } async function updateTimer() { const user = getCurrentUserId(); const organization = getCurrentOrganizationId(); if (organization) { const response = await handleApiRequestNotifications( () => api.updateTimeEntry( { description: currentTimeEntry.value.description, user_id: user, project_id: currentTimeEntry.value.project_id, task_id: currentTimeEntry.value.task_id, start: currentTimeEntry.value.start, billable: currentTimeEntry.value.billable, end: currentTimeEntry.value.end, tags: currentTimeEntry.value.tags, }, { params: { organization: organization, timeEntry: currentTimeEntry.value.id, }, } ), 'Time entry updated!' ); if (response?.data) { if (response.data.end === null) { currentTimeEntry.value = response.data; } else { $reset(); stopLiveTimer(); } } } else { throw new Error( 'Failed to fetch current time entry because organization ID is missing.' ); } } const isActive = computed(() => { if (currentTimeEntry.value) { return ( currentTimeEntry.value.start !== '' && currentTimeEntry.value.start !== null && currentTimeEntry.value.end === null ); } return false; }); async function setActiveState(newState: boolean) { if (newState) { startLiveTimer(); await startTimer(); } else { stopLiveTimer(); await stopTimer(); } queryClient.invalidateQueries({ queryKey: ['timeEntries'] }); } return { currentTimeEntry, fetchCurrentTimeEntry, updateTimer, isActive, startLiveTimer, stopLiveTimer, now, setActiveState, $reset, }; });