feat(time-tracking) : add pending complete entry flow and redesign sidebar timer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:19:58 +01:00
parent d4c5660ba6
commit 7484ce3e45
3 changed files with 91 additions and 26 deletions

View File

@@ -1,27 +1,20 @@
<template>
<div class="flex items-center gap-2">
<button
class="flex items-center justify-center rounded-full transition-colors"
:class="timerStore.isRunning
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-green-500 hover:bg-green-600 text-white'"
style="width: 32px; height: 32px;"
:title="timerStore.isRunning ? 'Arrêter le timer' : 'Démarrer un timer'"
@click="timerStore.isRunning ? timerStore.stop() : timerStore.start()"
>
<Icon
:name="timerStore.isRunning ? 'mdi:stop' : 'mdi:play'"
size="18"
/>
</button>
<span
v-if="!collapsed"
class="font-mono text-sm font-bold"
:class="timerStore.isRunning ? 'text-white' : 'text-neutral-400'"
>
<button
class="flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-semibold text-white transition"
:class="timerStore.isRunning
? 'bg-red-500 hover:bg-red-600'
: 'bg-primary-500 hover:bg-primary-600'"
:title="timerStore.isRunning ? 'Arrêter le timer' : 'Démarrer un timer'"
@click="timerStore.isRunning ? timerStore.stop() : timerStore.start()"
>
<Icon
:name="timerStore.isRunning ? 'mdi:stop' : 'mdi:play'"
size="16"
/>
<span v-if="!collapsed" class="font-mono tracking-wide">
{{ timerStore.elapsedFormatted }}
</span>
</div>
</button>
</template>
<script setup lang="ts">

View File

@@ -103,11 +103,24 @@
</main>
</div>
</div>
<TimeEntryDrawer
v-model="completeDrawerOpen"
:entry="timerStore.pendingCompleteEntry"
:users="refData.users"
:projects="refData.projects"
:types="refData.types"
@saved="onCompleteSaved"
/>
</div>
</template>
<script setup lang="ts">
import {useAppVersion} from "~/composables/useAppVersion";
import type { UserData } from '~/services/dto/user-data'
import type { Project } from '~/services/dto/project'
import type { TaskType } from '~/services/dto/task-type'
import { useAppVersion } from '~/composables/useAppVersion'
import { extractHydraMembers } from '~/utils/api'
const auth = useAuthStore()
const ui = useUiStore()
@@ -125,6 +138,50 @@ onMounted(() => {
timerStore.fetchActive()
})
const completeDrawerOpen = ref(false)
const refData = reactive({
users: [] as UserData[],
projects: [] as Project[],
types: [] as TaskType[],
loaded: false,
})
async function loadRefData() {
if (refData.loaded) return
const api = useApi()
const [usersData, projectsData, typesData] = await Promise.all([
api.get<any>('/users'),
api.get<any>('/projects'),
api.get<any>('/task_types'),
])
refData.users = extractHydraMembers(usersData)
refData.projects = extractHydraMembers(projectsData)
refData.types = extractHydraMembers(typesData)
refData.loaded = true
}
watch(() => timerStore.pendingCompleteEntry, async (entry) => {
if (entry) {
await loadRefData()
completeDrawerOpen.value = true
}
})
watch(completeDrawerOpen, (open) => {
if (!open) {
nextTick(() => {
timerStore.clearPendingEntry()
})
}
})
function onCompleteSaved() {
completeDrawerOpen.value = false
nextTick(() => {
timerStore.clearPendingEntry()
})
}
const handleLogout = async () => {
await auth.logout()
await navigateTo('/login')

View File

@@ -5,6 +5,7 @@ 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
@@ -78,9 +79,11 @@ export const useTimerStore = defineStore('timer', () => {
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}`),
project: task.project
? (typeof task.project === 'string' ? task.project : (task.project['@id'] ?? (task.project.id ? `/api/projects/${task.project.id}` : null)))
: null,
task: typeof task === 'string' ? task : (task['@id'] ?? `/api/tasks/${task.id}`),
types: task.types?.map((t) => typeof t === 'string' ? t : (t['@id'] ?? `/api/task_types/${t.id}`)) ?? [],
})
startTicking()
}
@@ -88,16 +91,27 @@ export const useTimerStore = defineStore('timer', () => {
async function stop() {
if (!activeEntry.value) return
const wasEmpty = !activeEntry.value.task
const { update } = useTimeEntryService()
await update(activeEntry.value.id, {
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,
@@ -105,5 +119,6 @@ export const useTimerStore = defineStore('timer', () => {
start,
startFromTask,
stop,
clearPendingEntry,
}
})