<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api, type Project, type Tag, type Task } from '../api'
import {
STATUS_LABELS,
deadlineLabel,
deadlineOverdue,
formatMinutes,
formatMoney,
recurrenceLabel,
statusLabel,
statusVariant,
} from '../taskui'
// Классический вид (ТЗ 3.6): список с фильтрами — проект, тег, статус.
const tasks = ref<Task[]>([])
const projects = ref<Project[]>([])
const tags = ref<Tag[]>([])
const error = ref('')
const loading = ref(false)
const filters = ref({
status: '',
project_id: '',
tag_id: '',
})
async function load() {
loading.value = true
error.value = ''
try {
tasks.value = await api.listTasks({
detail_state: 'approved',
status: filters.value.status,
project_id: filters.value.project_id,
tag_id: filters.value.tag_id,
})
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
onMounted(async () => {
await load()
projects.value = await api.listProjects()
tags.value = await api.listTags()
})
async function setStatus(task: Task, status: string) {
error.value = ''
try {
await api.updateTask(task.id, { status })
await load()
} catch (e) {
error.value = String(e)
}
}
async function remove(task: Task) {
error.value = ''
try {
await api.deleteTask(task.id)
await load()
} catch (e) {
error.value = String(e)
}
}
const statusOptions = [
{ value: '', label: '— любой статус —' },
...Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label })),
]
</script>
<template>
<section>
<GnPageHeader kicker="gntodo" title="Список задач">
<template #meta>Найдено: {{ tasks.length }}</template>
</GnPageHeader>
<form class="filters" @submit.prevent>
<GnSelect v-model="filters.status" label="Статус" icon="ph-funnel" :options="statusOptions" />
<GnSelect
v-model="filters.project_id"
label="Проект"
icon="ph-folder"
:options="[
{ value: '', label: '— любой —' },
...projects.map((p) => ({ value: String(p.id), label: p.name })),
]"
/>
<GnSelect
v-model="filters.tag_id"
label="Тег"
icon="ph-tag"
:options="[
{ value: '', label: '— любой —' },
...tags.map((t) => ({ value: String(t.id), label: t.name })),
]"
/>
<GnButton variant="secondary" icon="ph-funnel" @click="load">Применить</GnButton>
</form>
<GnAlert v-if="error" variant="error">{{ error }}</GnAlert>
<GnEmptyState
v-if="!loading && tasks.length === 0"
icon="ph-list-magnifying-glass"
title="Ничего не найдено"
text="Попробуйте ослабить фильтры или закиньте новую задачу в стек."
/>
<div class="task-list">
<GnCard v-for="task in tasks" :key="task.id" class="task-card">
<template #title>{{ task.title }}</template>
<div class="task-row">
<GnBadge :variant="statusVariant(task.status)">{{ statusLabel(task.status) }}</GnBadge>
<GnBadge v-if="task.project" variant="neutral" icon="ph-folder">
{{ task.project.name }}
</GnBadge>
<GnBadge v-if="task.priority !== null" variant="warning">P{{ task.priority }}</GnBadge>
<GnBadge v-if="task.estimated_minutes" variant="neutral">
≈ {{ formatMinutes(task.estimated_minutes) }}
</GnBadge>
<GnBadge v-if="task.task_type === 'recurring'" variant="info" icon="ph-repeat">
{{ recurrenceLabel(task) || 'регулярная' }}
</GnBadge>
<GnBadge
v-if="deadlineLabel(task)"
:variant="deadlineOverdue(task) ? 'error' : 'warning'"
icon="ph-alarm"
>
{{ deadlineLabel(task) }}
</GnBadge>
<GnBadge v-if="task.budget_money !== null" variant="neutral">
бюджет {{ formatMoney(task.budget_money) }}
<template v-if="task.cost_estimate_money !== null">
/ оценка {{ formatMoney(task.cost_estimate_money) }}
</template>
</GnBadge>
<GnBadge v-for="tag in task.tags" :key="tag.id" variant="neutral">{{ tag.name }}</GnBadge>
<span class="spacer" />
<GnSelect
class="status-select"
:model-value="task.status"
:options="statusOptions"
@update:model-value="(s: string) => setStatus(task, s)"
/>
<GnIconButton icon="ph-trash" label="Удалить задачу" @click="remove(task)" />
</div>
</GnCard>
</div>
</section>
</template>
<style scoped>
.filters {
display: flex;
gap: 0.75rem;
align-items: end;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.task-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.task-row {
display: flex;
gap: 0.4rem;
align-items: center;
flex-wrap: wrap;
}
.spacer {
flex: 1;
}
.status-select {
min-width: 9rem;
}
</style>