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 } })