feat : date filter, project drawer, and misc frontend improvements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 20:25:26 +01:00
parent 046ee396d3
commit f09ef67117
10 changed files with 852 additions and 9 deletions

View File

@@ -0,0 +1,385 @@
# Date Filter Component Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a reusable date filter component to the time-tracking page using `@vuepic/vue-datepicker`, enabling filtering by single day or date range.
**Architecture:** A wrapper component `DateFilter.vue` encapsulates `VueDatePicker` with project-consistent styling. It integrates into the existing filter bar on the time-tracking page. Filtering is client-side, matching the existing project/tag filter pattern.
**Tech Stack:** Vue 3, @vuepic/vue-datepicker, Tailwind CSS, @nuxtjs/i18n
---
## Chunk 1: Setup and Component
### Task 1: Install @vuepic/vue-datepicker and configure Nuxt
**Files:**
- Modify: `frontend/package.json`
- Modify: `frontend/nuxt.config.ts:1-66`
- [ ] **Step 1: Install the package**
Run inside the PHP container (where Node is available):
```bash
cd /home/r-dev/Lesstime/frontend && npm install @vuepic/vue-datepicker
```
- [ ] **Step 2: Add transpile config to nuxt.config.ts**
In `frontend/nuxt.config.ts`, add `build.transpile` after the `typescript` block:
```typescript
export default defineNuxtConfig({
// ... existing config ...
typescript: {
strict: true
},
build: {
transpile: ['@vuepic/vue-datepicker']
}
})
```
- [ ] **Step 3: Commit**
```bash
git add frontend/package.json frontend/package-lock.json frontend/nuxt.config.ts
git commit -m "feat(frontend) : add @vuepic/vue-datepicker dependency"
```
---
### Task 2: Add i18n translations
**Files:**
- Modify: `frontend/i18n/locales/fr.json:167-170`
- [ ] **Step 1: Add date filter translations to fr.json**
In `frontend/i18n/locales/fr.json`, add keys inside the existing `"common"` block:
```json
"common": {
"cancel": "Annuler",
"loading": "Chargement...",
"dateFilter": "Date",
"today": "Aujourd'hui",
"thisWeek": "Cette semaine",
"clear": "Effacer"
}
```
- [ ] **Step 2: Commit**
```bash
git add frontend/i18n/locales/fr.json
git commit -m "feat(frontend) : add date filter i18n translations"
```
---
### Task 3: Create DateFilter.vue component
**Files:**
- Create: `frontend/components/ui/DateFilter.vue`
- [ ] **Step 1: Create the component**
Create `frontend/components/ui/DateFilter.vue`:
```vue
<template>
<div class="date-filter">
<VueDatePicker
v-model="internalValue"
:range="isRange"
:enable-time-picker="false"
:text-input="textInputConfig"
:locale="'fr'"
:format="formatDate"
:preview-format="formatDate"
auto-apply
:multi-calendars="false"
position="left"
@update:model-value="onUpdate"
@cleared="onClear"
>
<template #dp-input="{ value, onInput, onEnter, onTab, onClear, openMenu }">
<div class="relative">
<input
:value="value"
class="w-full cursor-pointer rounded-md border border-neutral-300 bg-white px-3 py-[7px] text-sm text-neutral-700 outline-none transition placeholder:text-neutral-400 focus:border-primary-500"
:placeholder="placeholder || t('common.dateFilter')"
readonly
@click="openMenu"
@input="onInput"
@keydown.enter="onEnter"
@keydown.tab="onTab"
/>
<button
v-if="value"
class="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
@click.stop="onClear"
>
<Icon name="mdi:close-circle" size="16" />
</button>
<Icon
v-else
name="mdi:calendar"
size="16"
class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400"
/>
</div>
</template>
<template #action-buttons>
<div class="flex gap-2 px-3 pb-2">
<button
class="rounded px-2 py-1 text-xs font-medium text-primary-500 hover:bg-primary-500/10 transition"
@click="selectToday"
>
{{ t('common.today') }}
</button>
<button
class="rounded px-2 py-1 text-xs font-medium text-primary-500 hover:bg-primary-500/10 transition"
@click="selectThisWeek"
>
{{ t('common.thisWeek') }}
</button>
</div>
</template>
</VueDatePicker>
</div>
</template>
<script setup lang="ts">
import VueDatePicker from '@vuepic/vue-datepicker'
import '@vuepic/vue-datepicker/dist/main.css'
const { t } = useI18n()
const props = defineProps<{
modelValue?: Date | [Date, Date] | null
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: Date | [Date, Date] | null]
}>()
const isRange = ref(false)
const internalValue = ref<Date | Date[] | null>(null)
const firstClick = ref<Date | null>(null)
const textInputConfig = {
enterSubmit: true,
tabSubmit: true,
format: 'dd/MM/yyyy',
rangeSeparator: ' - ',
}
function formatDate(date: Date | Date[]): string {
if (Array.isArray(date)) {
return date.map(d => formatSingleDate(d)).join(' - ')
}
return formatSingleDate(date)
}
function formatSingleDate(d: Date): string {
const day = String(d.getDate()).padStart(2, '0')
const month = String(d.getMonth() + 1).padStart(2, '0')
const year = d.getFullYear()
return `${day}/${month}/${year}`
}
function onUpdate(value: Date | Date[] | null) {
if (value === null) {
firstClick.value = null
isRange.value = false
emit('update:modelValue', null)
return
}
if (Array.isArray(value) && value.length === 2) {
emit('update:modelValue', [value[0], value[1]])
return
}
if (value instanceof Date) {
if (firstClick.value === null) {
// First click — select single day, store for potential range
firstClick.value = value
emit('update:modelValue', value)
// Enable range mode for next click
nextTick(() => {
isRange.value = true
})
}
}
}
function onClear() {
internalValue.value = null
firstClick.value = null
isRange.value = false
emit('update:modelValue', null)
}
function selectToday() {
const today = new Date()
today.setHours(0, 0, 0, 0)
isRange.value = false
firstClick.value = null
internalValue.value = today
emit('update:modelValue', today)
}
function selectThisWeek() {
const now = new Date()
const day = now.getDay()
const monday = new Date(now)
monday.setDate(now.getDate() - day + (day === 0 ? -6 : 1))
monday.setHours(0, 0, 0, 0)
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
sunday.setHours(23, 59, 59, 999)
isRange.value = true
firstClick.value = null
internalValue.value = [monday, sunday]
emit('update:modelValue', [monday, sunday])
}
// Sync external modelValue to internal state
watch(() => props.modelValue, (val) => {
if (val === null || val === undefined) {
internalValue.value = null
firstClick.value = null
isRange.value = false
} else if (Array.isArray(val)) {
isRange.value = true
internalValue.value = [...val]
} else {
isRange.value = false
internalValue.value = val
}
}, { immediate: true })
</script>
<style>
.date-filter .dp__theme_light {
--dp-primary-color: #222783;
--dp-primary-text-color: #fff;
--dp-border-color: #d4d4d8;
--dp-menu-border-color: #d4d4d8;
--dp-border-color-hover: #222783;
--dp-hover-color: #f3f4f8;
--dp-font-size: 0.875rem;
}
.date-filter .dp__input_wrap {
width: auto;
}
.date-filter .dp__main {
font-family: inherit;
}
</style>
```
- [ ] **Step 2: Verify the component renders**
Run `make dev-nuxt` and navigate to the time-tracking page (integration comes in Task 4). Check that no build errors occur.
- [ ] **Step 3: Commit**
```bash
git add frontend/components/ui/DateFilter.vue
git commit -m "feat(frontend) : create DateFilter reusable component"
```
---
## Chunk 2: Integration
### Task 4: Integrate DateFilter into time-tracking page
**Files:**
- Modify: `frontend/pages/time-tracking.vue:15-73` (template filter bar)
- Modify: `frontend/pages/time-tracking.vue:138` (add ref)
- Modify: `frontend/pages/time-tracking.vue:184-193` (filteredEntries computed)
- [ ] **Step 1: Add the date filter ref**
In `frontend/pages/time-tracking.vue`, after line 138 (`selectedProjectId`), add:
```typescript
const selectedDateFilter = ref<Date | [Date, Date] | null>(null)
```
- [ ] **Step 2: Add DateFilter to the template filter bar**
In the filter bar `<div>` (line 15), after the tag MalioSelect block (after line 72), add:
```vue
<DateFilter
v-model="selectedDateFilter"
/>
```
- [ ] **Step 3: Add date filtering to filteredEntries computed**
In `frontend/pages/time-tracking.vue`, update the `filteredEntries` computed (around line 184) to include date filtering:
```typescript
const filteredEntries = computed(() => {
let result = entries.value
if (selectedProjectId.value) {
result = result.filter((e) => e.project?.id === selectedProjectId.value)
}
if (selectedTagId.value) {
result = result.filter((e) => e.tags.some((t) => t.id === selectedTagId.value))
}
if (selectedDateFilter.value) {
if (Array.isArray(selectedDateFilter.value)) {
const [start, end] = selectedDateFilter.value
const startDay = new Date(start)
startDay.setHours(0, 0, 0, 0)
const endDay = new Date(end)
endDay.setHours(23, 59, 59, 999)
result = result.filter((e) => {
const entryDate = new Date(e.startedAt)
return entryDate >= startDay && entryDate <= endDay
})
} else {
const day = new Date(selectedDateFilter.value)
day.setHours(0, 0, 0, 0)
const nextDay = new Date(day)
nextDay.setDate(nextDay.getDate() + 1)
result = result.filter((e) => {
const entryDate = new Date(e.startedAt)
return entryDate >= day && entryDate < nextDay
})
}
}
return result
})
```
- [ ] **Step 4: Verify manually**
Run `make dev-nuxt`, navigate to time-tracking page:
1. Verify DateFilter appears in the filter bar
2. Click a single day — entries filter to that day
3. Click a second day — entries filter to the range
4. Click "Aujourd'hui" — filters to today
5. Click "Cette semaine" — filters to current week
6. Clear the filter — all entries show again
- [ ] **Step 5: Commit**
```bash
git add frontend/pages/time-tracking.vue
git commit -m "feat(frontend) : integrate date filter into time-tracking page"
```

