Newer
Older
gnexus-tasks / frontend / src / views / ProjectsView.vue
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { api, type Project } from '../api'
import { relevanceLabel, renderMarkdown } from '../taskui'

// Проекты (ТЗ 3.7): актуальность, приоритет, Markdown-заметка с ресурсами.

const { t } = useI18n()

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

const editing = ref<Project | null>(null)
const editForm = ref({
  name: '',
  relevance_status: 'active',
  priority: null as number | null,
  note: '',
})

async function load() {
  loading.value = true
  error.value = ''
  try {
    projects.value = await api.listProjects()
  } catch (e) {
    error.value = String(e)
  } finally {
    loading.value = false
  }
}

onMounted(load)

const notePreview = computed(() => renderMarkdown(editForm.value.note))

function startEdit(project: Project) {
  editing.value = project
  showPreview.value = false
  editForm.value = {
    name: project.name,
    relevance_status: project.relevance_status,
    priority: project.priority,
    note: project.note,
  }
}

async function saveEdit() {
  if (!editing.value) return
  error.value = ''
  try {
    await api.updateProject(editing.value.id, {
      name: editForm.value.name,
      relevance_status: editForm.value.relevance_status,
      priority: editForm.value.priority,
      note: editForm.value.note,
    })
    editing.value = null
    await load()
  } catch (e) {
    error.value = String(e)
  }
}

async function createProject() {
  const name = window.prompt(t('projects.prompt'))
  if (!name) return
  error.value = ''
  try {
    await api.createProject(name.trim())
    await load()
  } catch (e) {
    error.value = String(e)
  }
}

async function remove(project: Project) {
  error.value = ''
  try {
    await api.deleteProject(project.id)
    await load()
  } catch (e) {
    error.value = String(e)
  }
}

const relevanceOptions = computed(() =>
  ['active', 'paused', 'archived'].map((value) => ({ value, label: relevanceLabel(value) })),
)
</script>

<template>
  <section>
    <GnPageHeader kicker="gntodo" :title="t('projects.title')">
      <template #meta>{{ t('projects.count', { n: projects.length }) }}</template>
      <template #actions>
        <GnButton variant="primary" icon="ph-plus" @click="createProject">
          {{ t('projects.new') }}
        </GnButton>
      </template>
    </GnPageHeader>

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

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

    <div class="project-list">
      <GnCard v-for="project in projects" :key="project.id" class="project-card">
        <template #title>
          <template v-if="editing?.id !== project.id">{{ project.name }}</template>
        </template>

        <form v-if="editing?.id === project.id" class="edit-form" @submit.prevent="saveEdit">
          <GnInput v-model="editForm.name" :label="t('projects.form.name')" required />
          <GnSelect
            v-model="editForm.relevance_status"
            :label="t('projects.form.relevance')"
            icon="ph-activity"
            :options="relevanceOptions"
          />
          <GnInput v-model.number="editForm.priority" :label="t('projects.form.priority')" type="number" />
          <GnTextarea v-model="editForm.note" :label="t('projects.form.note')" :rows="6" />
          <GnCheckbox v-model="showPreview" :label="t('projects.form.preview')" />
          <div v-if="showPreview" class="md-preview" v-html="notePreview" />
          <div class="form-actions">
            <GnButton type="submit" variant="accent" icon="ph-check">
              {{ t('common.save') }}
            </GnButton>
            <GnButton type="button" variant="secondary" @click="editing = null">
              {{ t('common.cancel') }}
            </GnButton>
          </div>
        </form>

        <template v-else>
          <div class="project-meta">
            <GnBadge variant="info">{{ relevanceLabel(project.relevance_status) }}</GnBadge>
            <GnBadge v-if="project.priority !== null" variant="warning">
              P{{ project.priority }}
            </GnBadge>
          </div>
          <div v-if="project.note" class="md-preview note" v-html="renderMarkdown(project.note)" />
          <div class="form-actions">
            <GnButton variant="secondary" size="sm" icon="ph-pencil-simple" @click="startEdit(project)">
              {{ t('projects.edit') }}
            </GnButton>
            <GnIconButton icon="ph-trash" :label="t('projects.delete')" @click="remove(project)" />
          </div>
        </template>
      </GnCard>
    </div>
  </section>
</template>

<style scoped>
.project-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.project-meta {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.edit-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.form-actions {
  display: flex;
  gap: 0.75rem;
  align-items: center;
}
.md-preview {
  border: 1px solid var(--border, #333);
  border-radius: 6px;
  padding: 0.75rem;
  overflow-x: auto;
}
.note {
  margin-bottom: 0.75rem;
}
</style>