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:
@@ -1,27 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<button
|
||||||
<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="flex items-center justify-center rounded-full transition-colors"
|
:class="timerStore.isRunning
|
||||||
:class="timerStore.isRunning
|
? 'bg-red-500 hover:bg-red-600'
|
||||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
: 'bg-primary-500 hover:bg-primary-600'"
|
||||||
: 'bg-green-500 hover:bg-green-600 text-white'"
|
:title="timerStore.isRunning ? 'Arrêter le timer' : 'Démarrer un timer'"
|
||||||
style="width: 32px; height: 32px;"
|
@click="timerStore.isRunning ? timerStore.stop() : timerStore.start()"
|
||||||
: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'"
|
||||||
<Icon
|
size="16"
|
||||||
:name="timerStore.isRunning ? 'mdi:stop' : 'mdi:play'"
|
/>
|
||||||
size="18"
|
<span v-if="!collapsed" class="font-mono tracking-wide">
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
v-if="!collapsed"
|
|
||||||
class="font-mono text-sm font-bold"
|
|
||||||
:class="timerStore.isRunning ? 'text-white' : 'text-neutral-400'"
|
|
||||||
>
|
|
||||||
{{ timerStore.elapsedFormatted }}
|
{{ timerStore.elapsedFormatted }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -103,11 +103,24 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TimeEntryDrawer
|
||||||
|
v-model="completeDrawerOpen"
|
||||||
|
:entry="timerStore.pendingCompleteEntry"
|
||||||
|
:users="refData.users"
|
||||||
|
:projects="refData.projects"
|
||||||
|
:types="refData.types"
|
||||||
|
@saved="onCompleteSaved"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 auth = useAuthStore()
|
||||||
const ui = useUiStore()
|
const ui = useUiStore()
|
||||||
@@ -125,6 +138,50 @@ onMounted(() => {
|
|||||||
timerStore.fetchActive()
|
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 () => {
|
const handleLogout = async () => {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
await navigateTo('/login')
|
await navigateTo('/login')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTimeEntryService } from '~/services/time-entries'
|
|||||||
|
|
||||||
export const useTimerStore = defineStore('timer', () => {
|
export const useTimerStore = defineStore('timer', () => {
|
||||||
const activeEntry = ref<TimeEntry | null>(null)
|
const activeEntry = ref<TimeEntry | null>(null)
|
||||||
|
const pendingCompleteEntry = ref<TimeEntry | null>(null)
|
||||||
const now = ref(Date.now())
|
const now = ref(Date.now())
|
||||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
@@ -78,9 +79,11 @@ export const useTimerStore = defineStore('timer', () => {
|
|||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
user: `/api/users/${authStore.user.id}`,
|
user: `/api/users/${authStore.user.id}`,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
project: task.project?.['@id'] ?? `/api/projects/${task.project?.id}`,
|
project: task.project
|
||||||
task: task['@id'] ?? `/api/tasks/${task.id}`,
|
? (typeof task.project === 'string' ? task.project : (task.project['@id'] ?? (task.project.id ? `/api/projects/${task.project.id}` : null)))
|
||||||
types: task.types.map((t) => t['@id'] ?? `/api/task_types/${t.id}`),
|
: 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()
|
startTicking()
|
||||||
}
|
}
|
||||||
@@ -88,16 +91,27 @@ export const useTimerStore = defineStore('timer', () => {
|
|||||||
async function stop() {
|
async function stop() {
|
||||||
if (!activeEntry.value) return
|
if (!activeEntry.value) return
|
||||||
|
|
||||||
|
const wasEmpty = !activeEntry.value.task
|
||||||
|
|
||||||
const { update } = useTimeEntryService()
|
const { update } = useTimeEntryService()
|
||||||
await update(activeEntry.value.id, {
|
const stoppedEntry = await update(activeEntry.value.id, {
|
||||||
stoppedAt: new Date().toISOString(),
|
stoppedAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
activeEntry.value = null
|
activeEntry.value = null
|
||||||
stopTicking()
|
stopTicking()
|
||||||
|
|
||||||
|
if (wasEmpty) {
|
||||||
|
pendingCompleteEntry.value = stoppedEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingEntry() {
|
||||||
|
pendingCompleteEntry.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeEntry,
|
activeEntry,
|
||||||
|
pendingCompleteEntry,
|
||||||
isRunning,
|
isRunning,
|
||||||
elapsed,
|
elapsed,
|
||||||
elapsedFormatted,
|
elapsedFormatted,
|
||||||
@@ -105,5 +119,6 @@ export const useTimerStore = defineStore('timer', () => {
|
|||||||
start,
|
start,
|
||||||
startFromTask,
|
startFromTask,
|
||||||
stop,
|
stop,
|
||||||
|
clearPendingEntry,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user