feat(time-tracking) : add list view for time entries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:29:58 +01:00
parent 049275fd96
commit c15a10b36f
2 changed files with 120 additions and 9 deletions

View File

@@ -0,0 +1,104 @@
<template>
<div class="space-y-2">
<div v-if="entries.length === 0" class="rounded-lg border border-neutral-200 bg-neutral-50 py-12 text-center text-sm text-neutral-400">
Aucune activité pour cette période
</div>
<div
v-for="entry in sortedEntries"
:key="entry.id"
class="group flex items-center gap-4 rounded-lg border border-neutral-200 bg-white px-4 py-3 cursor-pointer transition hover:border-neutral-300 hover:shadow-sm"
@click="emit('editEntry', entry)"
>
<!-- Color bar -->
<div
class="h-10 w-1 shrink-0 rounded-full"
:style="{ backgroundColor: entry.project?.color ?? '#94a3b8' }"
/>
<!-- Main info -->
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-neutral-900">
{{ entry.title || 'Sans titre' }}
</span>
<span
v-for="type in entry.types"
:key="type.id"
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold text-white"
:style="{ backgroundColor: type.color }"
>
{{ type.label }}
</span>
</div>
<div class="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
<span v-if="entry.project">{{ entry.project.name }}</span>
<span v-if="entry.project && entry.description" class="text-neutral-300">·</span>
<span v-if="entry.description" class="truncate">{{ entry.description }}</span>
</div>
</div>
<!-- Time info -->
<div class="shrink-0 text-right">
<div class="text-sm font-semibold tabular-nums text-neutral-900">
{{ formatDuration(entry) }}
</div>
<div class="text-xs tabular-nums text-neutral-400">
{{ formatTime(entry.startedAt) }} {{ entry.stoppedAt ? formatTime(entry.stoppedAt) : '...' }}
</div>
</div>
<!-- Date -->
<div class="hidden shrink-0 text-xs text-neutral-400 sm:block">
{{ formatDate(entry.startedAt) }}
</div>
<!-- Delete action -->
<button
class="shrink-0 rounded-md p-1.5 text-neutral-300 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
title="Supprimer"
@click.stop="emit('deleteEntry', entry)"
>
<Icon name="mdi:delete-outline" size="18" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { TimeEntry } from '~/services/dto/time-entry'
const props = defineProps<{
entries: TimeEntry[]
}>()
const emit = defineEmits<{
(e: 'editEntry', entry: TimeEntry): void
(e: 'deleteEntry', entry: TimeEntry): void
}>()
const sortedEntries = computed(() => {
return [...props.entries].sort((a, b) => {
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
})
})
function formatDuration(entry: TimeEntry): string {
const start = new Date(entry.startedAt).getTime()
const end = entry.stoppedAt ? new Date(entry.stoppedAt).getTime() : Date.now()
const diff = end - start
const h = Math.floor(diff / 3600000)
const m = Math.floor((diff % 3600000) / 60000)
return m > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${h}h`
}
function formatTime(iso: string): string {
const d = new Date(iso)
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
}
function formatDate(iso: string): string {
const d = new Date(iso)
return d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short' })
}
</script>

View File

@@ -20,13 +20,13 @@
<Icon name="mdi:chevron-left" size="20" />
</button>
<button
v-for="mode in (['week', 'day'] as const)"
v-for="mode in (['week', 'day', 'list'] as const)"
:key="mode"
class="px-3 py-1 text-sm font-semibold transition"
:class="viewMode === mode ? 'bg-primary-500 text-white rounded' : 'text-neutral-500 hover:text-neutral-700'"
@click="viewMode = mode"
>
{{ mode === 'week' ? 'Semaine' : 'Jour' }}
{{ mode === 'week' ? 'Semaine' : mode === 'day' ? 'Jour' : 'Liste' }}
</button>
<button class="px-2 py-1 text-neutral-500 hover:text-neutral-700" @click="navigateNext">
<Icon name="mdi:chevron-right" size="20" />
@@ -59,7 +59,14 @@
</div>
<div class="mt-4">
<TimeEntryList
v-if="viewMode === 'list'"
:entries="filteredEntries"
@edit-entry="openEditDrawer"
@delete-entry="onDelete"
/>
<TimeTrackingCalendar
v-else
:entries="filteredEntries"
:start-date="startDate"
:view-mode="viewMode"
@@ -108,7 +115,7 @@ useHead({ title: 'Suivi des temps' })
const authStore = useAuthStore()
const timeEntryService = useTimeEntryService()
const viewMode = ref<'week' | 'day'>('week')
const viewMode = ref<'week' | 'day' | 'list'>('week')
const startDate = ref(getMonday(new Date()))
const selectedUserId = ref<number | null>(authStore.user?.id ?? null)
const selectedTypeId = ref<number | null>(null)
@@ -160,15 +167,15 @@ function getMonday(d: Date): Date {
function navigatePrev() {
const d = new Date(startDate.value)
d.setDate(d.getDate() - (viewMode.value === 'week' ? 7 : 1))
startDate.value = viewMode.value === 'week' ? getMonday(d) : d
d.setDate(d.getDate() - (viewMode.value === 'day' ? 1 : 7))
startDate.value = viewMode.value === 'day' ? d : getMonday(d)
loadEntries()
}
function navigateNext() {
const d = new Date(startDate.value)
d.setDate(d.getDate() + (viewMode.value === 'week' ? 7 : 1))
startDate.value = viewMode.value === 'week' ? getMonday(d) : d
d.setDate(d.getDate() + (viewMode.value === 'day' ? 1 : 7))
startDate.value = viewMode.value === 'day' ? d : getMonday(d)
loadEntries()
}
@@ -245,7 +252,7 @@ async function onDelete(entry: TimeEntry) {
async function loadEntries() {
const end = new Date(startDate.value)
end.setDate(end.getDate() + (viewMode.value === 'week' ? 7 : 1))
end.setDate(end.getDate() + (viewMode.value === 'day' ? 1 : 7))
entries.value = await timeEntryService.getByDateRange({
after: startDate.value.toISOString(),
@@ -274,7 +281,7 @@ onMounted(async () => {
})
watch(viewMode, () => {
startDate.value = viewMode.value === 'week' ? getMonday(startDate.value) : startDate.value
startDate.value = viewMode.value === 'day' ? startDate.value : getMonday(startDate.value)
loadEntries()
})
</script>