Большое обновление
1. Создание личных проектов 2. Управление командой 3. Приглашение участников 4. Уведомления и многое другое...
This commit is contained in:
@@ -1,8 +1,51 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { projectsApi, usersApi, cardsApi } from '../api'
|
||||
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([])
|
||||
@@ -16,6 +59,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
const loading = ref(false)
|
||||
const initialized = ref(false)
|
||||
const currentUser = ref(null) // Текущий авторизованный пользователь
|
||||
const pendingInvitesCount = ref(0) // Количество pending-приглашений
|
||||
|
||||
// Текущий проект (из localStorage)
|
||||
const savedProjectId = localStorage.getItem('currentProjectId')
|
||||
@@ -35,8 +79,8 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
return project ? Number(project.id_ready) : null
|
||||
})
|
||||
|
||||
// ID текущего пользователя
|
||||
const currentUserId = computed(() => currentUser.value?.id || null)
|
||||
// ID текущего пользователя (приводим к числу для корректного сравнения)
|
||||
const currentUserId = computed(() => currentUser.value?.id ? Number(currentUser.value.id) : null)
|
||||
|
||||
// Имя текущего пользователя
|
||||
const currentUserName = computed(() => currentUser.value?.name || '')
|
||||
@@ -45,12 +89,81 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
const currentUserAvatar = computed(() => currentUser.value?.avatar_url || '')
|
||||
|
||||
// Является ли текущий пользователь админом проекта
|
||||
// Сервер возвращает id_admin: true только если текущий пользователь — админ
|
||||
const isProjectAdmin = computed(() => {
|
||||
const project = projects.value.find(p => p.id === currentProjectId.value)
|
||||
return project?.id_admin === true
|
||||
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 () => {
|
||||
@@ -64,7 +177,8 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
|
||||
if (result.success) {
|
||||
if (result.data.projects) {
|
||||
projects.value = result.data.projects
|
||||
// Применяем локальный порядок сортировки
|
||||
projects.value = applyLocalOrder(result.data.projects)
|
||||
|
||||
// Применяем данные активного проекта
|
||||
if (result.data.active) {
|
||||
@@ -73,11 +187,18 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
labels.value = result.data.active.labels
|
||||
}
|
||||
} else {
|
||||
projects.value = result.data
|
||||
// Применяем локальный порядок сортировки
|
||||
projects.value = applyLocalOrder(result.data)
|
||||
}
|
||||
|
||||
// Если нет проектов — очищаем currentProjectId
|
||||
if (projects.value.length === 0) {
|
||||
currentProjectId.value = null
|
||||
localStorage.removeItem('currentProjectId')
|
||||
localStorage.removeItem('currentProjectName')
|
||||
}
|
||||
// Если нет выбранного проекта — выбираем первый
|
||||
if (!currentProjectId.value || !projects.value.find(p => p.id === currentProjectId.value)) {
|
||||
else if (!currentProjectId.value || !projects.value.find(p => p.id === currentProjectId.value)) {
|
||||
if (projects.value.length > 0) {
|
||||
await selectProject(projects.value[0].id, true) // Загружаем данные проекта
|
||||
}
|
||||
@@ -91,20 +212,22 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем пользователей
|
||||
const usersData = await usersApi.getAll()
|
||||
if (usersData.success) users.value = usersData.data
|
||||
|
||||
// Получаем текущего пользователя из кэша роутера (без повторного запроса)
|
||||
// Получаем текущего пользователя из кэша роутера
|
||||
if (!currentUser.value) {
|
||||
const cachedUser = getCachedUser()
|
||||
if (cachedUser) {
|
||||
// Находим полные данные пользователя (с id) из списка users
|
||||
const fullUser = users.value.find(u => u.username === cachedUser.username)
|
||||
currentUser.value = fullUser || cachedUser
|
||||
currentUser.value = cachedUser
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем участников проекта (users теперь привязаны к проекту)
|
||||
if (currentProjectId.value) {
|
||||
await fetchUsers()
|
||||
// Загружаем количество pending-приглашений (только если есть проекты)
|
||||
// Если проектов нет — NoProjectsPage сама загрузит полные данные с count
|
||||
await fetchPendingInvitesCount()
|
||||
}
|
||||
|
||||
initialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации:', error)
|
||||
@@ -113,6 +236,60 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Принудительная перезагрузка проектов (для обновления после принятия приглашения/выхода из проекта)
|
||||
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
|
||||
@@ -140,17 +317,46 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
departments.value = projectData.data.departments
|
||||
labels.value = projectData.data.labels
|
||||
|
||||
// Обновляем id_admin в списке проектов (сервер возвращает true если текущий пользователь админ)
|
||||
// Обновляем is_admin в списке проектов
|
||||
const project = projects.value.find(p => p.id === currentProjectId.value)
|
||||
if (project && projectData.data.project?.id_admin === true) {
|
||||
project.id_admin = true
|
||||
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) => {
|
||||
@@ -221,9 +427,11 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
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 ПРОЕКТОВ ====================
|
||||
@@ -233,13 +441,19 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
const result = await projectsApi.create(name)
|
||||
if (result.success) {
|
||||
// Добавляем проект в список
|
||||
projects.value.push({
|
||||
const newProject = {
|
||||
id: result.id,
|
||||
name,
|
||||
id_order: projects.value.length + 1,
|
||||
id_ready: result.id_ready,
|
||||
id_admin: true // Создатель = админ
|
||||
})
|
||||
is_admin: true // Создатель = админ
|
||||
}
|
||||
projects.value.push(newProject)
|
||||
|
||||
// Добавляем в локальный порядок
|
||||
const currentOrder = getLocalProjectsOrder()
|
||||
currentOrder.push(result.id)
|
||||
saveLocalProjectsOrder(currentOrder)
|
||||
|
||||
// Переключаемся на новый проект
|
||||
await selectProject(result.id)
|
||||
}
|
||||
@@ -270,6 +484,12 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
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) {
|
||||
@@ -287,17 +507,17 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
return result
|
||||
}
|
||||
|
||||
// Обновление порядка проектов
|
||||
const reorderProjects = async (ids) => {
|
||||
// Оптимистичное обновление
|
||||
// Обновление порядка проектов (локально, без отправки на сервер)
|
||||
const reorderProjects = (ids) => {
|
||||
// Применяем новый порядок
|
||||
const reordered = ids.map((id, index) => {
|
||||
const project = projects.value.find(p => p.id === id)
|
||||
return { ...project, id_order: index + 1 }
|
||||
})
|
||||
return { ...project }
|
||||
}).filter(Boolean)
|
||||
projects.value = reordered
|
||||
|
||||
// Отправляем на сервер
|
||||
await projectsApi.updateOrder(ids)
|
||||
// Сохраняем порядок локально
|
||||
saveLocalProjectsOrder(ids)
|
||||
}
|
||||
|
||||
// ==================== CRUD КОЛОНОК ====================
|
||||
@@ -390,6 +610,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
initialized,
|
||||
currentProjectId,
|
||||
currentUser,
|
||||
pendingInvitesCount,
|
||||
// Геттеры
|
||||
currentProject,
|
||||
doneColumnId,
|
||||
@@ -397,10 +618,19 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
currentUserName,
|
||||
currentUserAvatar,
|
||||
isProjectAdmin,
|
||||
currentUserPermissions,
|
||||
// Проверки прав
|
||||
can,
|
||||
canEditTask,
|
||||
canMoveTask,
|
||||
canCreateComment,
|
||||
// Действия
|
||||
init,
|
||||
refreshProjects,
|
||||
selectProject,
|
||||
fetchProjectData,
|
||||
fetchUsers,
|
||||
fetchPendingInvitesCount,
|
||||
fetchCards,
|
||||
fetchArchivedCards,
|
||||
clearCards,
|
||||
|
||||
Reference in New Issue
Block a user