1. Создание личных проектов 2. Управление командой 3. Приглашение участников 4. Уведомления и многое другое...
652 lines
23 KiB
JavaScript
652 lines
23 KiB
JavaScript
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
|
||
}
|
||
|
||
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
|
||
}
|
||
})
|