1
0
Files
TaskBoard/front_vue/src/stores/projects.js
Falknat 6928687982 Фиксы
Доп правки
2026-01-18 21:02:23 +07:00

731 lines
26 KiB
JavaScript
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.

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { projectsApi, usersApi, cardsApi, projectInviteApi } from '../api'
import { getCachedUser } from '../router'
// ==================== ЛОКАЛЬНЫЙ ПОРЯДОК ПРОЕКТОВ ====================
const PROJECTS_ORDER_KEY = 'projectsOrder'
// Получить сохранённый порядок из localStorage
const getLocalProjectsOrder = () => {
try {
const saved = localStorage.getItem(PROJECTS_ORDER_KEY)
return saved ? JSON.parse(saved) : []
} catch {
return []
}
}
// Сохранить порядок в localStorage
const saveLocalProjectsOrder = (ids) => {
localStorage.setItem(PROJECTS_ORDER_KEY, JSON.stringify(ids))
}
// Применить локальный порядок к массиву проектов
const applyLocalOrder = (projectsArray) => {
const savedOrder = getLocalProjectsOrder()
if (!savedOrder.length) return projectsArray
// Создаём Map для быстрого доступа
const projectsMap = new Map(projectsArray.map(p => [p.id, p]))
const result = []
// Сначала добавляем проекты в сохранённом порядке
for (const id of savedOrder) {
if (projectsMap.has(id)) {
result.push(projectsMap.get(id))
projectsMap.delete(id)
}
}
// Затем добавляем новые проекты (которых нет в сохранённом порядке)
for (const project of projectsMap.values()) {
result.push(project)
}
return result
}
export const useProjectsStore = defineStore('projects', () => {
// ==================== СОСТОЯНИЕ ====================
const projects = ref([])
const departments = ref([])
const labels = ref([])
const columns = ref([])
const users = ref([])
const cards = ref([]) // Активные карточки текущего проекта
const archivedCards = ref([]) // Архивные карточки текущего проекта
const cardsLoading = ref(false) // Загрузка карточек
const loading = ref(false)
const initialized = ref(false)
const currentUser = ref(null) // Текущий авторизованный пользователь
const pendingInvitesCount = ref(0) // Количество pending-приглашений
// Текущий проект (из localStorage)
const savedProjectId = localStorage.getItem('currentProjectId')
const savedProjectName = localStorage.getItem('currentProjectName')
const currentProjectId = ref(savedProjectId ? parseInt(savedProjectId) : null)
// ==================== ГЕТТЕРЫ ====================
// Текущий проект (объект)
const currentProject = computed(() =>
projects.value.find(p => p.id === currentProjectId.value) ||
(savedProjectName && currentProjectId.value ? { id: currentProjectId.value, name: savedProjectName } : null)
)
// ID колонки "Готово" текущего проекта
const doneColumnId = computed(() => {
const project = projects.value.find(p => p.id === currentProjectId.value)
return project ? Number(project.id_ready) : null
})
// ID текущего пользователя (приводим к числу для корректного сравнения)
const currentUserId = computed(() => currentUser.value?.id ? Number(currentUser.value.id) : null)
// Имя текущего пользователя
const currentUserName = computed(() => currentUser.value?.name || '')
// Аватар текущего пользователя
const currentUserAvatar = computed(() => currentUser.value?.avatar_url || '')
// Является ли текущий пользователь админом проекта
const isProjectAdmin = computed(() => {
const project = projects.value.find(p => p.id === currentProjectId.value)
return project?.is_admin === true
})
// Права текущего пользователя в проекте (загружаются с users)
const currentUserPermissions = computed(() => {
if (!currentUserId.value) return {}
const member = users.value.find(u => u.id_user === currentUserId.value)
return member?.permissions || {}
})
// Проверка конкретного права (админ имеет все права)
const can = (permission) => {
if (isProjectAdmin.value) return true
return currentUserPermissions.value[permission] === true
}
// Проверка права на редактирование задачи
const canEditTask = (task) => {
if (!currentUserId.value) return false
if (isProjectAdmin.value) return true
if (can('edit_task')) return true
const creatorId = task?.create_id_account ? Number(task.create_id_account) : null
// accountId (mapped) или id_account (raw)
const assigneeId = task?.accountId ? Number(task.accountId) : (task?.id_account ? Number(task.id_account) : null)
// Создатель + право создания → может редактировать
if (creatorId === currentUserId.value && can('create_task')) return true
// Назначена на себя + право edit_own_task_only
if (assigneeId === currentUserId.value && can('edit_own_task_only')) return true
return false
}
// Проверка права на перемещение задачи
const canMoveTask = (task) => {
if (!currentUserId.value) return false
if (isProjectAdmin.value) return true
if (can('move_task')) return true
const creatorId = task?.create_id_account ? Number(task.create_id_account) : null
// accountId (mapped) или id_account (raw)
const assigneeId = task?.accountId ? Number(task.accountId) : (task?.id_account ? Number(task.id_account) : null)
// Создатель + право создания → может перемещать
if (creatorId === currentUserId.value && can('create_task')) return true
// Назначена на себя + право move_own_task_only
if (assigneeId === currentUserId.value && can('move_own_task_only')) return true
return false
}
// Проверка права на создание комментария в задаче
const canCreateComment = (task) => {
if (!currentUserId.value) return false
if (isProjectAdmin.value) return true
if (can('create_comment')) return true
const creatorId = task?.create_id_account ? Number(task.create_id_account) : null
// accountId (mapped) или id_account (raw)
const assigneeId = task?.accountId ? Number(task.accountId) : (task?.id_account ? Number(task.id_account) : null)
// Создатель + право создания → может комментировать
if (creatorId === currentUserId.value && can('create_task')) return true
// Назначена на себя + право create_comment_own_task_only
if (assigneeId === currentUserId.value && can('create_comment_own_task_only')) return true
return false
}
// ==================== ДЕЙСТВИЯ ====================
// Инициализация (загрузка проектов + данных активного)
const init = async () => {
// Если уже инициализировано И есть данные — пропускаем
if (initialized.value && projects.value.length > 0) return
loading.value = true
try {
// Загружаем проекты и данные активного одним запросом
const result = await projectsApi.getAll(currentProjectId.value || undefined)
if (result.success) {
if (result.data.projects) {
// Применяем локальный порядок сортировки
projects.value = applyLocalOrder(result.data.projects)
// Применяем данные активного проекта
if (result.data.active) {
columns.value = result.data.active.columns
departments.value = result.data.active.departments
labels.value = result.data.active.labels
}
} else {
// Применяем локальный порядок сортировки
projects.value = applyLocalOrder(result.data)
}
// Если нет проектов — очищаем currentProjectId
if (projects.value.length === 0) {
currentProjectId.value = null
localStorage.removeItem('currentProjectId')
localStorage.removeItem('currentProjectName')
}
// Если нет выбранного проекта — выбираем первый
else if (!currentProjectId.value || !projects.value.find(p => p.id === currentProjectId.value)) {
if (projects.value.length > 0) {
await selectProject(projects.value[0].id, true) // Загружаем данные проекта
}
} else if (!columns.value.length) {
// Есть проект но нет данных — загружаем
await fetchProjectData()
} else {
// Обновляем название в localStorage
const project = projects.value.find(p => p.id === currentProjectId.value)
if (project) localStorage.setItem('currentProjectName', project.name)
}
}
// Получаем текущего пользователя из кэша роутера
if (!currentUser.value) {
const cachedUser = getCachedUser()
if (cachedUser) {
currentUser.value = cachedUser
}
}
// Загружаем участников проекта (users теперь привязаны к проекту)
if (currentProjectId.value) {
await fetchUsers()
// Загружаем количество pending-приглашений (только если есть проекты)
// Если проектов нет — NoProjectsPage сама загрузит полные данные с count
await fetchPendingInvitesCount()
}
initialized.value = true
} catch (error) {
console.error('Ошибка инициализации:', error)
} finally {
loading.value = false
}
}
// Принудительная перезагрузка проектов (для обновления после принятия приглашения/выхода из проекта)
const refreshProjects = async () => {
loading.value = true
try {
const result = await projectsApi.getAll()
if (result.success) {
if (result.data.projects) {
// Применяем локальный порядок сортировки
projects.value = applyLocalOrder(result.data.projects)
} else {
// Применяем локальный порядок сортировки
projects.value = applyLocalOrder(result.data)
}
// Если нет проектов — очищаем currentProjectId
if (projects.value.length === 0) {
currentProjectId.value = null
columns.value = []
departments.value = []
labels.value = []
users.value = []
cards.value = []
archivedCards.value = []
localStorage.removeItem('currentProjectId')
localStorage.removeItem('currentProjectName')
}
// Проверяем, есть ли текущий проект в списке
else if (currentProjectId.value && !projects.value.find(p => p.id === currentProjectId.value)) {
// Текущий проект больше недоступен — переключаемся на первый
await selectProject(projects.value[0].id, true)
}
}
// Устанавливаем текущего пользователя из кэша (если ещё не установлен)
if (!currentUser.value) {
const cachedUser = getCachedUser()
if (cachedUser) {
currentUser.value = cachedUser
}
}
// Обновляем количество pending-приглашений
await fetchPendingInvitesCount()
initialized.value = true
} catch (error) {
console.error('Ошибка перезагрузки проектов:', error)
} finally {
loading.value = false
}
}
// Выбор проекта
const selectProject = async (projectId, fetchData = true) => {
currentProjectId.value = projectId
localStorage.setItem('currentProjectId', projectId.toString())
// Сохраняем название
const project = projects.value.find(p => p.id === projectId)
if (project) localStorage.setItem('currentProjectName', project.name)
// Загружаем данные проекта
if (fetchData) {
await fetchProjectData()
}
}
// Загрузка данных текущего проекта
const fetchProjectData = async () => {
if (!currentProjectId.value) return
try {
const projectData = await projectsApi.getData(currentProjectId.value)
if (projectData.success) {
columns.value = projectData.data.columns
departments.value = projectData.data.departments
labels.value = projectData.data.labels
// Обновляем is_admin в списке проектов
const project = projects.value.find(p => p.id === currentProjectId.value)
if (project && projectData.data.project?.is_admin === true) {
project.is_admin = true
}
}
// Загружаем участников проекта
await fetchUsers()
} catch (error) {
console.error('Ошибка загрузки данных проекта:', error)
}
}
// Загрузка участников проекта
const fetchUsers = async () => {
if (!currentProjectId.value) return
try {
const usersData = await usersApi.getAll(currentProjectId.value)
if (usersData.success) {
users.value = usersData.data
}
} catch (error) {
console.error('Ошибка загрузки участников:', error)
}
}
// Загрузка количества pending-приглашений
const fetchPendingInvitesCount = async () => {
try {
const result = await projectInviteApi.getCount()
if (result.success) {
pendingInvitesCount.value = result.count
}
} catch (error) {
console.error('Ошибка загрузки приглашений:', error)
}
}
// ==================== КАРТОЧКИ ====================
// Загрузка активных карточек (silent = тихое обновление без loading)
const fetchCards = async (silent = false) => {
if (!currentProjectId.value) {
cardsLoading.value = false
return
}
if (!silent) cardsLoading.value = true
try {
const result = await cardsApi.getAll(currentProjectId.value)
if (result.success) cards.value = result.data
} finally {
if (!silent) cardsLoading.value = false
}
}
// Загрузка архивных карточек
const fetchArchivedCards = async () => {
if (!currentProjectId.value) return
cardsLoading.value = true
try {
const result = await cardsApi.getAll(currentProjectId.value, 1) // archive = 1
if (result.success) {
archivedCards.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 {
cardsLoading.value = false
}
}
// Очистка карточек при смене проекта
const clearCards = () => {
cards.value = []
archivedCards.value = []
}
// Сброс при выходе
const reset = () => {
projects.value = []
departments.value = []
labels.value = []
columns.value = []
users.value = []
cards.value = []
archivedCards.value = []
currentProjectId.value = null
currentUser.value = null
pendingInvitesCount.value = 0
initialized.value = false
localStorage.removeItem('currentProjectId')
localStorage.removeItem('currentProjectName')
localStorage.removeItem(PROJECTS_ORDER_KEY)
}
// ==================== CRUD ПРОЕКТОВ ====================
// Создание проекта
const createProject = async (name) => {
const result = await projectsApi.create(name)
if (result.success) {
// Добавляем проект в список
const newProject = {
id: result.id,
name,
id_ready: result.id_ready,
is_admin: true // Создатель = админ
}
projects.value.push(newProject)
// Добавляем в локальный порядок
const currentOrder = getLocalProjectsOrder()
currentOrder.push(result.id)
saveLocalProjectsOrder(currentOrder)
// Переключаемся на новый проект
await selectProject(result.id)
}
return result
}
// Обновление проекта
const updateProject = async (id, name) => {
const result = await projectsApi.update(id, name)
if (result.success) {
const project = projects.value.find(p => p.id === id)
if (project) {
project.name = name
// Обновляем localStorage если это текущий проект
if (id === currentProjectId.value) {
localStorage.setItem('currentProjectName', name)
}
}
}
return result
}
// Удаление проекта
const deleteProject = async (id) => {
const result = await projectsApi.delete(id)
if (result.success) {
const index = projects.value.findIndex(p => p.id === id)
if (index !== -1) {
projects.value.splice(index, 1)
}
// Удаляем из локального порядка
const currentOrder = getLocalProjectsOrder()
const filteredOrder = currentOrder.filter(pid => pid !== id)
saveLocalProjectsOrder(filteredOrder)
// Если удалили текущий проект — переключаемся на первый
if (id === currentProjectId.value) {
if (projects.value.length > 0) {
await selectProject(projects.value[0].id)
} else {
currentProjectId.value = null
columns.value = []
departments.value = []
cards.value = []
localStorage.removeItem('currentProjectId')
localStorage.removeItem('currentProjectName')
}
}
}
return result
}
// Обновление порядка проектов (локально, без отправки на сервер)
const reorderProjects = (ids) => {
// Применяем новый порядок
const reordered = ids.map((id, index) => {
const project = projects.value.find(p => p.id === id)
return { ...project }
}).filter(Boolean)
projects.value = reordered
// Сохраняем порядок локально
saveLocalProjectsOrder(ids)
}
// ==================== CRUD КОЛОНОК ====================
// Добавление колонки
const addColumn = async (name, color = '#6366f1') => {
if (!currentProjectId.value) return { success: false }
const result = await projectsApi.addColumn(currentProjectId.value, name, color)
if (result.success) {
columns.value.push(result.column)
}
return result
}
// Обновление колонки
const updateColumn = async (id, name, color) => {
const result = await projectsApi.updateColumn(id, name, color)
if (result.success) {
const column = columns.value.find(c => c.id === id)
if (column) {
if (name !== null && name !== undefined) column.name_columns = name
if (color !== null && color !== undefined) column.color = color
}
}
return result
}
// Получение количества задач в колонке
const getColumnTasksCount = async (id) => {
const result = await projectsApi.getColumnTasksCount(id)
return result.success ? result.count : 0
}
// Удаление колонки
const deleteColumn = async (id) => {
const result = await projectsApi.deleteColumn(id)
if (result.success) {
const index = columns.value.findIndex(c => c.id === id)
if (index !== -1) {
columns.value.splice(index, 1)
}
// Удаляем карточки этой колонки из локального состояния
cards.value = cards.value.filter(c => c.column_id !== id)
}
return result
}
// Обновление порядка колонок
const reorderColumns = async (ids) => {
if (!currentProjectId.value) return
// Оптимистичное обновление
const reordered = ids.map((id, index) => {
const column = columns.value.find(c => c.id === id)
return { ...column, id_order: index + 1 }
})
columns.value = reordered
// Отправляем на сервер
await projectsApi.updateColumnsOrder(currentProjectId.value, ids)
}
// Установка финальной колонки
const setReadyColumn = async (columnId) => {
if (!currentProjectId.value) return { success: false }
const result = await projectsApi.setReadyColumn(currentProjectId.value, columnId)
if (result.success) {
// Обновляем id_ready в проекте
const project = projects.value.find(p => p.id === currentProjectId.value)
if (project) {
project.id_ready = columnId
}
}
return result
}
// ==================== CRUD ОТДЕЛОВ ====================
// Добавление отдела
const addDepartment = async (name, color = '#6366f1') => {
if (!currentProjectId.value) return { success: false }
const result = await projectsApi.addDepartment(currentProjectId.value, name, color)
if (result.success) {
departments.value.push(result.department)
}
return result
}
// Обновление отдела
const updateDepartment = async (id, name, color) => {
const result = await projectsApi.updateDepartment(id, name, color)
if (result.success) {
const department = departments.value.find(d => d.id === id)
if (department) {
if (name !== null && name !== undefined) department.name_departments = name
if (color !== null && color !== undefined) department.color = color
}
}
return result
}
// Получение количества задач в отделе
const getDepartmentTasksCount = async (id) => {
const result = await projectsApi.getDepartmentTasksCount(id)
return result.success ? result.count : 0
}
// Удаление отдела
const deleteDepartment = async (id) => {
const result = await projectsApi.deleteDepartment(id)
if (result.success) {
const index = departments.value.findIndex(d => d.id === id)
if (index !== -1) {
departments.value.splice(index, 1)
}
// Обнуляем id_department у карточек этого отдела
cards.value.forEach(card => {
if (card.id_department === id) {
card.id_department = null
}
})
}
return result
}
// Обновление порядка отделов
const reorderDepartments = async (ids) => {
if (!currentProjectId.value) return
// Оптимистичное обновление
const reordered = ids.map((id, index) => {
const department = departments.value.find(d => d.id === id)
return { ...department, order_id: index + 1 }
})
departments.value = reordered
// Отправляем на сервер
await projectsApi.updateDepartmentsOrder(currentProjectId.value, ids)
}
// Добавление отдела в конкретный проект (для создания нового проекта)
const addDepartmentToProject = async (projectId, name, color = '#6366f1') => {
const result = await projectsApi.addDepartment(projectId, name, color)
// Не добавляем в локальный state — при переключении на проект данные загрузятся
return result
}
return {
// Состояние
projects,
departments,
labels,
columns,
users,
cards,
archivedCards,
cardsLoading,
loading,
initialized,
currentProjectId,
currentUser,
pendingInvitesCount,
// Геттеры
currentProject,
doneColumnId,
currentUserId,
currentUserName,
currentUserAvatar,
isProjectAdmin,
currentUserPermissions,
// Проверки прав
can,
canEditTask,
canMoveTask,
canCreateComment,
// Действия
init,
refreshProjects,
selectProject,
fetchProjectData,
fetchUsers,
fetchPendingInvitesCount,
fetchCards,
fetchArchivedCards,
clearCards,
reset,
// CRUD проектов
createProject,
updateProject,
deleteProject,
reorderProjects,
// CRUD колонок
addColumn,
updateColumn,
getColumnTasksCount,
deleteColumn,
reorderColumns,
setReadyColumn,
// CRUD отделов
addDepartment,
addDepartmentToProject,
updateDepartment,
getDepartmentTasksCount,
deleteDepartment,
reorderDepartments
}
})