feat(workflow) : my-tasks - kanban groupé par catégorie avec badge statut, suppression drag-to-status

This commit is contained in:
2026-05-19 20:09:17 +02:00
parent 5d42009348
commit e6d765f7bb
2 changed files with 27 additions and 69 deletions

View File

@@ -40,6 +40,13 @@
</div> </div>
<div class="mt-2 flex items-center gap-1.5"> <div class="mt-2 flex items-center gap-1.5">
<span
v-if="showStatusBadge && task.status"
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
:style="{ backgroundColor: task.status.color }"
>
{{ task.status.label }}
</span>
<span <span
v-if="task.priority" v-if="task.priority"
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white" class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
@@ -106,8 +113,10 @@ import type { Task } from '~/services/dto/task'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
task: Task task: Task
showProjectColor?: boolean showProjectColor?: boolean
showStatusBadge?: boolean
}>(), { }>(), {
showProjectColor: false, showProjectColor: false,
showStatusBadge: false,
}) })
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -7,6 +7,8 @@ import type { TaskTag } from '~/services/dto/task-tag'
import type { TaskGroup } from '~/services/dto/task-group' import type { TaskGroup } from '~/services/dto/task-group'
import type { UserData } from '~/services/dto/user-data' import type { UserData } from '~/services/dto/user-data'
import type { Project } from '~/services/dto/project' import type { Project } from '~/services/dto/project'
import type { StatusCategory } from '~/services/dto/workflow'
import { STATUS_CATEGORY_LABEL } from '~/services/dto/workflow'
import { useTaskService } from '~/services/tasks' import { useTaskService } from '~/services/tasks'
import { useTaskStatusService } from '~/services/task-statuses' import { useTaskStatusService } from '~/services/task-statuses'
import { useTaskEffortService } from '~/services/task-efforts' import { useTaskEffortService } from '~/services/task-efforts'
@@ -112,13 +114,11 @@ const sortOptions = computed(() => [
{ label: t('myTasks.sortScheduledStart'), value: SORT_SCHEDULED }, { label: t('myTasks.sortScheduledStart'), value: SORT_SCHEDULED },
]) ])
// Kanban helpers // Kanban helpers (grouped by canonical status category)
const sortedStatuses = computed(() => const CATEGORIES: StatusCategory[] = ['todo', 'in_progress', 'blocked', 'review', 'done']
[...statuses.value].sort((a, b) => a.position - b.position)
)
function tasksByStatus(statusId: number): Task[] { function tasksByCategory(category: StatusCategory): Task[] {
return tasks.value.filter(t => t.status?.id === statusId) return tasks.value.filter(t => t.status?.category === category)
} }
const backlogTasks = computed(() => const backlogTasks = computed(() =>
@@ -205,44 +205,6 @@ watch(selectedProjectId, () => {
selectedGroupId.value = null selectedGroupId.value = null
}, { flush: 'sync' }) }, { flush: 'sync' })
// Drag & drop
const dragOverStatusId = ref<number | null>(null)
const dragCounter = ref(0)
function onDragEnter(id: number) {
dragCounter.value++
dragOverStatusId.value = id
}
function onDragLeave() {
dragCounter.value--
if (dragCounter.value === 0) {
dragOverStatusId.value = null
}
}
function onDrop(event: DragEvent) {
dragCounter.value = 0
dragOverStatusId.value = null
return Number(event.dataTransfer!.getData('text/plain'))
}
async function onDropStatus(event: DragEvent, status: TaskStatus) {
const taskId = onDrop(event)
const task = tasks.value.find(t => t.id === taskId)
if (!task || task.status?.id === status.id) return
task.status = status
await taskService.update(taskId, { status: `/api/task_statuses/${status.id}` })
}
async function onDropBacklog(event: DragEvent) {
const taskId = onDrop(event)
const task = tasks.value.find(t => t.id === taskId)
if (!task || !task.status) return
task.status = null
await taskService.update(taskId, { status: null })
}
// Modal // Modal
function openTaskCreate() { function openTaskCreate() {
selectedTask.value = null selectedTask.value = null
@@ -428,36 +390,29 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- Kanban View --> <!-- Kanban View grouped by canonical category -->
<div v-if="viewMode === 'kanban'"> <div v-if="viewMode === 'kanban'">
<div class="mt-6 flex h-[calc(100vh-260px)] gap-3 overflow-x-auto pb-4"> <div class="mt-6 flex h-[calc(100vh-260px)] gap-3 overflow-x-auto pb-4">
<div <div
v-for="status in sortedStatuses" v-for="cat in CATEGORIES"
:key="status.id" :key="cat"
class="flex min-w-36 flex-1 flex-col rounded-lg transition-colors" class="flex min-w-40 flex-1 flex-col rounded-lg bg-neutral-50"
:class="dragOverStatusId === status.id ? 'bg-neutral-200' : 'bg-neutral-50'"
@dragover.prevent
@dragenter.prevent="onDragEnter(status.id)"
@dragleave="onDragLeave"
@drop.prevent="onDropStatus($event, status)"
> >
<div <div class="shrink-0 rounded-t-lg bg-neutral-200 px-4 py-3 text-sm font-bold text-neutral-800">
class="shrink-0 rounded-t-lg px-4 py-3 text-sm font-bold text-white" {{ STATUS_CATEGORY_LABEL[cat] }} ({{ tasksByCategory(cat).length }})
:style="{ backgroundColor: status.color }"
>
{{ status.label }} ({{ tasksByStatus(status.id).length }})
</div> </div>
<div class="min-h-0 flex-1 overflow-y-auto p-3"> <div class="min-h-0 flex-1 overflow-y-auto p-3">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<TaskCard <TaskCard
v-for="task in tasksByStatus(status.id)" v-for="task in tasksByCategory(cat)"
:key="task.id" :key="task.id"
:task="task" :task="task"
show-project-color show-project-color
show-status-badge
@click="openTaskEdit(task)" @click="openTaskEdit(task)"
/> />
<p <p
v-if="tasksByStatus(status.id).length === 0" v-if="tasksByCategory(cat).length === 0"
class="py-4 text-center text-xs text-neutral-400" class="py-4 text-center text-xs text-neutral-400"
> >
{{ $t('myTasks.noTasks') }} {{ $t('myTasks.noTasks') }}
@@ -467,15 +422,8 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- Backlog below kanban --> <!-- Backlog below kanban (no drag/drop status change goes through TaskModal) -->
<div <div class="mt-8 rounded-lg bg-neutral-50 p-4">
class="mt-8 rounded-lg p-4 transition-colors"
:class="dragOverStatusId === 0 ? 'bg-neutral-200' : 'bg-neutral-50'"
@dragover.prevent
@dragenter.prevent="onDragEnter(0)"
@dragleave="onDragLeave"
@drop.prevent="onDropBacklog($event)"
>
<h2 class="text-lg font-bold text-neutral-900">{{ $t('myTasks.backlog') }} ({{ backlogTasks.length }})</h2> <h2 class="text-lg font-bold text-neutral-900">{{ $t('myTasks.backlog') }} ({{ backlogTasks.length }})</h2>
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<TaskCard <TaskCard
@@ -483,6 +431,7 @@ onMounted(async () => {
:key="task.id" :key="task.id"
:task="task" :task="task"
show-project-color show-project-color
show-status-badge
@click="openTaskEdit(task)" @click="openTaskEdit(task)"
/> />
</div> </div>