Newer
Older
gnexus-tasks / frontend / src / components / TaskCard.vue
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import type { Task } from '../api'
import { proposalItems, statusOptions } from '../taskui'
import TaskBadges from './TaskBadges.vue'

// Карточка задачи для списков (список, страница проекта): заголовок-ссылка
// на страницу задачи, сниппет описания/предложения ИИ, бейджи, «⋯»-меню.
// API-вызовы и подтверждение удаления — на родителе (эмитим события).

const props = defineProps<{
  task: Task
  /** Показывать бейдж проекта (скрывать на странице проекта) */
  showProject?: boolean
}>()

const emit = defineEmits<{
  'status-change': [task: Task, status: string]
  delete: [task: Task]
}>()

const { t } = useI18n()
const router = useRouter()

// Сниппет: описание задачи, а без него — пункты предложения ИИ.
// Markdown-разметку убираем — сниппет plain-текст.
const snippet = computed(() => {
  const text =
    props.task.description.trim() || proposalItems(props.task).map((i) => i.label).join(' · ')
  return text
    .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
    .replace(/\*\*([^*]+)\*\*/g, '$1')
    .replace(/\*([^*]+)\*/g, '$1')
    .replace(/`([^`]+)`/g, '$1')
})

function menu(task: Task) {
  return [
    ...statusOptions().map((o) => ({
      label: o.label,
      icon: o.value === task.status ? 'ph-check' : undefined,
      onSelect: () => emit('status-change', task, o.value),
    })),
    { label: t('card.open'), icon: 'ph-arrow-square-out', onSelect: () => open(task.id) },
    { label: t('common.delete'), icon: 'ph-trash', danger: true, onSelect: () => emit('delete', task) },
  ]
}

function open(id: number) {
  void router.push(`/tasks/${id}`)
}
</script>

<template>
  <GnCard class="task-card">
    <div class="task-main">
      <RouterLink class="title" :class="{ done: task.status === 'done' }" :to="`/tasks/${task.id}`">
        {{ task.title }}
      </RouterLink>
      <GnDropdown class="task-menu" :items="menu(task)">
        <template #trigger="{ toggle }">
          <GnIconButton icon="ph-dots-three-outline" :label="t('common.edit')" @click="toggle" />
        </template>
      </GnDropdown>
    </div>
    <p v-if="snippet" class="snippet">{{ snippet }}</p>
    <div class="task-meta">
      <TaskBadges :task="task" :hide-project="!showProject" />
    </div>
  </GnCard>
</template>

<style scoped>
.task-main {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.title {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.title:hover {
  color: var(--accent, #7aa2f7);
}
.title.done {
  text-decoration: line-through;
  color: var(--text-muted, #888);
}
.snippet {
  margin: 0.25rem 0 0;
  color: var(--text-muted, #a9b1d6);
  font-size: 0.9em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.task-menu {
  flex: none;
}
.task-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  align-items: center;
  margin-top: 0.5rem;
}
</style>