Files
mws.frontend.vue/src/views/tasks/TasksBoardView.vue
T

215 lines
6.6 KiB
Vue
Raw Normal View History

2026-08-11 23:17:04 +07:00
<template>
2026-08-26 00:11:45 +07:00
<div class="flex h-full flex-col">
<div class="mb-3 flex items-center justify-end">
2026-08-11 23:17:04 +07:00
<Button v-if="auth.can('tasks', 'create')" label="New Task" icon="pi pi-plus" @click="openCreate" />
</div>
2026-08-26 00:11:45 +07:00
<div class="board-container flex min-h-0 flex-1 overflow-x-auto rounded-2xl border" style="border-color: var(--hairline); background: var(--panel)">
2026-08-11 23:17:04 +07:00
<div
2026-08-26 00:11:45 +07:00
v-for="(col, i) in columns"
2026-08-11 23:17:04 +07:00
:key="col.status"
2026-08-26 00:11:45 +07:00
class="board-col flex min-w-[260px] flex-1 flex-col p-3"
:class="{ 'drag-over': dragOverStatus === col.status }"
:style="i > 0 ? 'border-left: 1px solid var(--hairline)' : ''"
@dragover.prevent="onDragOver(col.status, $event)"
@dragleave="onDragLeave(col.status, $event)"
2026-08-11 23:17:04 +07:00
@drop.prevent="onDrop(col.status)"
>
2026-08-26 00:11:45 +07:00
<div class="mb-3 flex items-center justify-between">
<Tag :value="col.label" :severity="col.severity" />
2026-08-11 23:17:04 +07:00
<Tag :value="tasksIn(col.status).length" severity="secondary" />
</div>
2026-08-26 00:11:45 +07:00
<div class="flex flex-1 flex-col gap-2">
<div
v-for="task in tasksIn(col.status)"
:key="task.id"
class="task-card cursor-pointer rounded-xl border p-3 shadow-sm"
:style="{ background: 'var(--canvas)', borderColor: 'var(--hairline)' }"
:class="{ 'opacity-40': draggingId === task.id }"
draggable="true"
@dragstart="onDragStart(task)"
@dragend="onDragEnd"
@click.stop="toggleDesc(task.id)"
>
<div class="font-medium">{{ task.title }}</div>
<div v-if="expandedDescs.has(task.id) && task.description" class="muted-note mt-1 line-clamp-2 text-[0.85rem]">{{ task.description }}</div>
<div class="mt-2 text-[0.8rem]" v-if="task.assigneeName">
<span class="flex items-center gap-1 muted-note">
<i class="pi pi-user text-[0.7rem]"></i> {{ task.assigneeName }}
</span>
</div>
<div class="text-[0.8rem]" v-if="task.dueDate">
<span class="flex items-center gap-1 muted-note">
<i class="pi pi-calendar text-[0.7rem]"></i> {{ formatDate(task.dueDate) }}
</span>
</div>
</div>
<div
v-if="tasksIn(col.status).length === 0 && dragOverStatus === col.status"
class="flex flex-1 items-center justify-center rounded-xl text-sm"
style="border: 2px dashed var(--primary); color: var(--primary)"
>
Drop here
2026-08-11 23:17:04 +07:00
</div>
</div>
</div>
</div>
<TaskDetailDialog
v-model:visible="dialogVisible"
:task="editingTask"
:project-id="projectId"
:members="members"
:can-edit="auth.can('tasks', 'edit')"
:can-delete="auth.can('tasks', 'delete')"
@saved="onSaved"
@deleted="onDeleted"
/>
</div>
</template>
<script setup lang="ts">
import TaskDetailDialog from './TaskDetailDialog.vue'
import { getTasks, updateTask } from '../../services/modules'
import { getMembers } from '../../services/backend'
import { errorMessage } from '../../services/api'
import { useAuthStore } from '../../stores/auth'
2026-08-26 00:11:45 +07:00
import type { Task, TaskStatus } from '../../types'
2026-08-11 23:17:04 +07:00
const route = useRoute()
const toast = useToast()
const auth = useAuthStore()
const projectId = String(route.params.id)
const tasks = ref<Task[]>([])
const members = ref<{ userId: string; displayName: string }[]>([])
const draggingId = ref<string | null>(null)
const dragOverStatus = ref<TaskStatus | null>(null)
const dialogVisible = ref(false)
const editingTask = ref<Task | null>(null)
2026-08-26 00:11:45 +07:00
const expandedDescs = ref(new Set<string>())
function toggleDesc(id: string) {
const s = new Set(expandedDescs.value)
if (s.has(id)) s.delete(id)
else s.add(id)
expandedDescs.value = s
}
2026-08-11 23:17:04 +07:00
const columns = [
2026-08-26 00:11:45 +07:00
{ status: 'Todo' as TaskStatus, label: 'To Do', severity: 'secondary' as const },
{ status: 'InProgress' as TaskStatus, label: 'In Progress', severity: 'info' as const },
{ status: 'Done' as TaskStatus, label: 'Done', severity: 'success' as const },
{ status: 'Cancelled' as TaskStatus, label: 'Cancelled', severity: 'danger' as const },
2026-08-11 23:17:04 +07:00
]
function tasksIn(status: TaskStatus) {
return tasks.value.filter((t) => t.status === status)
}
async function load() {
try {
2026-08-24 08:07:23 +07:00
const res = await getTasks(projectId, undefined, 1, 100)
tasks.value = res.items
2026-08-11 23:17:04 +07:00
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
}
}
async function loadMembers() {
try {
2026-08-24 08:07:23 +07:00
const res = await getMembers(projectId, 1, 100)
members.value = res.items.map((m) => ({
2026-08-11 23:17:04 +07:00
userId: m.userId,
displayName: m.displayName,
}))
} catch {
members.value = []
}
}
function onDragStart(task: Task) {
draggingId.value = task.id
}
function onDragEnd() {
draggingId.value = null
dragOverStatus.value = null
}
2026-08-26 00:11:45 +07:00
function onDragOver(status: TaskStatus, e: DragEvent) {
e.dataTransfer!.dropEffect = 'move'
dragOverStatus.value = status
}
function onDragLeave(_status: TaskStatus, e: DragEvent) {
const related = e.relatedTarget as HTMLElement | null
if (related && (e.currentTarget as HTMLElement).contains(related)) return
if (dragOverStatus.value === _status) dragOverStatus.value = null
}
2026-08-11 23:17:04 +07:00
async function onDrop(status: TaskStatus) {
const id = draggingId.value
dragOverStatus.value = null
draggingId.value = null
2026-08-26 00:11:45 +07:00
if (!id || !auth.can('tasks', 'edit')) return
2026-08-11 23:17:04 +07:00
const task = tasks.value.find((t) => t.id === id)
if (!task || task.status === status) return
try {
await updateTask(id, {
title: task.title,
description: task.description,
status,
priority: task.priority,
assigneeId: task.assigneeId,
dueDate: task.dueDate,
})
toast.add({ severity: 'success', summary: `Moved to ${statusLabel(status)}`, life: 2000 })
await load()
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
}
}
function openCreate() {
editingTask.value = null
dialogVisible.value = true
}
function onSaved() {
dialogVisible.value = false
void load()
}
function onDeleted() {
dialogVisible.value = false
void load()
}
function statusLabel(s: TaskStatus) {
return s.replace(/([A-Z])/g, ' $1').trim()
}
function formatDate(v: string) {
return new Date(v).toLocaleDateString()
}
onMounted(async () => {
await loadMembers()
await load()
})
</script>
2026-08-24 08:07:23 +07:00
<style scoped>
.task-card:hover {
border-color: var(--primary) !important;
}
2026-08-26 00:11:45 +07:00
.board-col.drag-over {
background: color-mix(in srgb, var(--primary) 6%, transparent);
box-shadow: inset 0 0 0 2px var(--primary);
border-radius: 0.75rem;
}
2026-08-24 08:07:23 +07:00
</style>