Newer
Older
gnexus-tasks / frontend / src / views / ListView.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api, type Project, type Tag, type Task } from '../api'
import { STATUS_LABELS, formatMinutes, formatMoney, 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.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>