// Базовый 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 }) }) }