- Система комментариев к задачам с вложенными ответами - Редактирование и удаление комментариев - Прикрепление файлов к задачам и комментариям (картинки, архивы до 10 МБ) - Система прав проекта: админ проекта может удалять чужие комментарии и файлы - Универсальный класс FileUpload для загрузки файлов - Защита загрузки: только автор комментария может добавлять файлы - Каскадное удаление: задача → комментарии → файлы - Автообновление комментариев в реальном времени
206 lines
7.6 KiB
JavaScript
206 lines
7.6 KiB
JavaScript
// Базовый URL API (берётся из внешнего config.js)
|
||
const API_BASE = window.APP_CONFIG?.API_BASE || ''
|
||
|
||
// Формирование полного URL (добавляет домен к относительным путям)
|
||
export const getFullUrl = (url) => {
|
||
if (!url) return ''
|
||
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) {
|
||
return url
|
||
}
|
||
return API_BASE + url
|
||
}
|
||
|
||
// Базовая функция запроса
|
||
const request = async (endpoint, options = {}) => {
|
||
const res = await fetch(`${API_BASE}${endpoint}`, options)
|
||
return res.json()
|
||
}
|
||
|
||
// ==================== AUTH ====================
|
||
export const authApi = {
|
||
login: (username, password) => request('/api/user', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'auth_login', username, password })
|
||
}),
|
||
check: () => request('/api/user', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'check_session' })
|
||
}),
|
||
logout: () => request('/api/user', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'logout' })
|
||
})
|
||
}
|
||
|
||
// ==================== PROJECTS ====================
|
||
export const projectsApi = {
|
||
// active: ID проекта для загрузки данных (опционально)
|
||
getAll: (active = null) => {
|
||
let url = '/api/project'
|
||
if (active) url += `?active=${active}`
|
||
return request(url, { credentials: 'include' })
|
||
},
|
||
// Получение данных проекта (проект + колонки + отделы)
|
||
getData: (id_project) => request('/api/project', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'get_project_data', id_project })
|
||
})
|
||
}
|
||
|
||
// ==================== CARDS ====================
|
||
export const cardsApi = {
|
||
// id_project: ID проекта (обязательный)
|
||
// archive: 0 = неархивные (по умолчанию), 1 = архивные, 'all' = все
|
||
getAll: (id_project, archive = 0) => {
|
||
let url = `/api/task?id_project=${id_project}`
|
||
if (archive !== 0) url += `&archive=${archive}`
|
||
return request(url, { credentials: 'include' })
|
||
},
|
||
updateOrder: (id, column_id, to_index) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'update_order', id, column_id, to_index })
|
||
}),
|
||
create: (data) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'create', ...data })
|
||
}),
|
||
update: (data) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'update', ...data })
|
||
}),
|
||
delete: (id) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', id })
|
||
}),
|
||
setArchive: (id, archive = 1) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'set_archive', id, archive })
|
||
})
|
||
}
|
||
|
||
// ==================== TASK IMAGES ====================
|
||
export const taskImageApi = {
|
||
upload: (task_id, file_data, file_name) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'upload_image', task_id, file_data, file_name })
|
||
}),
|
||
// Принимает строку (один файл) или массив (несколько файлов)
|
||
delete: (task_id, file_names) => request('/api/task', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete_image', task_id, file_names })
|
||
})
|
||
}
|
||
|
||
// ==================== COMMENT IMAGES ====================
|
||
export const commentImageApi = {
|
||
upload: (comment_id, file_data, file_name) => request('/api/comment', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'upload_image', comment_id, file_data, file_name })
|
||
}),
|
||
// Принимает строку (один файл) или массив (несколько файлов)
|
||
delete: (comment_id, file_names) => request('/api/comment', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete_image', comment_id, file_names })
|
||
})
|
||
}
|
||
|
||
// ==================== USERS ====================
|
||
export const usersApi = {
|
||
getAll: () => request('/api/user', { credentials: 'include' })
|
||
}
|
||
|
||
// ==================== SERVER ====================
|
||
export const serverApi = {
|
||
// Получение настроек сервера (timezone и т.д.) — публичный action
|
||
getSettings: () => request('/api/server', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'get_settings' })
|
||
})
|
||
}
|
||
|
||
// Хранилище серверных настроек
|
||
export const serverSettings = {
|
||
timezoneOffset: '+03:00', // дефолт, обновится при загрузке
|
||
|
||
// Инициализация — вызвать один раз при старте приложения
|
||
async init() {
|
||
try {
|
||
const result = await serverApi.getSettings()
|
||
if (result.success) {
|
||
this.timezoneOffset = result.data.timezone_offset
|
||
}
|
||
} catch (e) {
|
||
console.warn('Не удалось получить настройки сервера:', e)
|
||
}
|
||
},
|
||
|
||
// Парсинг даты с сервера с учётом таймзоны
|
||
parseDate(dateStr) {
|
||
if (!dateStr) return null
|
||
// Добавляем таймзону сервера для корректного парсинга
|
||
const normalized = dateStr.replace(' ', 'T')
|
||
// Если уже есть таймзона — не добавляем
|
||
if (normalized.includes('+') || normalized.includes('Z')) {
|
||
return new Date(normalized)
|
||
}
|
||
return new Date(normalized + this.timezoneOffset)
|
||
}
|
||
}
|
||
|
||
// ==================== COMMENTS ====================
|
||
export const commentsApi = {
|
||
// Получение комментариев задачи
|
||
getByTask: (id_task) => request(`/api/comment?id_task=${id_task}`, { credentials: 'include' }),
|
||
|
||
// Создание комментария (id_answer — опционально, для ответа на комментарий)
|
||
create: (id_task, text, id_answer = null) => request('/api/comment', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'create', id_task, text, id_answer })
|
||
}),
|
||
|
||
// Обновление комментария
|
||
update: (id, text) => request('/api/comment', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'update', id, text })
|
||
}),
|
||
|
||
// Удаление комментария
|
||
delete: (id) => request('/api/comment', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'delete', id })
|
||
})
|
||
} |