1
0

Большое обновление

1. Создание личных проектов
2. Управление командой
3. Приглашение участников
4. Уведомления

и многое другое...
This commit is contained in:
2026-01-18 20:17:02 +07:00
parent 250eac70a7
commit 190b4d0a5e
51 changed files with 6179 additions and 426 deletions

View File

@@ -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,