Newer
Older
gnexus-tasks / frontend / src / views / OptionsView.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from 'gnexus-ui-kit/vue'
import { api, type Task } from '../api'
import { formatMinutes, renderMarkdown, statusLabel } from '../taskui'

// Режим «3 варианта» (ТЗ 3.8): анти-прокрастинация — выбрать задачу под
// доступное время и делать её прямо сейчас.

const { t } = useI18n()
const toast = useToast()

const availableMinutes = ref(180)
const options = ref<Task[]>([])
const error = ref('')
const loading = ref(false)
const chosen = ref<Task | null>(null)

async function fetchOptions() {
  loading.value = true
  error.value = ''
  options.value = []
  try {
    options.value = await api.suggestTasks(availableMinutes.value)
  } catch (e) {
    error.value = String(e)
  } finally {
    loading.value = false
  }
}

onMounted(fetchOptions)

async function choose(task: Task) {
  try {
    chosen.value = await api.updateTask(task.id, { status: 'in_progress' })
    toast.success({ title: t('common.taskTaken') })
    options.value = []
  } catch (e) {
    toast.error({ title: t('common.error'), text: String(e) })
  }
}

async function finishChosen() {
  if (!chosen.value) return
  try {
    await api.updateTask(chosen.value.id, { status: 'done' })
    toast.success({ title: t('common.taskFinished') })
    chosen.value = null
    await fetchOptions()
  } catch (e) {
    toast.error({ title: t('common.error'), text: String(e) })
  }
}

async function dropChosen() {
  if (!chosen.value) return
  chosen.value = null
  await fetchOptions()
}
</script>

<template>
  <section>
    <GnPageHeader kicker="GNexus Tasks" :title="t('options.title')" />

    <!-- Выбранная задача: прямой переход к выполнению -->
    <GnCard v-if="chosen" class="chosen-card">
      <template #title>{{ chosen.title }}</template>
      <div class="chosen-meta">
        <GnBadge variant="accent">{{ statusLabel(chosen.status) }}</GnBadge>
        <GnBadge v-if="chosen.estimated_minutes" variant="warning">
          ≈ {{ formatMinutes(chosen.estimated_minutes) }}
        </GnBadge>
        <GnBadge v-if="chosen.project" variant="neutral">{{ chosen.project.name }}</GnBadge>
      </div>
      <div v-if="chosen.description" class="md-preview" v-html="renderMarkdown(chosen.description)" />
      <div class="form-actions">
        <GnButton variant="accent" icon="ph-check-circle" @click="finishChosen">
          {{ t('options.done') }}
        </GnButton>
        <GnButton variant="secondary" icon="ph-shuffle" @click="dropChosen">
          {{ t('options.notNow') }}
        </GnButton>
      </div>
    </GnCard>

    <form v-else class="time-form" @submit.prevent="fetchOptions">
      <GnInput
        v-model.number="availableMinutes"
        :label="t('options.available')"
        type="number"
        icon="ph-hourglass"
        min="5"
        max="1440"
      />
      <GnButton type="submit" variant="primary" icon="ph-shuffle" :disabled="loading">
        {{ t('options.suggest') }}
      </GnButton>
    </form>

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

    <template v-if="!chosen">
      <GnSkeleton v-if="loading" type="block" stack :count="3" class="list-skeleton" />
      <GnEmptyState
        v-else-if="options.length === 0"
        icon="ph-confetti"
        :title="t('options.emptyTitle')"
        :text="t('options.emptyText')"
      />
      <div class="options">
        <GnCard v-for="task in options" :key="task.id" class="option-card">
          <template #title>{{ task.title }}</template>
          <div class="option-meta">
            <GnBadge v-if="task.estimated_minutes" variant="warning">
              ≈ {{ formatMinutes(task.estimated_minutes) }}
            </GnBadge>
            <GnBadge v-if="task.priority !== null" variant="info">P{{ task.priority }}</GnBadge>
            <GnBadge v-if="task.project" variant="neutral">{{ task.project.name }}</GnBadge>
          </div>
          <GnButton variant="accent" icon="ph-play" @click="choose(task)">
            {{ t('options.choose') }}
          </GnButton>
        </GnCard>
      </div>
    </template>
  </section>
</template>

<style scoped>
.time-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: end;
  margin-bottom: 1.5rem;
}
/* GnInput рендерит div.form-group > label > input — ограничиваем обёртку */
.time-form :deep(.form-group) {
  width: 12rem;
  margin-bottom: 0;
}
.options {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.list-skeleton {
  margin-bottom: 1rem;
}
.option-meta,
.chosen-meta {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  flex-wrap: wrap;
}
.chosen-card {
  margin-bottom: 1.5rem;
}
.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.md-preview {
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  padding: 0.75rem;
  margin-bottom: 0.75rem;
  overflow-x: auto;
}
</style>