407 lines
11 KiB
Vue
407 lines
11 KiB
Vue
<template>
|
||
<PageLayout>
|
||
<!-- Шапка с заголовком и фильтрами -->
|
||
<Header title="Архив задач">
|
||
<template #filters>
|
||
<DepartmentTags
|
||
v-model="activeDepartment"
|
||
@project-change="onProjectChange"
|
||
/>
|
||
</template>
|
||
|
||
<!-- Мобильные фильтры -->
|
||
<template #mobile-filters>
|
||
<ProjectSelector @change="onProjectChange" />
|
||
<MobileSelect
|
||
v-model="activeDepartment"
|
||
:options="departmentOptions"
|
||
title="Фильтр по отделам"
|
||
placeholder="Все отделы"
|
||
icon="filter"
|
||
compact
|
||
/>
|
||
</template>
|
||
|
||
<template #stats>
|
||
<div class="header-stats">
|
||
<div class="stat">
|
||
<span class="stat-value">{{ filteredCards.length }}</span>
|
||
<span class="stat-label">в архиве</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</Header>
|
||
|
||
<!-- Список архивных задач -->
|
||
<main class="main" :class="{ mobile: isMobile }">
|
||
<!-- Мобильный заголовок над карточками -->
|
||
<div v-if="isMobile" class="mobile-archive-header">
|
||
<div class="archive-title-row">
|
||
<span class="archive-dot"></span>
|
||
<span class="archive-title">В архиве</span>
|
||
<span class="archive-count">{{ filteredCards.length }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="archive-list" :class="{ mobile: isMobile }">
|
||
<ArchiveCard
|
||
v-for="card in filteredCards"
|
||
:key="card.id"
|
||
:card="card"
|
||
@click="openTaskPanel(card)"
|
||
@restore="confirmRestore"
|
||
@delete="confirmDelete"
|
||
/>
|
||
|
||
<!-- Пустое состояние -->
|
||
<div v-if="filteredCards.length === 0 && !loading" class="empty-state" :class="{ mobile: isMobile }">
|
||
<i data-lucide="archive-x"></i>
|
||
<p>Архив пуст</p>
|
||
<span>Архивированные задачи появятся здесь</span>
|
||
</div>
|
||
|
||
<!-- Загрузка -->
|
||
<Loader v-if="loading" />
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Модальные окна -->
|
||
<template #modals>
|
||
<TaskPanel
|
||
:show="panelOpen"
|
||
:card="editingCard"
|
||
:column-id="null"
|
||
:is-archived="true"
|
||
:on-save="handleSaveTask"
|
||
@close="closePanel"
|
||
@delete="handleDeleteTask"
|
||
@restore="handleRestoreFromPanel"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:show="confirmDialogOpen"
|
||
type="deleteTask"
|
||
:action="handleConfirmDelete"
|
||
@confirm="confirmDialogOpen = false"
|
||
@cancel="confirmDialogOpen = false"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:show="restoreDialogOpen"
|
||
type="restore"
|
||
:action="handleConfirmRestore"
|
||
@confirm="restoreDialogOpen = false"
|
||
@cancel="restoreDialogOpen = false"
|
||
/>
|
||
</template>
|
||
</PageLayout>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import PageLayout from '../components/PageLayout.vue'
|
||
import Header from '../components/Header.vue'
|
||
import ArchiveCard from '../components/ArchiveCard.vue'
|
||
import TaskPanel from '../components/TaskPanel'
|
||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||
import DepartmentTags from '../components/DepartmentTags.vue'
|
||
import ProjectSelector from '../components/ProjectSelector.vue'
|
||
import MobileSelect from '../components/ui/MobileSelect.vue'
|
||
import Loader from '../components/ui/Loader.vue'
|
||
import { useProjectsStore } from '../stores/projects'
|
||
import { cardsApi } from '../api'
|
||
import { useMobile } from '../composables/useMobile'
|
||
import { useDepartmentFilter } from '../composables/useDepartmentFilter'
|
||
|
||
const { isMobile } = useMobile()
|
||
|
||
|
||
// ==================== STORE ====================
|
||
const store = useProjectsStore()
|
||
|
||
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
|
||
const { activeDepartment, departmentOptions, resetFilter } = useDepartmentFilter()
|
||
|
||
// ==================== КАРТОЧКИ ====================
|
||
const cards = ref([])
|
||
const loading = ref(true)
|
||
|
||
// Отфильтрованные карточки (сортируем по дате завершения, новые сверху)
|
||
const filteredCards = computed(() => {
|
||
let result = cards.value
|
||
if (activeDepartment.value) {
|
||
result = result.filter(card => card.departmentId === activeDepartment.value)
|
||
}
|
||
return result.sort((a, b) => {
|
||
const dateA = a.dateClosed ? new Date(a.dateClosed).getTime() : 0
|
||
const dateB = b.dateClosed ? new Date(b.dateClosed).getTime() : 0
|
||
return dateB - dateA
|
||
})
|
||
})
|
||
|
||
// Загрузка архивных карточек
|
||
const fetchCards = async () => {
|
||
if (!store.currentProjectId) {
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
try {
|
||
const result = await cardsApi.getAll(store.currentProjectId, 1) // archive = 1
|
||
if (result.success) {
|
||
cards.value = result.data.map(card => ({
|
||
id: card.id,
|
||
title: card.title,
|
||
description: card.descript,
|
||
details: card.descript_full,
|
||
departmentId: card.id_department,
|
||
labelId: card.id_label,
|
||
accountId: card.id_account,
|
||
assignee: card.avatar_img,
|
||
dueDate: card.date,
|
||
dateCreate: card.date_create,
|
||
dateClosed: card.date_closed,
|
||
columnId: card.column_id,
|
||
order: card.order ?? 0,
|
||
comments_count: card.comments_count || 0,
|
||
files: card.files || (card.file_img || []).map(f => ({
|
||
name: f.name,
|
||
url: f.url,
|
||
size: f.size,
|
||
preview: f.url
|
||
}))
|
||
}))
|
||
}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// При смене проекта
|
||
const onProjectChange = async () => {
|
||
resetFilter()
|
||
await fetchCards()
|
||
}
|
||
|
||
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
|
||
const panelOpen = ref(false)
|
||
const editingCard = ref(null)
|
||
|
||
const openTaskPanel = (card) => {
|
||
editingCard.value = card
|
||
panelOpen.value = true
|
||
}
|
||
|
||
const closePanel = () => {
|
||
panelOpen.value = false
|
||
editingCard.value = null
|
||
}
|
||
|
||
const handleSaveTask = async (taskData) => {
|
||
if (taskData.id) {
|
||
await cardsApi.update({
|
||
id: taskData.id,
|
||
id_department: taskData.departmentId,
|
||
id_label: taskData.labelId,
|
||
id_account: taskData.accountId,
|
||
date: taskData.dueDate,
|
||
title: taskData.title,
|
||
descript: taskData.description,
|
||
descript_full: taskData.details
|
||
})
|
||
|
||
const card = cards.value.find(c => c.id === taskData.id)
|
||
if (card) {
|
||
card.title = taskData.title
|
||
card.description = taskData.description
|
||
card.details = taskData.details
|
||
card.departmentId = taskData.departmentId
|
||
card.labelId = taskData.labelId
|
||
card.dueDate = taskData.dueDate
|
||
card.accountId = taskData.accountId
|
||
card.assignee = taskData.assignee
|
||
card.files = taskData.files || []
|
||
}
|
||
}
|
||
closePanel()
|
||
}
|
||
|
||
const handleDeleteTask = async (cardId) => {
|
||
const result = await cardsApi.delete(cardId)
|
||
if (result.success) {
|
||
cards.value = cards.value.filter(c => c.id !== cardId)
|
||
}
|
||
closePanel()
|
||
}
|
||
|
||
// ==================== УДАЛЕНИЕ С ПОДТВЕРЖДЕНИЕМ ====================
|
||
const confirmDialogOpen = ref(false)
|
||
const cardToDelete = ref(null)
|
||
|
||
const confirmDelete = (cardId) => {
|
||
cardToDelete.value = cardId
|
||
confirmDialogOpen.value = true
|
||
}
|
||
|
||
// ==================== ВОССТАНОВЛЕНИЕ С ПОДТВЕРЖДЕНИЕМ ====================
|
||
const restoreDialogOpen = ref(false)
|
||
const cardToRestore = ref(null)
|
||
|
||
const confirmRestore = (cardId) => {
|
||
cardToRestore.value = cardId
|
||
restoreDialogOpen.value = true
|
||
}
|
||
|
||
const handleConfirmDelete = async () => {
|
||
if (!cardToDelete.value) {
|
||
throw new Error('Задача не выбрана')
|
||
}
|
||
|
||
const result = await cardsApi.delete(cardToDelete.value)
|
||
if (!result.success) {
|
||
throw new Error('Ошибка удаления задачи')
|
||
}
|
||
|
||
cards.value = cards.value.filter(c => c.id !== cardToDelete.value)
|
||
cardToDelete.value = null
|
||
}
|
||
|
||
// ==================== ВОССТАНОВЛЕНИЕ (действие) ====================
|
||
const handleConfirmRestore = async () => {
|
||
if (!cardToRestore.value) {
|
||
throw new Error('Задача не выбрана')
|
||
}
|
||
|
||
const result = await cardsApi.setArchive(cardToRestore.value, 0)
|
||
if (!result.success) {
|
||
throw new Error('Ошибка восстановления задачи')
|
||
}
|
||
|
||
cards.value = cards.value.filter(c => c.id !== cardToRestore.value)
|
||
cardToRestore.value = null
|
||
}
|
||
|
||
const handleRestoreFromPanel = async (cardId) => {
|
||
const result = await cardsApi.setArchive(cardId, 0)
|
||
if (result.success) {
|
||
cards.value = cards.value.filter(c => c.id !== cardId)
|
||
}
|
||
closePanel()
|
||
}
|
||
|
||
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
|
||
onMounted(async () => {
|
||
await store.init()
|
||
await fetchCards()
|
||
|
||
if (window.lucide) window.lucide.createIcons()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Специфичные стили для архива (вертикальный скролл) */
|
||
.main {
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Мобильный архив — с padding */
|
||
.main.mobile {
|
||
padding: 0 16px 16px;
|
||
}
|
||
|
||
/* Список архивных карточек */
|
||
.archive-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-width: 1200px;
|
||
min-height: 200px;
|
||
}
|
||
|
||
/* Пустое состояние */
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 80px 20px;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.empty-state i {
|
||
width: 48px;
|
||
height: 48px;
|
||
opacity: 0.4;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.empty-state p {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.empty-state span {
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* ========== MOBILE: Архив ========== */
|
||
.archive-list.mobile {
|
||
/* 60px header + 40px title + 64px nav + safe-area */
|
||
max-height: calc(100dvh - 60px - 40px - 64px - var(--safe-area-bottom, 0px));
|
||
max-width: none;
|
||
gap: 12px;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-width: none;
|
||
-ms-overflow-style: none;
|
||
padding-bottom: 16px;
|
||
}
|
||
|
||
.archive-list.mobile::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.empty-state.mobile {
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.empty-state.mobile i {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
|
||
/* Мобильный заголовок над карточками */
|
||
.mobile-archive-header {
|
||
padding: 12px 0 16px 4px;
|
||
}
|
||
|
||
.archive-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.archive-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--orange, #ff9f43);
|
||
}
|
||
|
||
.archive-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.archive-count {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text-muted);
|
||
}
|
||
</style>
|