feat(backend) : add project relation to TaskStatus entity with migration and fixtures
Add ManyToOne project field on TaskStatus, SearchFilter for API filtering, migration to add the column, and update fixtures to create statuses per project. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,308 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-neutral-900">{{ project?.name ?? '' }}</h1>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="rounded-md bg-secondary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-600"
|
||||
@click="openGroupCreate"
|
||||
>
|
||||
+ Ajouter un groupe
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
||||
@click="openTaskCreate"
|
||||
>
|
||||
+ Ajouter un ticket
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<MalioSelect
|
||||
v-model="selectedGroupId"
|
||||
:options="groupFilterOptions"
|
||||
label="Groupe"
|
||||
empty-option-label="Tous les groupes"
|
||||
min-width="w-64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Kanban -->
|
||||
<div class="mt-6 flex gap-4 overflow-x-auto pb-4">
|
||||
<div
|
||||
v-for="status in statuses"
|
||||
:key="status.id"
|
||||
class="flex w-72 shrink-0 flex-col rounded-lg transition-colors"
|
||||
: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
|
||||
class="rounded-t-lg px-4 py-3 text-sm font-bold text-white"
|
||||
:style="{ backgroundColor: status.color }"
|
||||
>
|
||||
{{ status.label }} ({{ tasksByStatus(status.id).length }})
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 p-3">
|
||||
<TaskCard
|
||||
v-for="task in tasksByStatus(status.id)"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@click="openTaskEdit(task)"
|
||||
/>
|
||||
<p
|
||||
v-if="tasksByStatus(status.id).length === 0"
|
||||
class="py-4 text-center text-xs text-neutral-400"
|
||||
>
|
||||
Aucun ticket
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backlog -->
|
||||
<div
|
||||
class="mt-8 rounded-lg p-4 transition-colors"
|
||||
:class="dragOverStatusId === 0 ? 'bg-tertiary-600' : 'bg-tertiary-500'"
|
||||
@dragover.prevent
|
||||
@dragenter.prevent="onDragEnter(0)"
|
||||
@dragleave="onDragLeave"
|
||||
@drop.prevent="onDropBacklog($event)"
|
||||
>
|
||||
<h2 class="text-lg font-bold text-neutral-900">Backlog</h2>
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div
|
||||
v-for="task in backlogTasks"
|
||||
:key="task.id"
|
||||
class="flex cursor-pointer items-center justify-between rounded-lg border border-neutral-200 bg-white px-4 py-3 hover:shadow-sm"
|
||||
draggable="true"
|
||||
@dragstart="onBacklogDragStart($event, task)"
|
||||
@dragend="onBacklogDragEnd"
|
||||
@click="openTaskEdit(task)"
|
||||
>
|
||||
<span class="text-sm font-semibold text-neutral-900">{{ task.title }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-for="type in task.types"
|
||||
:key="type.id"
|
||||
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
||||
:style="{ backgroundColor: type.color }"
|
||||
>
|
||||
{{ type.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="task.priority"
|
||||
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
||||
:style="{ backgroundColor: task.priority.color }"
|
||||
>
|
||||
{{ task.priority.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="task.effort"
|
||||
class="text-sm font-bold text-neutral-700"
|
||||
>
|
||||
{{ task.effort.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="task.assignee"
|
||||
class="flex h-5 w-5 items-center justify-center rounded-full bg-primary-500 text-[10px] font-bold text-white"
|
||||
:title="task.assignee.username"
|
||||
>
|
||||
{{ task.assignee.username.substring(0, 2).toUpperCase() }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 text-neutral-400"
|
||||
>
|
||||
<Icon name="mdi:account-outline" size="14" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TaskDrawer
|
||||
v-model="taskDrawerOpen"
|
||||
:task="selectedTask"
|
||||
:project-id="projectId"
|
||||
:statuses="statuses"
|
||||
:efforts="efforts"
|
||||
:priorities="priorities"
|
||||
:types="types"
|
||||
:groups="groups"
|
||||
:users="users"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
|
||||
<TaskGroupDrawer
|
||||
v-model="groupDrawerOpen"
|
||||
:group="selectedGroup"
|
||||
:project-id="projectId"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Project } from '~/services/dto/project'
|
||||
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 { TaskType } from '~/services/dto/task-type'
|
||||
import type { TaskGroup } from '~/services/dto/task-group'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import { useProjectService } from '~/services/projects'
|
||||
import { useTaskService } from '~/services/tasks'
|
||||
import { useTaskStatusService } from '~/services/task-statuses'
|
||||
import { useTaskEffortService } from '~/services/task-efforts'
|
||||
import { useTaskPriorityService } from '~/services/task-priorities'
|
||||
import { useTaskTypeService } from '~/services/task-types'
|
||||
import { useTaskGroupService } from '~/services/task-groups'
|
||||
import { useUserService } from '~/services/users'
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => Number(route.params.id))
|
||||
|
||||
useHead({ title: 'Projet' })
|
||||
|
||||
const projectService = useProjectService()
|
||||
const taskService = useTaskService()
|
||||
const statusService = useTaskStatusService()
|
||||
const effortService = useTaskEffortService()
|
||||
const priorityService = useTaskPriorityService()
|
||||
const typeService = useTaskTypeService()
|
||||
const groupService = useTaskGroupService()
|
||||
const userService = useUserService()
|
||||
|
||||
const project = ref<Project | null>(null)
|
||||
const tasks = ref<Task[]>([])
|
||||
const statuses = ref<TaskStatus[]>([])
|
||||
const efforts = ref<TaskEffort[]>([])
|
||||
const priorities = ref<TaskPriority[]>([])
|
||||
const types = ref<TaskType[]>([])
|
||||
const groups = ref<TaskGroup[]>([])
|
||||
const users = ref<UserData[]>([])
|
||||
const isLoading = ref(true)
|
||||
|
||||
const selectedGroupId = ref<number | null>(null)
|
||||
const dragOverStatusId = ref<number | null>(null)
|
||||
const dragCounter = ref(0)
|
||||
const taskDrawerOpen = ref(false)
|
||||
const selectedTask = ref<Task | null>(null)
|
||||
const groupDrawerOpen = ref(false)
|
||||
const selectedGroup = ref<TaskGroup | null>(null)
|
||||
|
||||
const groupFilterOptions = computed(() =>
|
||||
groups.value.map(g => ({ label: g.title, value: g.id }))
|
||||
)
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
if (!selectedGroupId.value) return tasks.value
|
||||
return tasks.value.filter(t => t.group?.id === selectedGroupId.value)
|
||||
})
|
||||
|
||||
function tasksByStatus(statusId: number): Task[] {
|
||||
return filteredTasks.value.filter(t => t.status?.id === statusId)
|
||||
}
|
||||
|
||||
const backlogTasks = computed(() =>
|
||||
filteredTasks.value.filter(t => !t.status)
|
||||
)
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const [p, t, s, e, pr, ty, g, u] = await Promise.all([
|
||||
projectService.getById(projectId.value),
|
||||
taskService.getByProject(projectId.value),
|
||||
statusService.getAll(),
|
||||
effortService.getAll(),
|
||||
priorityService.getAll(),
|
||||
typeService.getAll(),
|
||||
groupService.getByProject(projectId.value),
|
||||
userService.getAll(),
|
||||
])
|
||||
project.value = p
|
||||
tasks.value = t
|
||||
statuses.value = s
|
||||
efforts.value = e
|
||||
priorities.value = pr
|
||||
types.value = ty
|
||||
groups.value = g
|
||||
users.value = u
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openTaskCreate() {
|
||||
selectedTask.value = null
|
||||
taskDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function openTaskEdit(task: Task) {
|
||||
selectedTask.value = task
|
||||
taskDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function openGroupCreate() {
|
||||
selectedGroup.value = null
|
||||
groupDrawerOpen.value = true
|
||||
}
|
||||
|
||||
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'))
|
||||
}
|
||||
|
||||
function onBacklogDragStart(event: DragEvent, task: Task) {
|
||||
event.dataTransfer!.effectAllowed = 'move'
|
||||
event.dataTransfer!.setData('text/plain', String(task.id))
|
||||
;(event.target as HTMLElement).classList.add('opacity-50')
|
||||
}
|
||||
|
||||
function onBacklogDragEnd(event: DragEvent) {
|
||||
;(event.target as HTMLElement).classList.remove('opacity-50')
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
async function onSaved() {
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user