feat(time-tracking) : add calendar overlap columns, responsive cards, project filter, and auto-scroll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:20:07 +01:00
parent a9ba2f3815
commit 049275fd96
3 changed files with 604 additions and 96 deletions

View File

@@ -41,10 +41,17 @@
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.username }}</option>
</select>
<select
v-model="selectedProjectId"
class="rounded-md border border-neutral-200 px-3 py-1.5 text-sm"
>
<option :value="null">Projet</option>
<option v-for="p in projects" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
<select
v-model="selectedTypeId"
class="rounded-md border border-neutral-200 px-3 py-1.5 text-sm"
@change="loadEntries"
>
<option :value="null">Type</option>
<option v-for="t in types" :key="t.id" :value="t.id">{{ t.label }}</option>
@@ -105,6 +112,7 @@ const viewMode = ref<'week' | 'day'>('week')
const startDate = ref(getMonday(new Date()))
const selectedUserId = ref<number | null>(authStore.user?.id ?? null)
const selectedTypeId = ref<number | null>(null)
const selectedProjectId = ref<number | null>(null)
const entries = ref<TimeEntry[]>([])
const users = ref<UserData[]>([])
@@ -131,10 +139,14 @@ const currentMonthLabel = computed(() => {
})
const filteredEntries = computed(() => {
if (!selectedTypeId.value) return entries.value
return entries.value.filter((e) =>
e.types.some((t) => t.id === selectedTypeId.value)
)
let result = entries.value
if (selectedProjectId.value) {
result = result.filter((e) => e.project?.id === selectedProjectId.value)
}
if (selectedTypeId.value) {
result = result.filter((e) => e.types.some((t) => t.id === selectedTypeId.value))
}
return result
})
function getMonday(d: Date): Date {
@@ -173,13 +185,31 @@ function openEditDrawer(entry: TimeEntry) {
}
async function onMoveEntry(entry: TimeEntry, newStartedAt: string, newStoppedAt: string) {
await timeEntryService.update(entry.id, { startedAt: newStartedAt, stoppedAt: newStoppedAt })
await loadEntries()
// Optimistic update — instant visual feedback
const idx = entries.value.findIndex((e) => e.id === entry.id)
if (idx === -1) return
const original = entries.value[idx]!
entries.value[idx] = { ...original, startedAt: newStartedAt, stoppedAt: newStoppedAt }
try {
await timeEntryService.update(entry.id, { startedAt: newStartedAt, stoppedAt: newStoppedAt })
} catch {
entries.value[idx] = original
}
}
async function onResizeEntry(entry: TimeEntry, newStoppedAt: string) {
await timeEntryService.update(entry.id, { stoppedAt: newStoppedAt })
await loadEntries()
async function onResizeEntry(entry: TimeEntry, newStartedAt: string, newStoppedAt: string) {
// Optimistic update — instant visual feedback
const idx = entries.value.findIndex((e) => e.id === entry.id)
if (idx === -1) return
const original = entries.value[idx]!
entries.value[idx] = { ...original, startedAt: newStartedAt, stoppedAt: newStoppedAt }
try {
await timeEntryService.update(entry.id, { startedAt: newStartedAt, stoppedAt: newStoppedAt })
} catch {
entries.value[idx] = original
}
}
function onContextMenu(event: MouseEvent, entry: TimeEntry | null) {