Newer
Older
gnexus-tasks / frontend / src / views / TreeView.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { api, type Project, type Task } from '../api'
import QuickPrompt from '../components/QuickPrompt.vue'
import TaskTree from '../components/TaskTree.vue'

defineOptions({ name: 'TreeView' })

// Дерево задач (основной вид, ТЗ 3.6): подзадачи — ветви произвольной вложенности.
// Сырые задачи живут в стеке; дерево показывает утверждённые. Наглядность
// (шевроны, линии, прогресс поддерева) — в компоненте TaskTree.

const { t } = useI18n()

const tasks = ref<Task[]>([])
const projects = ref<Project[]>([])
const error = ref('')
const loading = ref(false)

async function load() {
  loading.value = true
  error.value = ''
  try {
    tasks.value = await api.listTasks({ detail_state: 'approved' })
  } catch (e) {
    error.value = String(e)
  } finally {
    loading.value = false
  }
}

onMounted(async () => {
  await load()
  projects.value = await api.listProjects()
})

async function setStatus(task: Task, status: string) {
  error.value = ''
  try {
    await api.updateTask(task.id, { status })
    await load()
  } catch (e) {
    error.value = String(e)
  }
}

// Быстрое добавление подзадачи — модальное окно вместо window.prompt
const subtaskFor = ref<Task | null>(null)
const subtaskOpen = ref(false)

function openSubtaskPrompt(task: Task) {
  subtaskFor.value = task
  subtaskOpen.value = true
}

async function createSubtask(title: string) {
  if (!subtaskFor.value) return
  error.value = ''
  try {
    await api.createTask(title, '', subtaskFor.value.id)
    // подзадача попадёт в стек; в дереве появится после утверждения
    await load()
  } catch (e) {
    error.value = String(e)
  }
}

// Удаление — только через подтверждение (действие необратимо)
const deleteTarget = ref<Task | null>(null)
const confirmOpen = ref(false)

function askDelete(task: Task) {
  deleteTarget.value = task
  confirmOpen.value = true
}

async function remove() {
  const task = deleteTarget.value
  if (!task) return
  confirmOpen.value = false
  error.value = ''
  try {
    await api.deleteTask(task.id)
    await load()
  } catch (e) {
    error.value = String(e)
  }
}
</script>

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

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

    <GnSkeleton v-if="loading" type="line" stack :count="6" class="tree-skeleton" />

    <GnEmptyState
      v-if="!loading && tasks.length === 0"
      icon="ph-tree-structure"
      :title="t('tree.emptyTitle')"
      :text="t('tree.emptyText')"
    >
      <template #actions>
        <GnButton variant="primary" icon="ph-tray" @click="$router.push('/stack')">
          {{ t('nav.stack') }}
        </GnButton>
      </template>
    </GnEmptyState>

    <TaskTree
      :tasks="tasks"
      @status-change="setStatus"
      @add-subtask="openSubtaskPrompt"
      @delete="askDelete"
    />

    <QuickPrompt
      v-model:open="subtaskOpen"
      :title="t('tree.promptSubtask')"
      @confirm="createSubtask"
    />

    <GnConfirmDialog
      v-model:open="confirmOpen"
      :title="t('common.confirmTitle')"
      :message="t('common.deleteConfirmTask', { title: deleteTarget?.title ?? '' })"
      :confirm-text="t('common.delete')"
      :cancel-text="t('common.cancel')"
      confirm-variant="danger"
      @confirm="remove"
    />
  </section>
</template>

<style scoped>
.tree-skeleton {
  margin-bottom: 1rem;
}
</style>