diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index bce1fa7..350d491 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -85,5 +85,21 @@ "statusFinal": "Statut final", "groupArchiveDisabled": "Tous les tickets doivent être en statut final pour archiver le groupe.", "groupNonFinalTasks": "Il reste {count} ticket(s) sans statut final dans ce groupe." + }, + "myTasks": { + "title": "Mes tâches", + "viewKanban": "Vue Kanban", + "viewList": "Vue Liste", + "allProjects": "Tous les projets", + "allGroups": "Tous les groupes", + "allTypes": "Tous les types", + "allPriorities": "Toutes les priorités", + "allEfforts": "Tous les efforts", + "allAssignees": "Tous", + "noTasks": "Aucune tâche", + "backlog": "Backlog" + }, + "sidebar": { + "myTasks": "Mes tâches" } } diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 044e3a2..04ad910 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -27,6 +27,12 @@ :collapsed="ui.sidebarCollapsed" :class="ui.sidebarCollapsed ? 'mt-4' : 'border-t border-secondary-500 pt-6'" /> + +import type { Task } from '~/services/dto/task' +import type { TaskStatus } from '~/services/dto/task-status' +import type { TaskEffort } from '~/services/dto/task-effort' +import type { TaskPriority } from '~/services/dto/task-priority' +import type { TaskTag } from '~/services/dto/task-tag' +import type { TaskGroup } from '~/services/dto/task-group' +import type { UserData } from '~/services/dto/user-data' +import type { Project } from '~/services/dto/project' +import { useTaskService } from '~/services/tasks' +import { useTaskStatusService } from '~/services/task-statuses' +import { useTaskEffortService } from '~/services/task-efforts' +import { useTaskPriorityService } from '~/services/task-priorities' +import { useTaskTagService } from '~/services/task-tags' +import { useTaskGroupService } from '~/services/task-groups' +import { useUserService } from '~/services/users' +import { useProjectService } from '~/services/projects' + +const { t } = useI18n() +const auth = useAuthStore() + +useHead({ title: t('myTasks.title') }) + +const taskService = useTaskService() +const statusService = useTaskStatusService() +const effortService = useTaskEffortService() +const priorityService = useTaskPriorityService() +const tagService = useTaskTagService() +const groupService = useTaskGroupService() +const userService = useUserService() +const projectService = useProjectService() + +const tasks = ref([]) +const statuses = ref([]) +const efforts = ref([]) +const priorities = ref([]) +const tags = ref([]) +const groups = ref([]) +const users = ref([]) +const projects = ref([]) +const isLoading = ref(true) + +// Filters +const selectedProjectId = ref(null) +const selectedGroupId = ref(null) +const selectedTagId = ref(null) +const selectedPriorityId = ref(null) +const selectedEffortId = ref(null) +const selectedAssigneeId = ref(auth.user?.id ?? null) + +// View toggle +const viewMode = ref<'kanban' | 'list'>('kanban') + +// Modal +const taskModalOpen = ref(false) +const selectedTask = ref(null) + +// Timer +const timerStore = useTimerStore() + +function isTimerOnTask(task: Task): boolean { + const entry = timerStore.activeEntry + if (!entry?.task) return false + const entryTaskId = typeof entry.task === 'string' + ? entry.task + : (entry.task['@id'] ?? entry.task.id) + const taskId = task['@id'] ?? task.id + return entryTaskId === taskId || entryTaskId === `/api/tasks/${task.id}` +} + +// Filter options +const projectOptions = computed(() => + projects.value.map(p => ({ label: p.name, value: p.id })) +) + +const groupOptions = computed(() => { + let g = groups.value.filter(g => !g.archived) + if (selectedProjectId.value) { + g = g.filter(g => g.project?.id === selectedProjectId.value) + } + return g.map(g => ({ label: g.title, value: g.id })) +}) + +const tagOptions = computed(() => + tags.value.map(t => ({ label: t.label, value: t.id })) +) + +const priorityOptions = computed(() => + priorities.value.map(p => ({ label: p.label, value: p.id })) +) + +const effortOptions = computed(() => + efforts.value.map(e => ({ label: e.label, value: e.id })) +) + +const assigneeOptions = computed(() => + users.value.map(u => ({ label: u.username, value: u.id })) +) + +// Kanban helpers +const sortedStatuses = computed(() => + [...statuses.value].sort((a, b) => a.position - b.position) +) + +function tasksByStatus(statusId: number): Task[] { + return tasks.value.filter(t => t.status?.id === statusId) +} + +const backlogTasks = computed(() => + tasks.value.filter(t => !t.status) +) + +// Data loading +async function loadReferenceData() { + const [s, e, pr, tg, g, u, p] = await Promise.all([ + statusService.getAll(), + effortService.getAll(), + priorityService.getAll(), + tagService.getAll(), + groupService.getAll(), + userService.getAll(), + projectService.getAll(), + ]) + statuses.value = s + efforts.value = e + priorities.value = pr + tags.value = tg + groups.value = g + users.value = u + projects.value = p +} + +async function loadTasks() { + const params: Record = { + archived: false, + } + if (selectedAssigneeId.value) { + params.assignee = `/api/users/${selectedAssigneeId.value}` + } + if (selectedProjectId.value) { + params.project = `/api/projects/${selectedProjectId.value}` + } + if (selectedGroupId.value) { + params.group = `/api/task_groups/${selectedGroupId.value}` + } + if (selectedPriorityId.value) { + params.priority = `/api/task_priorities/${selectedPriorityId.value}` + } + if (selectedEffortId.value) { + params.effort = `/api/task_efforts/${selectedEffortId.value}` + } + if (selectedTagId.value) { + params['tags[]'] = `/api/task_tags/${selectedTagId.value}` + } + tasks.value = await taskService.getFiltered(params) +} + +async function loadAll() { + isLoading.value = true + try { + await Promise.all([loadReferenceData(), loadTasks()]) + } finally { + isLoading.value = false + } +} + +// Watch filters to reload tasks +watch( + [selectedProjectId, selectedGroupId, selectedTagId, selectedPriorityId, selectedEffortId, selectedAssigneeId], + () => { loadTasks() }, +) + +// Reset group when project changes +watch(selectedProjectId, () => { + selectedGroupId.value = null +}, { flush: 'sync' }) + +// Modal +function openTaskEdit(task: Task) { + selectedTask.value = task + taskModalOpen.value = true +} + +async function onSaved() { + await loadTasks() +} + +onMounted(() => { + loadAll() +}) + + + diff --git a/frontend/services/tasks.ts b/frontend/services/tasks.ts index 551e2bb..647ca01 100644 --- a/frontend/services/tasks.ts +++ b/frontend/services/tasks.ts @@ -26,6 +26,11 @@ export function useTaskService() { return extractHydraMembers(data) } + async function getFiltered(params: Record): Promise { + const data = await api.get>('/tasks', params as Record) + return extractHydraMembers(data) + } + async function create(payload: TaskWrite): Promise { return api.post('/tasks', payload as Record, { toastSuccessKey: 'tasks.created', @@ -44,5 +49,5 @@ export function useTaskService() { }) } - return { getAll, getByProject, getByProjectArchived, create, update, remove } + return { getAll, getByProject, getByProjectArchived, getFiltered, create, update, remove } } diff --git a/src/Entity/Task.php b/src/Entity/Task.php index 64b419d..5956b3c 100644 --- a/src/Entity/Task.php +++ b/src/Entity/Task.php @@ -22,7 +22,7 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ApiResource( operations: [ - new GetCollection(), + new GetCollection(paginationEnabled: false), new Get(), new Post(security: "is_granted('ROLE_ADMIN')", processor: TaskNumberProcessor::class), new Patch(security: "is_granted('ROLE_ADMIN')"), @@ -32,7 +32,7 @@ use Symfony\Component\Serializer\Attribute\Groups; denormalizationContext: ['groups' => ['task:write']], order: ['id' => 'DESC'], )] -#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact', 'group' => 'exact'])] +#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact', 'group' => 'exact', 'assignee' => 'exact', 'priority' => 'exact', 'effort' => 'exact', 'tags' => 'exact', 'status' => 'exact'])] #[ApiFilter(BooleanFilter::class, properties: ['archived'])] #[ORM\Entity(repositoryClass: TaskRepository::class)] class Task