Files
Lesstime/frontend/stores/timer.ts
matthieu e4fc34b90f refactor : simplify codebase and fix critical issues
Backend:
- Add MCP Serializer to centralize entity-to-array conversion (~300 lines deduped)
- Fix race condition in task/ticket number generation (SELECT FOR UPDATE + transaction)
- Add unique constraint on task (project_id, number) with migration
- Fix MIME type validation: use server-detected finfo instead of client-supplied type
- Add allowlist of permitted MIME types for uploads
- Fix TaskDocumentDownloadController: allow ROLE_CLIENT access, add priority:1
- Fix notification sent even when ticket status unchanged
- Remove redundant exception constructors
- Simplify services (BookStackApi double fetch, TokenEncryptor, GiteaApi)
- Consolidate duplicate checks in processors

Frontend:
- Fix useApi isHandlingUnauthorized scope (module-level to prevent double 401 redirect)
- Fix client-tickets toast key copy-paste bug
- Merge duplicated tasks service methods (getByProject + getByProjectArchived)
- Extract shared uploadWithRelation helper in task-documents service
- Extract formatFileSize utility from duplicated component code
- Extract status transition logic into useClientTicketHelpers composable
- Remove dead code (unused router, handleLogout, empty script blocks)
- Merge duplicate watchers and onMounted calls
- Normalize arrow functions to function declarations per convention

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:09:16 +01:00

128 lines
3.6 KiB
TypeScript

import { defineStore } from 'pinia'
import type { TimeEntry } from '~/services/dto/time-entry'
import type { Task } from '~/services/dto/task'
import { useTimeEntryService } from '~/services/time-entries'
export const useTimerStore = defineStore('timer', () => {
const activeEntry = ref<TimeEntry | null>(null)
const pendingCompleteEntry = ref<TimeEntry | null>(null)
const now = ref(Date.now())
let intervalId: ReturnType<typeof setInterval> | null = null
const isRunning = computed(() => activeEntry.value !== null)
const elapsed = computed(() => {
if (!activeEntry.value) return 0
const start = new Date(activeEntry.value.startedAt).getTime()
return Math.floor((now.value - start) / 1000)
})
const elapsedFormatted = computed(() => {
const total = elapsed.value
const h = Math.floor(total / 3600)
const m = Math.floor((total % 3600) / 60)
const s = total % 60
return [h, m, s].map((v) => String(v).padStart(2, '0')).join(':')
})
function startTicking() {
stopTicking()
now.value = Date.now()
intervalId = setInterval(() => {
now.value = Date.now()
}, 1000)
}
function stopTicking() {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
async function fetchActive() {
const { getActive } = useTimeEntryService()
activeEntry.value = await getActive()
if (activeEntry.value) {
startTicking()
} else {
stopTicking()
}
}
async function start() {
const authStore = useAuthStore()
if (!authStore.user) return
if (isRunning.value) {
await stop()
}
const { create } = useTimeEntryService()
activeEntry.value = await create({
startedAt: new Date().toISOString(),
user: `/api/users/${authStore.user.id}`,
})
startTicking()
}
function toIri<T extends { '@id'?: string; id: number }>(entity: T | string, prefix: string): string {
if (typeof entity === 'string') return entity
return entity['@id'] ?? `${prefix}/${entity.id}`
}
async function startFromTask(task: Task) {
const authStore = useAuthStore()
if (!authStore.user) return
if (isRunning.value) {
await stop()
}
const { create } = useTimeEntryService()
activeEntry.value = await create({
startedAt: new Date().toISOString(),
user: `/api/users/${authStore.user.id}`,
title: task.title,
project: task.project ? toIri(task.project, '/api/projects') : null,
task: toIri(task, '/api/tasks'),
tags: task.tags?.map(t => toIri(t, '/api/task_tags')) ?? [],
})
startTicking()
}
async function stop() {
if (!activeEntry.value) return
const wasEmpty = !activeEntry.value.task
const { update } = useTimeEntryService()
const stoppedEntry = await update(activeEntry.value.id, {
stoppedAt: new Date().toISOString(),
})
activeEntry.value = null
stopTicking()
if (wasEmpty) {
pendingCompleteEntry.value = stoppedEntry
}
}
function clearPendingEntry() {
pendingCompleteEntry.value = null
}
return {
activeEntry,
pendingCompleteEntry,
isRunning,
elapsed,
elapsedFormatted,
fetchActive,
start,
startFromTask,
stop,
clearPendingEntry,
}
})