Compare commits

...

5 Commits

Author SHA1 Message Date
gitea-actions
b25be8fd6a chore: bump version to v0.3.32
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 43s
2026-05-06 13:58:46 +00:00
Matthieu
3e6b0e877a fix(time-tracking) : filtres projet/tag server-side et vue liste au mois
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
- Pousse les filtres projet et tag a l'API (au lieu d'un filtrage client-side
  partiel sur la page courante) pour eviter les resultats incomplets en cas
  de pagination
- Ajoute les watchers selectedProjectId/selectedTagId qui declenchent un reload
- Mode liste : navigation et plage de chargement passent a 1 mois (au lieu
  d'une fenetre de 7 jours qui rendait le mode liste inutilisable)
- Renomme l'option vide du filtre User en "Tous" (etait "User", ambigu)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:51:18 +02:00
Matthieu
9f3fc05a52 fix(project) : masquer le filtre status en mode kanban
En mode kanban, selectionner un statut dans le filtre Status vidait toutes
les autres colonnes ET le backlog (tasks?.status?.id !== selectedId) : le
filtre etait redondant avec les colonnes et cassait la vue.

Conditionne l'affichage du filtre Status a viewMode === 'list' et reset le
filtre lors du retour en kanban.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:51:09 +02:00
Matthieu
4c3721b6ac fix(dashboard) : appliquer le filtre user aux KPIs et charts de taches
Avant, seul le KPI "Heures sur la periode" reagissait au filtre Utilisateur ;
"Taches totales", "Mes taches actives" et tous les graphiques tache restaient
inchanges. Le computed tasks ne filtrait que par projet, et myTasks etait
hardcode sur auth.user.id (cf ticket LST40).

Ajoute un effectiveUserId (selectedUser ?? auth.user) et applique le filtre
user a tasks pour propager dans tous les charts et KPIs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:51:02 +02:00
Matthieu
06d733f88e docs : ajoute note delegation Codex pour taches mecaniques 2026-05-06 08:49:20 +02:00
6 changed files with 91 additions and 25 deletions

View File

@@ -103,6 +103,10 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash`
- Portal client : pages sous `/portal/`, layout `portal.vue`, middleware redirige `ROLE_CLIENT` (sans `ROLE_ADMIN`) vers `/portal`
- Users admin+client : ne pas bloquer — vérifier `ROLE_CLIENT && !ROLE_ADMIN` pour les restrictions
### Composants UI
La librairie `@malio/layer-ui` fournit les composants de formulaire et d'action. La documentation complète des props, events et exemples d'utilisation se trouve dans `frontend/node_modules/@malio/layer-ui/COMPONENTS.md`. Toujours s'y référer avant d'utiliser un composant Malio.
### MCP Server
- 25 tools MCP exposant projets, tâches, métadonnées, time tracking, et récurrences
@@ -136,3 +140,12 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash`
- API token admin (dev) : `dev-mcp-token-for-testing-only-do-not-use-in-production`
- ZimbraConfiguration : serverUrl `https://mail.ovh.com`, username `lesstime@ovh.fr`, enabled false
- TaskRecurrence (hebdomadaire lun/mer/ven) attachée à la tâche "Réunion de suivi hebdomadaire" (SIRH)
## Delegation Codex
Pour les taches mecaniques (tests, boilerplate, renommages, refacto repetitif), delegue a Codex via le plugin `codex`. Garde Claude pour la reflexion, l'architecture et la verification.
- **Codex** = junior dev rapide et pas cher (executions mecaniques)
- **Claude** = senior dev qui verifie et reflechit (design, review, decisions)
C'est le meilleur ratio qualite/credits.

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.3.31'
app.version: '0.3.32'

View File

@@ -93,11 +93,22 @@ const isWeekPeriod = computed(() =>
selectedPeriod.value === 'thisWeek' || selectedPeriod.value === 'lastWeek'
)
// ── Filtered data (client-side project filter) ──
// ── Filtered data (client-side project + user filter) ──
const effectiveUserId = computed(() => selectedUserId.value ?? auth.user?.id ?? null)
const tasks = computed(() => {
if (!selectedProjectId.value) return allTasks.value
return allTasks.value.filter(t => t.project?.id === selectedProjectId.value)
let result = allTasks.value
if (selectedProjectId.value) {
result = result.filter(t => t.project?.id === selectedProjectId.value)
}
if (selectedUserId.value) {
result = result.filter(t =>
t.assignee?.id === selectedUserId.value
|| t.collaborators?.some(c => c.id === selectedUserId.value),
)
}
return result
})
const timeEntries = computed(() => {
@@ -173,8 +184,8 @@ const totalHoursThisWeek = computed(() =>
const myTasks = computed(() =>
tasks.value.filter(t =>
t.assignee?.id === auth.user?.id
|| t.collaborators?.some(c => c.id === auth.user?.id)
t.assignee?.id === effectiveUserId.value
|| t.collaborators?.some(c => c.id === effectiveUserId.value),
)
)

View File

@@ -61,6 +61,7 @@
text-value="text-sm"
/>
<MalioSelect
v-if="viewMode === 'list'"
v-model="selectedStatusId"
:options="statusFilterOptions"
label="Status"
@@ -258,6 +259,12 @@ const selectedStatusId = ref<number | null>(null)
const selectedPriorityId = ref<number | null>(null)
const selectedEffortId = ref<number | null>(null)
const viewMode = ref<'kanban' | 'list'>('kanban')
watch(viewMode, (mode) => {
if (mode === 'kanban') {
selectedStatusId.value = null
}
})
const selectedTaskIds = reactive(new Set<number>())
const dragOverStatusId = ref<number | null>(null)
const dragCounter = ref(0)

View File

@@ -56,7 +56,7 @@
text-field="text-sm"
text-value="text-sm"
label="User"
empty-option-label="User"
empty-option-label="Tous"
/>
</div>
@@ -217,16 +217,7 @@ function updatePageHeaderHeight() {
pageHeaderHeight.value = pageHeaderEl.value?.offsetHeight ?? 0
}
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))
}
return result
})
const filteredEntries = computed(() => entries.value)
function getMonday(d: Date): Date {
const date = new Date(d)
@@ -239,15 +230,35 @@ function getMonday(d: Date): Date {
function navigatePrev() {
const d = new Date(startDate.value)
d.setDate(d.getDate() - (viewMode.value === 'day' ? 1 : 7))
startDate.value = viewMode.value === 'day' ? d : getMonday(d)
if (viewMode.value === 'day') {
d.setDate(d.getDate() - 1)
startDate.value = d
} else if (viewMode.value === 'list') {
d.setMonth(d.getMonth() - 1)
d.setDate(1)
d.setHours(0, 0, 0, 0)
startDate.value = d
} else {
d.setDate(d.getDate() - 7)
startDate.value = getMonday(d)
}
loadEntries()
}
function navigateNext() {
const d = new Date(startDate.value)
d.setDate(d.getDate() + (viewMode.value === 'day' ? 1 : 7))
startDate.value = viewMode.value === 'day' ? d : getMonday(d)
if (viewMode.value === 'day') {
d.setDate(d.getDate() + 1)
startDate.value = d
} else if (viewMode.value === 'list') {
d.setMonth(d.getMonth() + 1)
d.setDate(1)
d.setHours(0, 0, 0, 0)
startDate.value = d
} else {
d.setDate(d.getDate() + 7)
startDate.value = getMonday(d)
}
loadEntries()
}
@@ -359,12 +370,20 @@ async function onExport(params: {
async function loadEntries() {
const end = new Date(startDate.value)
end.setDate(end.getDate() + (viewMode.value === 'day' ? 1 : 7))
if (viewMode.value === 'day') {
end.setDate(end.getDate() + 1)
} else if (viewMode.value === 'list') {
end.setMonth(end.getMonth() + 1)
} else {
end.setDate(end.getDate() + 7)
}
entries.value = await timeEntryService.getByDateRange({
after: startDate.value.toISOString(),
before: end.toISOString(),
user: selectedUserId.value ?? undefined,
project: selectedProjectId.value ?? undefined,
tag: selectedTagId.value ?? undefined,
})
}
@@ -400,11 +419,20 @@ onMounted(async () => {
watch(viewMode, () => {
selectedDateFilter.value = null
startDate.value = viewMode.value === 'day' ? startDate.value : getMonday(startDate.value)
if (viewMode.value === 'day') {
// keep current date
} else if (viewMode.value === 'list') {
const d = new Date(startDate.value)
d.setDate(1)
d.setHours(0, 0, 0, 0)
startDate.value = d
} else {
startDate.value = getMonday(startDate.value)
}
loadEntries()
})
watch(selectedUserId, () => {
watch([selectedUserId, selectedProjectId, selectedTagId], () => {
loadEntries()
})

View File

@@ -9,7 +9,8 @@ export function useTimeEntryService() {
after: string
before: string
user?: number
types?: number[]
project?: number
tag?: number
}): Promise<TimeEntry[]> {
const query: Record<string, unknown> = {
'startedAt[after]': params.after,
@@ -18,6 +19,12 @@ export function useTimeEntryService() {
if (params.user) {
query.user = `/api/users/${params.user}`
}
if (params.project) {
query.project = `/api/projects/${params.project}`
}
if (params.tag) {
query['tags[]'] = `/api/task_tags/${params.tag}`
}
const data = await api.get<HydraCollection<TimeEntry>>('/time_entries', query)
return extractHydraMembers(data)
}