<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { api, type Project } from '../api'
import { RELEVANCE_LABELS, relevanceLabel, renderMarkdown } from '../taskui'
// Проекты (ТЗ 3.7): актуальность, приоритет, Markdown-заметка с ресурсами.
const projects = ref<Project[]>([])
const error = ref('')
const loading = ref(false)
const showPreview = ref(false)
const editing = ref<Project | null>(null)
const editForm = ref({
name: '',
relevance_status: 'active',
priority: null as number | null,
note: '',
})
async function load() {
loading.value = true
error.value = ''
try {
projects.value = await api.listProjects()
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
onMounted(load)
const notePreview = computed(() => renderMarkdown(editForm.value.note))
function startEdit(project: Project) {
editing.value = project
showPreview.value = false
editForm.value = {
name: project.name,
relevance_status: project.relevance_status,
priority: project.priority,
note: project.note,
}
}
async function saveEdit() {
if (!editing.value) return
error.value = ''
try {
await api.updateProject(editing.value.id, {
name: editForm.value.name,
relevance_status: editForm.value.relevance_status,
priority: editForm.value.priority,
note: editForm.value.note,
})
editing.value = null
await load()
} catch (e) {
error.value = String(e)
}
}
async function createProject() {
const name = window.prompt('Название нового проекта:')
if (!name) return
error.value = ''
try {
await api.createProject(name.trim())
await load()
} catch (e) {
error.value = String(e)
}
}
async function remove(project: Project) {
error.value = ''
try {
await api.deleteProject(project.id)
await load()
} catch (e) {
error.value = String(e)
}
}
const relevanceOptions = Object.entries(RELEVANCE_LABELS).map(([value, label]) => ({ value, label }))
</script>
<template>
<section>
<GnPageHeader kicker="gntodo" title="Проекты">
<template #meta>Проектов: {{ projects.length }}</template>
<template #actions>
<GnButton variant="primary" icon="ph-plus" @click="createProject">Новый проект</GnButton>
</template>
</GnPageHeader>
<GnAlert v-if="error" variant="error">{{ error }}</GnAlert>
<GnEmptyState
v-if="!loading && projects.length === 0"
icon="ph-folders"
title="Проектов нет"
text="Создайте проект — задачи можно будет группировать и добавлять заметки с ресурсами."
/>
<div class="project-list">
<GnCard v-for="project in projects" :key="project.id" class="project-card">
<template #title>
<template v-if="editing?.id !== project.id">{{ project.name }}</template>
</template>
<form v-if="editing?.id === project.id" class="edit-form" @submit.prevent="saveEdit">
<GnInput v-model="editForm.name" label="Название" required />
<GnSelect
v-model="editForm.relevance_status"
label="Актуальность"
icon="ph-activity"
:options="relevanceOptions"
/>
<GnInput v-model.number="editForm.priority" label="Приоритет (0–10)" type="number" />
<GnTextarea v-model="editForm.note" label="Заметка (Markdown: ссылки, контекст)" :rows="6" />
<GnCheckbox v-model="showPreview" label="Предпросмотр" />
<div v-if="showPreview" class="md-preview" v-html="notePreview" />
<div class="form-actions">
<GnButton type="submit" variant="accent" icon="ph-check">Сохранить</GnButton>
<GnButton type="button" variant="secondary" @click="editing = null">Отмена</GnButton>
</div>
</form>
<template v-else>
<div class="project-meta">
<GnBadge variant="info">{{ relevanceLabel(project.relevance_status) }}</GnBadge>
<GnBadge v-if="project.priority !== null" variant="warning">
P{{ project.priority }}
</GnBadge>
</div>
<div v-if="project.note" class="md-preview note" v-html="renderMarkdown(project.note)" />
<div class="form-actions">
<GnButton variant="secondary" size="sm" icon="ph-pencil-simple" @click="startEdit(project)">
Редактировать
</GnButton>
<GnIconButton icon="ph-trash" label="Удалить проект" @click="remove(project)" />
</div>
</template>
</GnCard>
</div>
</section>
</template>
<style scoped>
.project-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.project-meta {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.edit-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-actions {
display: flex;
gap: 0.75rem;
align-items: center;
}
.md-preview {
border: 1px solid var(--border, #333);
border-radius: 6px;
padding: 0.75rem;
overflow-x: auto;
}
.note {
margin-bottom: 0.75rem;
}
</style>