<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api, type Project, type Task } from '../api'
// Стек входящих: сырые задачи, ожидающие детализации и утверждения.
const stack = ref<Task[]>([])
const projects = ref<Project[]>([])
const tags = ref<{ id: number; name: string }[]>([])
const quickTitle = ref('')
const error = ref('')
const loading = ref(false)
const editing = ref<Task | null>(null)
const editForm = ref({
title: '',
description: '',
project_id: '' as string | number,
tagSelection: {} as Record<number, boolean>,
priority: null as number | null,
})
async function loadStack() {
loading.value = true
try {
stack.value = await api.listTasks({ detail_state: 'raw' })
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
async function quickAdd() {
const title = quickTitle.value.trim()
if (!title) return
error.value = ''
try {
await api.createTask(title)
quickTitle.value = ''
await loadStack()
} catch (e) {
error.value = String(e)
}
}
function selectedTagIds(): number[] {
return Object.entries(editForm.value.tagSelection)
.filter(([, on]) => on)
.map(([id]) => Number(id))
}
function startEdit(task: Task) {
editing.value = task
const selection: Record<number, boolean> = {}
task.tags.forEach((t) => (selection[t.id] = true))
editForm.value = {
title: task.title,
description: task.description,
project_id: task.project?.id ?? '',
tagSelection: selection,
priority: task.priority,
}
}
async function saveEdit() {
if (!editing.value) return
error.value = ''
try {
await api.updateTask(editing.value.id, {
title: editForm.value.title,
description: editForm.value.description,
project_id: editForm.value.project_id === '' ? null : Number(editForm.value.project_id),
tag_ids: selectedTagIds(),
priority: editForm.value.priority,
})
await api.approveTask(editing.value.id)
editing.value = null
await loadStack()
} catch (e) {
error.value = String(e)
}
}
async function approve(task: Task) {
error.value = ''
try {
await api.approveTask(task.id)
await loadStack()
} catch (e) {
error.value = String(e)
}
}
async function remove(task: Task) {
error.value = ''
try {
await api.deleteTask(task.id)
await loadStack()
} catch (e) {
error.value = String(e)
}
}
async function createProjectInline() {
const name = window.prompt('Название нового проекта:')
if (!name) return
try {
const p = await api.createProject(name.trim())
projects.value.push(p)
editForm.value.project_id = p.id
} catch (e) {
error.value = String(e)
}
}
async function createTagInline() {
const name = window.prompt('Название нового тега:')
if (!name) return
try {
const t = await api.createTag(name.trim())
tags.value.push(t)
editForm.value.tagSelection[t.id] = true
} catch (e) {
error.value = String(e)
}
}
const projectOptions = () => [
{ value: '', label: '— без проекта —' },
...projects.value.map((p) => ({ value: String(p.id), label: p.name })),
]
onMounted(async () => {
await loadStack()
projects.value = await api.listProjects()
tags.value = await api.listTags()
})
</script>
<template>
<section>
<GnPageHeader kicker="gntodo" title="Стек входящих" :meta="`Задач в стеке: ${stack.length}`" />
<!-- Быстрый ввод: одна строка, без метаданных -->
<form class="quick-add" @submit.prevent="quickAdd">
<GnInput
v-model="quickTitle"
icon="ph-plus"
placeholder="Закинуть задачу — только текст, остальное потом…"
autofocus
/>
<GnButton type="submit" variant="primary" icon="ph-arrow-circle-down">Закинуть</GnButton>
</form>
<GnAlert v-if="error" variant="error">{{ error }}</GnAlert>
<GnEmptyState
v-if="!loading && stack.length === 0"
icon="ph-inbox"
title="Стек пуст"
text="Закиньте первую задачу — детализация подберётся автоматически."
/>
<div class="task-list">
<GnCard v-for="task in stack" :key="task.id" class="task-card">
<template #title>
<template v-if="editing?.id !== task.id">{{ task.title }}</template>
</template>
<!-- Редактирование: метаданные правятся вручную, затем утверждение -->
<form v-if="editing?.id === task.id" class="edit-form" @submit.prevent="saveEdit">
<GnInput v-model="editForm.title" label="Заголовок" required />
<GnTextarea v-model="editForm.description" label="Описание (Markdown)" :rows="4" />
<GnSelect
v-model="editForm.project_id"
label="Проект"
icon="ph-folder"
:options="projectOptions()"
/>
<div class="form-row-actions">
<GnButton type="button" variant="secondary" size="sm" icon="ph-plus" @click="createProjectInline">
Новый проект
</GnButton>
</div>
<fieldset class="tags-fieldset">
<legend>Теги</legend>
<GnCheckbox
v-for="t in tags"
:key="t.id"
v-model="editForm.tagSelection[t.id]"
:label="t.name"
/>
<GnButton type="button" variant="secondary" size="sm" icon="ph-plus" @click="createTagInline">
Новый тег
</GnButton>
</fieldset>
<GnInput v-model.number="editForm.priority" label="Приоритет (0–10)" type="number" />
<div class="form-actions">
<GnButton type="submit" variant="accent" icon="ph-check-circle">Утвердить</GnButton>
<GnButton type="button" variant="secondary" @click="editing = null">Отмена</GnButton>
</div>
</form>
<!-- Просмотр сырой задачи -->
<template v-else>
<div class="task-meta">
<GnBadge variant="info">в стеке</GnBadge>
<span class="muted">{{ new Date(task.created_at).toLocaleString('ru') }}</span>
</div>
<div class="task-actions">
<GnButton variant="accent" size="sm" icon="ph-check" @click="approve(task)">
Да, всё верно
</GnButton>
<GnButton variant="secondary" size="sm" icon="ph-pencil-simple" @click="startEdit(task)">
Детализировать
</GnButton>
<GnIconButton icon="ph-trash" label="Удалить задачу" @click="remove(task)" />
</div>
</template>
</GnCard>
</div>
</section>
</template>
<style scoped>
.quick-add {
display: flex;
gap: 0.75rem;
align-items: start;
margin-bottom: 1.5rem;
}
.quick-add > :first-child {
flex: 1;
}
.task-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.task-meta {
display: flex;
gap: 0.75rem;
align-items: center;
margin-bottom: 0.75rem;
}
.muted {
color: var(--text-muted, #888);
font-size: 0.85em;
}
.task-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.edit-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-row-actions {
margin-top: -0.5rem;
}
.tags-fieldset {
border: 1px solid var(--border, #333);
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.tags-fieldset legend {
padding: 0 0.5rem;
font-size: 0.85em;
color: var(--text-muted, #888);
}
.form-actions {
display: flex;
gap: 0.75rem;
}
</style>