438 lines
11 KiB
Vue
438 lines
11 KiB
Vue
<template>
|
||
<div class="app">
|
||
<!-- Боковая панель навигации -->
|
||
<Sidebar />
|
||
|
||
<!-- Основной контент -->
|
||
<div class="main-wrapper">
|
||
<!-- Шапка с заголовком и фильтрами -->
|
||
<Header title="Архив задач">
|
||
<template #filters>
|
||
<div class="filters">
|
||
<!-- Выбор проекта -->
|
||
<ProjectSelector @change="onProjectChange" />
|
||
|
||
<div class="filter-divider"></div>
|
||
|
||
<!-- Фильтр по отделам -->
|
||
<button
|
||
class="filter-tag"
|
||
:class="{ active: activeDepartment === null }"
|
||
@click="activeDepartment = null"
|
||
>
|
||
Все
|
||
</button>
|
||
<button
|
||
v-for="dept in store.departments"
|
||
:key="dept.id"
|
||
class="filter-tag"
|
||
:class="{ active: activeDepartment === dept.id }"
|
||
@click="activeDepartment = activeDepartment === dept.id ? null : dept.id"
|
||
>
|
||
{{ dept.name_departments }}
|
||
</button>
|
||
</div>
|
||
</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">
|
||
<div class="archive-list">
|
||
<ArchiveCard
|
||
v-for="card in filteredCards"
|
||
:key="card.id"
|
||
:card="card"
|
||
:departments="store.departments"
|
||
:labels="store.labels"
|
||
@click="openTaskPanel(card)"
|
||
@restore="handleRestore"
|
||
@delete="confirmDelete"
|
||
/>
|
||
|
||
<!-- Пустое состояние -->
|
||
<div v-if="filteredCards.length === 0 && !loading" class="empty-state">
|
||
<i data-lucide="archive-x"></i>
|
||
<p>Архив пуст</p>
|
||
<span>Архивированные задачи появятся здесь</span>
|
||
</div>
|
||
|
||
<!-- Загрузка -->
|
||
<div v-if="loading" class="loading-state">
|
||
<i data-lucide="loader-2" class="spin"></i>
|
||
<span>Загрузка...</span>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Панель редактирования задачи -->
|
||
<TaskPanel
|
||
:show="panelOpen"
|
||
:card="editingCard"
|
||
:column-id="null"
|
||
:is-archived="true"
|
||
:departments="store.departments"
|
||
:labels="store.labels"
|
||
:users="store.users"
|
||
@close="closePanel"
|
||
@save="handleSaveTask"
|
||
@delete="handleDeleteTask"
|
||
@restore="handleRestoreFromPanel"
|
||
/>
|
||
|
||
<!-- Диалог подтверждения удаления -->
|
||
<ConfirmDialog
|
||
:show="confirmDialogOpen"
|
||
title="Удалить задачу?"
|
||
message="Задача будет удалена безвозвратно. Это действие нельзя отменить."
|
||
confirm-text="Удалить"
|
||
@confirm="handleConfirmDelete"
|
||
@cancel="confirmDialogOpen = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted } from 'vue'
|
||
import Sidebar from '../components/Sidebar.vue'
|
||
import Header from '../components/Header.vue'
|
||
import ArchiveCard from '../components/ArchiveCard.vue'
|
||
import TaskPanel from '../components/TaskPanel.vue'
|
||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||
import ProjectSelector from '../components/ProjectSelector.vue'
|
||
import { useProjectsStore } from '../stores/projects'
|
||
import { cardsApi } from '../api'
|
||
|
||
// ==================== STORE ====================
|
||
const store = useProjectsStore()
|
||
|
||
// ==================== КАРТОЧКИ ====================
|
||
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,
|
||
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 () => {
|
||
activeDepartment.value = null
|
||
await fetchCards()
|
||
}
|
||
|
||
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
|
||
const savedDepartment = localStorage.getItem('activeDepartment')
|
||
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
|
||
|
||
watch(activeDepartment, (newVal) => {
|
||
if (newVal === null) {
|
||
localStorage.removeItem('activeDepartment')
|
||
} else {
|
||
localStorage.setItem('activeDepartment', newVal.toString())
|
||
}
|
||
})
|
||
|
||
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
|
||
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 handleConfirmDelete = async () => {
|
||
if (cardToDelete.value) {
|
||
const result = await cardsApi.delete(cardToDelete.value)
|
||
if (result.success) {
|
||
cards.value = cards.value.filter(c => c.id !== cardToDelete.value)
|
||
}
|
||
}
|
||
confirmDialogOpen.value = false
|
||
cardToDelete.value = null
|
||
}
|
||
|
||
// ==================== ВОССТАНОВЛЕНИЕ ====================
|
||
const handleRestore = async (cardId) => {
|
||
const result = await cardsApi.setArchive(cardId, 0)
|
||
if (result.success) {
|
||
cards.value = cards.value.filter(c => c.id !== cardId)
|
||
}
|
||
}
|
||
|
||
const handleRestoreFromPanel = async (cardId) => {
|
||
await handleRestore(cardId)
|
||
closePanel()
|
||
}
|
||
|
||
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
|
||
onMounted(async () => {
|
||
await store.init()
|
||
await fetchCards()
|
||
|
||
if (window.lucide) window.lucide.createIcons()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Контейнер приложения */
|
||
.app {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* Основная область контента */
|
||
.main-wrapper {
|
||
flex: 1;
|
||
margin-left: 64px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg-main);
|
||
}
|
||
|
||
/* Контейнер фильтров */
|
||
.filters {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* Разделитель между проектом и отделами */
|
||
.filter-divider {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
margin: 0 8px;
|
||
}
|
||
|
||
/* Кнопка фильтра */
|
||
.filter-tag {
|
||
padding: 6px 12px;
|
||
background: var(--bg-card);
|
||
border: none;
|
||
border-radius: 6px;
|
||
color: var(--text-secondary);
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.3px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.filter-tag:hover {
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* Активный фильтр */
|
||
.filter-tag.active {
|
||
background: var(--accent);
|
||
color: #000;
|
||
}
|
||
|
||
/* Блок статистики */
|
||
.header-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 10px 20px;
|
||
background: var(--bg-card);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.stat {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 5px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* Основная область */
|
||
.main {
|
||
flex: 1;
|
||
padding: 0 36px 36px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Список архивных карточек */
|
||
.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;
|
||
}
|
||
|
||
/* Загрузка */
|
||
.loading-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding: 60px 20px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.loading-state i {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.spin {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|