<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useToast } from 'gnexus-ui-kit/vue'
import { api, type Project } from '../api'
import { useRealtimeRefresh } from '../realtime'
import { relevanceLabel } from '../taskui'
import PriorityBadge from '../components/PriorityBadge.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import Pagination from '../components/Pagination.vue'
// Архив (ТЗ 3.11): история проектов. Архивный проект виден со всей
// историей задач (страница проекта доступна на чтение), его можно
// восстановить как был или удалить навсегда.
const { t } = useI18n()
const router = useRouter()
const toast = useToast()
const projects = ref<Project[]>([])
const error = ref('')
const loading = ref(false)
// Счётчики задач по проектам: всего / завершено (как на «Проектах»)
const taskCounts = ref(new Map<number, { total: number; done: number }>())
async function load() {
loading.value = true
error.value = ''
try {
const [archived, tasks] = await Promise.all([
api.listProjects(true),
api.listTasks({ include_archived: 'true' }),
])
projects.value = archived
const map = new Map<number, { total: number; done: number }>()
tasks.forEach((task) => {
if (!task.project) return
const entry = map.get(task.project.id) ?? { total: 0, done: 0 }
entry.total += 1
if (task.status === 'done') entry.done += 1
map.set(task.project.id, entry)
})
taskCounts.value = map
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
onMounted(load)
// Реактивность (ТЗ 3.14): архив/восстановление из другой вкладки или MCP
useRealtimeRefresh(load, ['project.changed', 'project.deleted'])
// Пагинация: по 30 проектов на страницу
const page = ref(1)
const pagedProjects = computed(() =>
projects.value.slice((page.value - 1) * 30, page.value * 30),
)
function countsOf(id: number): { total: number; done: number } {
return taskCounts.value.get(id) ?? { total: 0, done: 0 }
}
// Восстановить: проект возвращается в «Проекты» со всеми задачами
async function restore(project: Project) {
try {
await api.restoreProject(project.id)
toast.success({ title: t('archive.restored', { name: project.name }) })
await load()
} catch (e) {
toast.error({ title: t('common.error'), text: String(e) })
}
}
// Удалить навсегда — через подтверждение
const deleteTarget = ref<Project | null>(null)
const confirmOpen = ref(false)
function askDelete(project: Project) {
deleteTarget.value = project
confirmOpen.value = true
}
async function remove() {
const project = deleteTarget.value
if (!project) return
confirmOpen.value = false
try {
await api.deleteProject(project.id)
toast.success({ title: t('common.projectDeleted') })
await load()
} catch (e) {
toast.error({ title: t('common.error'), text: String(e) })
}
}
const headerMeta = computed(() =>
projects.value.length ? t('archive.count', { n: projects.value.length }) : '',
)
</script>
<template>
<section>
<GnPageHeader kicker="GNexus Tasks" :title="t('archive.title')">
<template #meta>{{ headerMeta }}</template>
<!-- Спиннер обновления: данные уже есть, история дотягивается -->
<template #actions>
<GnLoader
v-if="loading && projects.length"
circle
label=""
:aria-label="t('common.loading')"
class="refresh-loader"
/>
</template>
</GnPageHeader>
<GnAlert v-if="error" variant="error">{{ error }}</GnAlert>
<!-- Скелетон карточек архива: имя + ряд бейджей, как в реальных -->
<div v-if="loading && projects.length === 0" class="list-skeleton" aria-hidden="true">
<div v-for="i in 2" :key="i" class="card">
<div class="card-content">
<span class="skeleton skeleton-title" />
<div class="sk-badges">
<span v-for="j in 2" :key="j" class="skeleton skeleton-square" />
</div>
</div>
</div>
</div>
<GnEmptyState
v-else-if="projects.length === 0"
icon="ph-archive"
:title="t('archive.emptyTitle')"
:text="t('archive.emptyText')"
/>
<div class="archive-list">
<GnCard v-for="project in pagedProjects" :key="project.id" class="archive-card">
<template #title>
<RouterLink class="card-link" :to="`/projects/${project.id}`">{{ project.name }}</RouterLink>
</template>
<div class="archive-meta">
<GnBadge variant="neutral"><i class="ph ph-activity" aria-hidden="true" />
{{ relevanceLabel(project.relevance_status) }}
</GnBadge>
<PriorityBadge v-if="project.priority !== null" :priority="project.priority" />
<GnBadge variant="neutral"><i class="ph ph-list-checks" aria-hidden="true" />
{{ t('projects.taskCounts', countsOf(project.id)) }}
</GnBadge>
</div>
<div class="form-actions">
<GnButton variant="secondary" size="sm" icon="ph-arrow-square-out" @click="router.push(`/projects/${project.id}`)">
{{ t('common.open') }}
</GnButton>
<GnButton variant="success" size="sm" icon="ph-arrow-counter-clockwise" @click="restore(project)">
{{ t('archive.restore') }}
</GnButton>
<GnIconButton icon="ph-trash" :label="t('archive.deleteForever')" @click="askDelete(project)" />
</div>
</GnCard>
</div>
<Pagination v-model:page="page" :total="projects.length" />
<ConfirmDialog
v-model:open="confirmOpen"
:title="t('common.confirmTitle')"
:message="t('archive.deleteConfirm', { name: deleteTarget?.name ?? '' })"
:confirm-text="t('archive.deleteForever')"
:cancel-text="t('common.cancel')"
confirm-variant="danger"
confirm-icon="ph-trash"
@confirm="remove"
/>
</section>
</template>
<style scoped>
.archive-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card-link {
color: inherit;
text-decoration: none;
}
.card-link:hover {
color: var(--accent, #7aa2f7);
}
.list-skeleton {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* карточка-скелет: как реальная карточка архива — тянется на ширину */
.list-skeleton .card {
width: 100%;
max-width: none;
}
.list-skeleton .card-content > .skeleton-title {
display: block;
margin-bottom: 10px;
}
.sk-badges {
display: flex;
gap: 6px;
}
.archive-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.form-actions {
display: flex;
gap: 0.75rem;
align-items: center;
}
</style>