View File

@@ -0,0 +1,86 @@
# Date Filter Component - Design Spec
## Summary
Add a reusable date filter component to the time-tracking page using `@vuepic/vue-datepicker`. Allows filtering by single day or date range via text input and mini calendar dropdown.
## Behavior
- **Single click** on a day = select that day
- **Second click** on another day = select range between the two dates
- **Text input**: type a date (`15/03/2026`) or a range (`15/03/2026 - 20/03/2026`)
- **Calendar dropdown**: opens on input click/focus
- **Quick shortcuts**: "Aujourd'hui" and "Cette semaine" buttons in calendar
- **No time picker**: filter by day granularity only
- **Format**: `dd/MM/yyyy` (French locale)
## Component: `DateFilter.vue`
Location: `frontend/components/ui/DateFilter.vue`
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `Date \| [Date, Date] \| null` | `null` | Selected date or range |
| `placeholder` | `string` | `t('common.dateFilter')` | Input placeholder |
### Emits
| Event | Payload | Description |
|-------|---------|-------------|
| `update:modelValue` | `Date \| [Date, Date] \| null` | Date selection changed |
### Implementation
- Wraps `VueDatePicker` with project-consistent styling
- Uses `#dp-input` slot for custom input matching MalioSelect style
- Configures `range` mode with `multi-calendars: false`
- Sets `text-input` with `format: 'dd/MM/yyyy'`, `rangeSeparator: ' - '`
- Disables time picker (`enable-time-picker: false`)
- Applies project primary color (`#222783`) via CSS overrides
- Responsive width: `!w-44 sm:!w-52`
## Integration: Time Tracking Page
### Filter bar addition
Add `DateFilter` to the existing filter bar in `frontend/pages/time-tracking.vue`, alongside user/project/tag filters.
### Filtering logic
- Client-side filtering (same pattern as project and tag filters)
- When a single date is selected: show only entries matching that day
- When a range is selected: show entries within the range (inclusive)
- When null: show all entries (no date filter)
## Files Impacted
| File | Action | Description |
|------|--------|-------------|
| `frontend/components/ui/DateFilter.vue` | Create | Reusable date filter wrapper |
| `frontend/nuxt.config.ts` | Modify | Add `@vuepic/vue-datepicker` to `build.transpile` |
| `frontend/pages/time-tracking.vue` | Modify | Integrate DateFilter in filter bar + client-side filtering |
| `frontend/i18n/locales/fr.json` | Modify | Add French translations |
| `frontend/i18n/locales/en.json` | Modify | Add English translations |
| `package.json` | Modify | Add `@vuepic/vue-datepicker` dependency |
## i18n Keys
```json
{
"common": {
"dateFilter": "Date",
"today": "Aujourd'hui",
"thisWeek": "Cette semaine"
}
}
```
## Style
- Input height and borders match MalioSelect components
- Text size: `text-sm`
- Selected date highlight: project primary color `#222783`
- Calendar dropdown: subtle shadow, rounded corners matching project style
- Override default vue-datepicker CSS variables to match project theme