feat: AI context, Webhooks, Kanban drag-drop, and UI fixes
This commit is contained in:
+78
-29
@@ -29,35 +29,45 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex-1 overflow-y-auto space-y-4 custom-scrollbar">
|
||||
<div
|
||||
v-for="task in getTasksForColumn(col.id)"
|
||||
:key="task.id"
|
||||
class="bg-slate-900 border border-slate-600 rounded-lg p-4 shadow-sm hover:border-blue-500 transition-colors cursor-pointer group"
|
||||
@click="openTask(task)"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="text-xs font-medium px-2 py-0.5 rounded" :class="getPriorityClass(task)">
|
||||
{{ getPriorityLabel(task) }}
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-500 font-mono">{{ task.id.split('-')[1] || task.id }}</div>
|
||||
</div>
|
||||
|
||||
<h4 class="font-medium text-white mb-2 line-clamp-2">{{ getTaskTitle(task) }}</h4>
|
||||
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-xs text-slate-400 flex items-center gap-1">
|
||||
<span>🗺️</span>
|
||||
<span class="truncate max-w-[120px]">{{ task.canvasName }}</span>
|
||||
</div>
|
||||
<div v-if="task.alarm || task.data?.alarm" class="text-xs text-orange-400 flex items-center gap-1" :title="new Date(task.alarm || task.data?.alarm).toLocaleString('ru-RU')">
|
||||
<span>⏰</span>
|
||||
<span>{{ formatShortDate(task.alarm || task.data?.alarm) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 flex-1 overflow-y-auto space-y-4 custom-scrollbar min-h-[200px]">
|
||||
<ClientOnly>
|
||||
<draggable
|
||||
v-model="columnsData[col.id]"
|
||||
group="tasks"
|
||||
item-key="id"
|
||||
class="space-y-4 min-h-[200px]"
|
||||
@change="(evt) => onTaskMove(evt, col.id)"
|
||||
>
|
||||
<template #item="{ element: task }">
|
||||
<div
|
||||
class="bg-slate-900 border border-slate-600 rounded-lg p-4 shadow-sm hover:border-blue-500 transition-colors cursor-pointer group"
|
||||
@click="openTask(task)"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="text-xs font-medium px-2 py-0.5 rounded" :class="getPriorityClass(task)">
|
||||
{{ getPriorityLabel(task) }}
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-500 font-mono">{{ task.id.split('-')[1] || task.id }}</div>
|
||||
</div>
|
||||
|
||||
<h4 class="font-medium text-white mb-2 line-clamp-2">{{ getTaskTitle(task) }}</h4>
|
||||
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-xs text-slate-400 flex items-center gap-1">
|
||||
<span>🗺️</span>
|
||||
<span class="truncate max-w-[120px]">{{ task.canvasName }}</span>
|
||||
</div>
|
||||
<div v-if="task.alarm || task.data?.alarm" class="text-xs text-orange-400 flex items-center gap-1" :title="new Date(task.alarm || task.data?.alarm).toLocaleString('ru-RU')">
|
||||
<span>⏰</span>
|
||||
<span>{{ formatShortDate(task.alarm || task.data?.alarm) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</ClientOnly>
|
||||
|
||||
<div v-if="getTasksForColumn(col.id).length === 0" class="text-center py-6 border-2 border-dashed border-slate-700 rounded-lg">
|
||||
<div v-if="columnsData[col.id].length === 0" class="text-center py-6 border-2 border-dashed border-slate-700 rounded-lg pointer-events-none">
|
||||
<span class="text-slate-500 text-sm">Нет задач</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,11 +78,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, reactive } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
const auth = useAuth()
|
||||
const loading = ref(true)
|
||||
const tasks = ref([])
|
||||
const columnsData = reactive({
|
||||
todo: [],
|
||||
in_progress: [],
|
||||
review: [],
|
||||
blocked: [],
|
||||
completed: []
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ id: 'todo', title: 'К выполнению', icon: '📋', color: 'text-slate-400' },
|
||||
@@ -91,6 +109,14 @@ const fetchTasks = async () => {
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
tasks.value = data.tasks || []
|
||||
|
||||
// Split tasks into columns
|
||||
columns.forEach(col => {
|
||||
columnsData[col.id] = tasks.value.filter(t => {
|
||||
const taskState = t.state || t.data?.state || 'todo'
|
||||
return taskState === col.id
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load tasks', e)
|
||||
@@ -99,6 +125,29 @@ const fetchTasks = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onTaskMove = async (evt, newStatus) => {
|
||||
if (evt.added) {
|
||||
const task = evt.added.element
|
||||
console.log(`Moving task ${task.id} to ${newStatus}`)
|
||||
|
||||
try {
|
||||
await $fetch('/api/tasks/update-status', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
taskId: task.id,
|
||||
canvasId: task.canvasId,
|
||||
newState: newStatus
|
||||
},
|
||||
headers: { Authorization: `Bearer ${auth.token.value}` }
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to update task status', e)
|
||||
// Rollback on error? (In a production app, we should)
|
||||
alert('Не удалось обновить статус задачи на сервере')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getTasksForColumn = (state) => {
|
||||
return tasks.value.filter(t => {
|
||||
const taskState = t.state || t.data?.state || 'todo'
|
||||
|
||||
Reference in New Issue
Block a user