Files
Avtex/app/pages/tasks.vue
T

313 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen bg-slate-900 text-slate-200">
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold text-white flex items-center gap-3">
<span class="text-blue-500">📋</span> Мои Задачи
</h1>
<div class="flex items-center gap-3">
<button @click="showCreateModal = true" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium shadow-lg shadow-blue-900/20 transition flex items-center gap-2">
<span>➕</span> Новая задача
</button>
</div>
</div>
<!-- Create Task Modal -->
<div v-if="showCreateModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4" @click.self="showCreateModal = false">
<div class="bg-slate-800 border border-slate-700 rounded-2xl p-6 w-full max-w-md shadow-2xl">
<h2 class="text-xl font-bold text-white mb-6">Создать новую задачу</h2>
<form @submit.prevent="createTask" class="space-y-4">
<div>
<label class="text-xs text-slate-500 uppercase font-bold">Название задачи</label>
<input v-model="newTask.title" required class="w-full mt-1 bg-slate-900 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-200 outline-none focus:border-blue-500" placeholder="Что нужно сделать?" />
</div>
<div>
<label class="text-xs text-slate-500 uppercase font-bold">Проект (Канвас)</label>
<select v-model="newTask.canvasId" required class="w-full mt-1 bg-slate-900 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-200 outline-none focus:border-blue-500">
<option value="" disabled>Выберите проект...</option>
<option v-for="c in availableCanvases" :key="c.id" :value="c.id">{{ c.name }}</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-xs text-slate-500 uppercase font-bold">Приоритет</label>
<select v-model="newTask.priority" class="w-full mt-1 bg-slate-900 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-200 outline-none focus:border-blue-500">
<option value="low">Низкий</option>
<option value="medium">Средний</option>
<option value="high">Высокий</option>
</select>
</div>
<div>
<label class="text-xs text-slate-500 uppercase font-bold">Статус</label>
<select v-model="newTask.state" class="w-full mt-1 bg-slate-900 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-200 outline-none focus:border-blue-500">
<option v-for="col in columns" :key="col.id" :value="col.id">{{ col.title }}</option>
</select>
</div>
</div>
<div class="flex gap-3 pt-4">
<button type="submit" :disabled="creating" class="flex-1 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold transition disabled:opacity-50">
{{ creating ? 'Создание...' : 'Создать задачу' }}
</button>
<button type="button" @click="showCreateModal = false" class="px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white rounded-xl font-bold transition">Отмена</button>
</div>
</form>
</div>
</div>
<div v-if="loading" class="flex items-center justify-center h-64">
<div class="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<div v-else-if="tasks.length === 0" class="flex flex-col items-center justify-center h-64 bg-slate-800 rounded-xl border border-slate-700">
<span class="text-4xl mb-4">📭</span>
<h3 class="text-xl font-medium text-white mb-2">У вас пока нет задач</h3>
<p class="text-slate-400">Вам не назначено ни одной задачи в проектах.</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Kanban Columns -->
<div v-for="col in columns" :key="col.id" class="bg-slate-800 rounded-xl border border-slate-700 flex flex-col max-h-[80vh]">
<div class="p-4 border-b border-slate-700 flex items-center justify-between sticky top-0 bg-slate-800 rounded-t-xl z-10">
<h3 class="font-bold text-white flex items-center gap-2">
<span :class="col.color">{{ col.icon }}</span> {{ col.title }}
</h3>
<span class="bg-slate-700 text-slate-300 text-xs px-2 py-1 rounded-full font-medium">
{{ getTasksForColumn(col.id).length }}
</span>
</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="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>
</div>
</div>
</div>
</div>
</template>
<script setup>
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 showCreateModal = ref(false)
const creating = ref(false)
const availableCanvases = ref([])
const newTask = reactive({
title: '',
canvasId: '',
priority: 'medium',
state: 'todo'
})
const columns = [
{ id: 'todo', title: 'К выполнению', icon: '📋', color: 'text-slate-400' },
{ id: 'in_progress', title: 'В работе', icon: '⏳', color: 'text-blue-400' },
{ id: 'review', title: 'На проверке', icon: '👀', color: 'text-purple-400' },
{ id: 'blocked', title: 'Заблокировано', icon: '⛔', color: 'text-red-400' },
{ id: 'completed', title: 'Завершено', icon: '✅', color: 'text-emerald-400' }
]
// Fetch all tasks for current user
const fetchTasks = async () => {
try {
const res = await fetch('/api/tasks', {
headers: { Authorization: `Bearer ${auth.token.value}` }
})
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)
} finally {
loading.value = false
}
}
const fetchCanvases = async () => {
try {
const res = await $fetch('/api/canvas')
availableCanvases.value = res.canvases || []
if (availableCanvases.value.length > 0) {
newTask.canvasId = availableCanvases.value[0].id
}
} catch (e) {
console.error('Failed to load canvases', e)
}
}
const createTask = async () => {
creating.value = true
try {
await $fetch('/api/tasks/create', {
method: 'POST',
body: newTask,
headers: { Authorization: `Bearer ${auth.token.value}` }
})
showCreateModal.value = false
newTask.title = ''
await fetchTasks() // Refresh list
} catch (e) {
alert('Ошибка при создании задачи: ' + (e.data?.message || e.message))
} finally {
creating.value = false
}
}
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'
return taskState === state
})
}
const getTaskTitle = (task) => {
if (task.label) return task.label
const text = task.text || task.data?.text
if (text) {
const lines = text.split('\n')
const firstLine = lines.find(l => l.trim().startsWith('# ')) || lines.find(l => l.trim())
return firstLine ? firstLine.replace('# ', '').trim().slice(0, 50) : 'Без названия'
}
return 'Задача'
}
const getPriorityClass = (task) => {
const priority = task.priority || task.data?.priority
if (priority === 'high') return 'bg-red-900/50 text-red-300'
if (priority === 'low') return 'bg-slate-700 text-slate-300'
return 'bg-blue-900/50 text-blue-300' // medium
}
const getPriorityLabel = (task) => {
const priority = task.priority || task.data?.priority
if (priority === 'high') return 'Высокий'
if (priority === 'low') return 'Низкий'
return 'Средний'
}
const formatShortDate = (dateStr) => {
try {
const d = new Date(dateStr)
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
} catch {
return dateStr
}
}
const openTask = (task) => {
navigateTo(`/canvas/${task.canvasId}`)
}
onMounted(async () => {
if (!auth.user.value) {
await auth.init()
}
await Promise.all([
fetchTasks(),
fetchCanvases()
])
})
</script>
<style scoped>
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #475569 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #475569;
border-radius: 4px;
}
</style>