diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index e7c2342..ebce0f0 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -63,5 +63,10 @@ "created": "Utilisateur créé avec succès.", "updated": "Utilisateur mis à jour avec succès.", "deleted": "Utilisateur supprimé avec succès." + }, + "timeEntries": { + "created": "Temps enregistré", + "updated": "Temps modifié", + "deleted": "Temps supprimé" } } diff --git a/frontend/services/dto/task.ts b/frontend/services/dto/task.ts index 8b103b4..f3e47f2 100644 --- a/frontend/services/dto/task.ts +++ b/frontend/services/dto/task.ts @@ -4,6 +4,7 @@ import type { TaskPriority } from './task-priority' import type { TaskType } from './task-type' import type { TaskGroup } from './task-group' import type { UserData } from './user-data' +import type { Project } from './project' export type Task = { id: number @@ -15,6 +16,7 @@ export type Task = { priority: TaskPriority | null assignee: UserData | null group: TaskGroup | null + project: Project | null types: TaskType[] } diff --git a/frontend/services/dto/time-entry.ts b/frontend/services/dto/time-entry.ts new file mode 100644 index 0000000..9b3fb84 --- /dev/null +++ b/frontend/services/dto/time-entry.ts @@ -0,0 +1,28 @@ +import type { UserData } from './user-data' +import type { Project } from './project' +import type { Task } from './task' +import type { TaskType } from './task-type' + +export type TimeEntry = { + id: number + '@id'?: string + title: string | null + description: string | null + startedAt: string + stoppedAt: string | null + user: UserData + project: Project | null + task: Task | null + types: TaskType[] +} + +export type TimeEntryWrite = { + title?: string | null + description?: string | null + startedAt: string + stoppedAt?: string | null + user: string + project?: string | null + task?: string | null + types?: string[] +} diff --git a/frontend/services/time-entries.ts b/frontend/services/time-entries.ts new file mode 100644 index 0000000..a37d766 --- /dev/null +++ b/frontend/services/time-entries.ts @@ -0,0 +1,53 @@ +import type { TimeEntry, TimeEntryWrite } from './dto/time-entry' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export function useTimeEntryService() { + const api = useApi() + + async function getByDateRange(params: { + after: string + before: string + user?: number + types?: number[] + }): Promise { + const query: Record = { + 'startedAt[after]': params.after, + 'startedAt[before]': params.before, + } + if (params.user) { + query.user = `/api/users/${params.user}` + } + const data = await api.get>('/time_entries', query) + return extractHydraMembers(data) + } + + async function getActive(): Promise { + try { + const result = await api.get('/time_entries/active', {}, { toast: false }) + return result ?? null + } catch { + return null + } + } + + async function create(payload: TimeEntryWrite): Promise { + return api.post('/time_entries', payload as Record, { + toastSuccessKey: 'timeEntries.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/time_entries/${id}`, payload as Record, { + toastSuccessKey: 'timeEntries.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/time_entries/${id}`, {}, { + toastSuccessKey: 'timeEntries.deleted', + }) + } + + return { getByDateRange, getActive, create, update, remove } +} diff --git a/frontend/stores/timer.ts b/frontend/stores/timer.ts new file mode 100644 index 0000000..463b4c0 --- /dev/null +++ b/frontend/stores/timer.ts @@ -0,0 +1,109 @@ +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(null) + const now = ref(Date.now()) + let intervalId: ReturnType | 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() + } + + 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?.['@id'] ?? `/api/projects/${task.project?.id}`, + task: task['@id'] ?? `/api/tasks/${task.id}`, + types: task.types.map((t) => t['@id'] ?? `/api/task_types/${t.id}`), + }) + startTicking() + } + + async function stop() { + if (!activeEntry.value) return + + const { update } = useTimeEntryService() + await update(activeEntry.value.id, { + stoppedAt: new Date().toISOString(), + }) + activeEntry.value = null + stopTicking() + } + + return { + activeEntry, + isRunning, + elapsed, + elapsedFormatted, + fetchActive, + start, + startFromTask, + stop, + } +})