1
0
Files
TaskBoard/front_vue/src/views/MainApp.vue
Falknat 190b4d0a5e Большое обновление
1. Создание личных проектов
2. Управление командой
3. Приглашение участников
4. Уведомления

и многое другое...
2026-01-18 20:17:02 +07:00

346 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<PageLayout>
<!-- Шапка с заголовком, фильтрами и статистикой -->
<Header title="Доска задач">
<!-- Десктоп: фильтры в одну строку -->
<template #filters>
<DepartmentTags
v-model="activeDepartment"
@project-change="onProjectChange"
@create-project="openCreateProjectPanel"
@edit-project="openEditProjectPanel"
/>
</template>
<!-- Мобильный: Проект + Отделы -->
<template #mobile-filters>
<ProjectSelector
@change="onProjectChange"
@edit="openEditProjectPanel"
/>
<!-- Кнопки управления проектами (мобильные) -->
<div class="mobile-project-actions">
<button
v-if="store.isProjectAdmin"
class="mobile-project-btn"
title="Настройки проекта"
@click="openEditProjectPanel(store.currentProject)"
>
<i data-lucide="settings"></i>
</button>
<button
class="mobile-project-btn"
title="Создать проект"
@click="openCreateProjectPanel"
>
<i data-lucide="plus"></i>
</button>
</div>
<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">{{ stats.total }}</span>
<span class="stat-label">задач</span>
</div>
<div class="stat-divider"></div>
<div class="stat">
<span class="stat-value">{{ stats.inProgress }}</span>
<span class="stat-label">в работе</span>
</div>
<div class="stat-divider"></div>
<div class="stat">
<span class="stat-value">{{ stats.done }}</span>
<span class="stat-label">готово</span>
</div>
</div>
</template>
</Header>
<!-- Доска с колонками и карточками -->
<main class="main" :class="{ mobile: isMobile }">
<Loader v-if="loading" />
<Board
v-else
ref="boardRef"
:active-department="activeDepartment"
:cards="cards"
@stats-updated="stats = $event"
@open-task="openTaskPanel"
@create-task="openNewTaskPanel"
@cards-moved="onCardsMoved"
/>
</main>
<!-- Модальные окна -->
<template #modals>
<TaskPanel
:show="panelOpen"
:card="editingCard"
:column-id="editingColumnId"
:on-save="handleSaveTask"
:on-archive="handleArchiveTask"
@close="closePanel"
@delete="handleDeleteTask"
/>
<ProjectPanel
:show="projectPanelOpen"
:project="editingProject"
@close="closeProjectPanel"
@saved="onProjectSaved"
/>
</template>
</PageLayout>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import PageLayout from '../components/PageLayout.vue'
import Header from '../components/Header.vue'
import Board from '../components/Board.vue'
import TaskPanel from '../components/TaskPanel'
import ProjectPanel from '../components/ProjectPanel.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)
// Загрузка карточек текущего проекта (silent = тихое обновление без Loader)
const fetchCards = async (silent = false) => {
if (!store.currentProjectId) {
loading.value = false
return
}
if (!silent) {
loading.value = true
}
try {
const result = await cardsApi.getAll(store.currentProjectId)
if (result.success) cards.value = result.data
} finally {
if (!silent) {
loading.value = false
}
}
}
// При смене проекта — перезагружаем карточки
const onProjectChange = async () => {
resetFilter()
loading.value = true
await fetchCards()
}
// После перемещения карточки — тихо обновляем данные с сервера
const onCardsMoved = async () => {
await fetchCards(true)
}
// ==================== СТАТИСТИКА ====================
const stats = ref({ total: 0, inProgress: 0, done: 0 })
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
const boardRef = ref(null)
const panelOpen = ref(false)
const editingCard = ref(null)
const editingColumnId = ref(null)
const openTaskPanel = ({ card, columnId }) => {
editingCard.value = card
editingColumnId.value = columnId
panelOpen.value = true
}
const openNewTaskPanel = (columnId) => {
editingCard.value = null
editingColumnId.value = columnId
panelOpen.value = true
}
const closePanel = () => {
panelOpen.value = false
editingCard.value = null
editingColumnId.value = null
}
const handleSaveTask = async (taskData) => {
if (!taskData.id && store.currentProjectId) {
taskData.id_project = store.currentProjectId
}
await boardRef.value?.saveTask(taskData, editingColumnId.value)
closePanel()
}
const handleDeleteTask = async (cardId) => {
await boardRef.value?.deleteTask(cardId, editingColumnId.value)
closePanel()
}
const handleArchiveTask = async (cardId) => {
await boardRef.value?.archiveTask(cardId)
closePanel()
}
// ==================== ПАНЕЛЬ ПРОЕКТА ====================
const projectPanelOpen = ref(false)
const editingProject = ref(null)
const openCreateProjectPanel = () => {
editingProject.value = null
projectPanelOpen.value = true
}
const openEditProjectPanel = (project) => {
editingProject.value = project
projectPanelOpen.value = true
}
const closeProjectPanel = () => {
projectPanelOpen.value = false
editingProject.value = null
}
const onProjectSaved = async (projectId) => {
// Перезагружаем данные если изменился текущий проект
if (projectId === store.currentProjectId) {
await store.fetchProjectData()
await fetchCards()
}
}
// ==================== АВТООБНОВЛЕНИЕ (POLLING) ====================
const CARDS_REFRESH_INTERVAL = (window.APP_CONFIG?.REFRESH_INTERVALS?.cards ?? 30) * 1000
const INVITES_REFRESH_INTERVAL = (window.APP_CONFIG?.REFRESH_INTERVALS?.invites ?? 30) * 1000
let pollTimer = null
let invitesPollTimer = null
const startPolling = () => {
// Polling карточек
if (pollTimer) clearInterval(pollTimer)
pollTimer = setInterval(async () => {
// Не обновляем когда открыта модалка — это может прерывать клики
if (panelOpen.value || projectPanelOpen.value) return
console.log('[AutoRefresh] Обновление данных...')
await fetchCards(true) // silent = true, без Loader
}, CARDS_REFRESH_INTERVAL)
// Polling приглашений (для бейджа)
if (invitesPollTimer) clearInterval(invitesPollTimer)
invitesPollTimer = setInterval(async () => {
await store.fetchPendingInvitesCount()
}, INVITES_REFRESH_INTERVAL)
}
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
if (invitesPollTimer) {
clearInterval(invitesPollTimer)
invitesPollTimer = null
}
}
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
onMounted(async () => {
// Store уже мог быть инициализирован при логине (prefetch) или в роутере
await store.init()
// Проверяем предзагруженные карточки
const prefetchedCards = sessionStorage.getItem('prefetchedCards')
if (prefetchedCards) {
try {
cards.value = JSON.parse(prefetchedCards)
sessionStorage.removeItem('prefetchedCards')
loading.value = false
} catch (e) {
await fetchCards()
}
} else {
await fetchCards()
}
startPolling()
if (window.lucide) window.lucide.createIcons()
})
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
/* Специфичные стили для доски (горизонтальный скролл) */
.main {
overflow-x: auto;
scroll-behavior: smooth;
}
/* Мобильная доска — без padding, Board сам управляет layout */
.main.mobile {
padding: 0;
}
/* Мобильные кнопки управления проектами */
.mobile-project-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.mobile-project-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: none;
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
}
.mobile-project-btn:active {
color: var(--accent);
background: rgba(255, 255, 255, 0.06);
}
.mobile-project-btn i {
width: 18px;
height: 18px;
}
</style>