Newer
Older
gnexus-tasks / frontend / src / views / ListView.vue
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { api, type Project, type Tag, type Task } from '../api'
import {
  budgetBadge,
  deadlineLabel,
  deadlineOverdue,
  formatMinutes,
  recurrenceLabel,
  statusLabel,
  statusOptions,
  statusVariant,
} from '../taskui'

// Классический вид (ТЗ 3.6): список с фильтрами — проект, тег, статус.

const { t } = useI18n()

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 statusOptionsWithAny = computed(() => [
  { value: '', label: t('common.anyStatus') },
  ...statusOptions(),
])
</script>

<template>
  <section>
    <GnPageHeader kicker="gntodo" :title="t('list.title')">
      <template #meta>{{ t('list.count', { n: tasks.length }) }}</template>
    </GnPageHeader>

    <form class="filters" @submit.prevent>
      <GnSelect v-model="filters.status" :label="t('list.filterStatus')" icon="ph-funnel" :options="statusOptionsWithAny" />
      <GnSelect
        v-model="filters.project_id"
        :label="t('list.filterProject')"
        icon="ph-folder"
        :options="[
          { value: '', label: t('common.any') },
          ...projects.map((p) => ({ value: String(p.id), label: p.name })),
        ]"
      />
      <GnSelect
        v-model="filters.tag_id"
        :label="t('list.filterTag')"
        icon="ph-tag"
        :options="[
          { value: '', label: t('common.any') },
          ...tags.map((tg) => ({ value: String(tg.id), label: tg.name })),
        ]"
      />
      <GnButton variant="secondary" icon="ph-funnel" @click="load">{{ t('list.apply') }}</GnButton>
    </form>

    <GnAlert v-if="error" variant="error">{{ error }}</GnAlert>

    <GnEmptyState
      v-if="!loading && tasks.length === 0"
      icon="ph-list-magnifying-glass"
      :title="t('list.emptyTitle')"
      :text="t('list.emptyText')"
    />

    <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) || t('taskui.recur.recurring') }}
          </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">
            {{ budgetBadge(task) }}
          </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="statusOptionsWithAny"
            @update:model-value="(s: string) => setStatus(task, s)"
          />
          <GnIconButton icon="ph-trash" :label="t('stack.deleteTask')" @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>