Files
minicrm/public/kanban.js
2026-01-26 17:44:28 +05:00

300 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// kanban.js - Канбан-доска
let kanbanTasks = [];
let kanbanDays = 14;
let currentDraggedTask = null;
function showKanbanSection() {
showSection('kanban');
loadKanbanTasks();
}
async function loadKanbanTasks() {
try {
const daysSelect = document.getElementById('kanban-days');
const filterSelect = document.getElementById('kanban-filter');
// Если есть выбор в интерфейсе - используем его, иначе - значение по умолчанию
if (daysSelect) {
kanbanDays = parseInt(daysSelect.value) || 14;
} else {
kanbanDays = 14;
}
let filter = 'all';
if (filterSelect) {
filter = filterSelect.value;
}
const response = await fetch(`/api/kanban-tasks?days=${kanbanDays}&filter=${filter}`);
if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`);
}
const data = await response.json();
kanbanTasks = data.tasks || [];
renderKanban(data.filter);
} catch (error) {
console.error('Ошибка загрузки задач для Канбана:', error);
document.getElementById('kanban-board').innerHTML = `
<div class="error-message">
❌ Ошибка загрузки Канбана: ${error.message}
<button onclick="loadKanbanTasks()" class="retry-btn">Повторить</button>
</div>
`;
}
}
function renderKanban(filter = 'all') {
const container = document.getElementById('kanban-board');
// Группируем задачи по статусам (убрали 'unassigned')
const columns = {
'assigned': { title: 'Назначены', tasks: [], color: '#e74c3c' },
'in_progress': { title: 'В работе', tasks: [], color: '#f39c12' },
'rework': { title: 'На доработке', tasks: [], color: '#f1c40f' },
'overdue': { title: 'Просрочены', tasks: [], color: '#c0392b' },
'completed': { title: 'Выполнены', tasks: [], color: '#2ecc71' }
};
// Распределяем задачи по колонкам
kanbanTasks.forEach(task => {
const status = task.kanbanStatus || 'assigned';
// Преобразуем 'unassigned' в 'assigned'
const actualStatus = status === 'unassigned' ? 'assigned' : status;
if (columns[actualStatus]) {
columns[actualStatus].tasks.push(task);
} else {
// Если статус не найден, добавляем в 'assigned'
columns['assigned'].tasks.push(task);
}
});
// Статистика по фильтру
let filterTitle = 'Все задачи';
if (filter === 'created') filterTitle = 'Задачи, которые я поставил';
if (filter === 'assigned') filterTitle = 'Задачи, которые мне поставили';
container.innerHTML = `
<div class="kanban-controls">
<div class="kanban-filters">
<div class="kanban-period">
<label>Период просмотра:</label>
<select id="kanban-days" onchange="loadKanbanTasks()">
${[1, 2, 3, 4, 5, 6, 7, 14, 30, 62].map(days =>
`<option value="${days}" ${days === kanbanDays ? 'selected' : ''}>${days} ${getDayWord(days)}</option>`
).join('')}
</select>
</div>
<div class="kanban-filter-type">
<label>Показать:</label>
<select id="kanban-filter" onchange="loadKanbanTasks()">
<option value="all" ${filter === 'all' ? 'selected' : ''}>Все задачи</option>
<option value="created" ${filter === 'created' ? 'selected' : ''}>Я поставил</option>
<option value="assigned" ${filter === 'assigned' ? 'selected' : ''}>Мне поставили</option>
</select>
</div>
<div class="kanban-stats">
<span class="filter-title">${filterTitle}</span>
<span class="task-count">Всего задач: ${kanbanTasks.length}</span>
<button onclick="loadKanbanTasks()" class="refresh-btn">🔄 Обновить</button>
</div>
</div>
</div>
<div class="kanban-columns">
${Object.entries(columns).map(([status, column]) => `
<div class="kanban-column" data-status="${status}" ${status === 'overdue' || status === 'assigned' ? 'ondragover="return false" ondrop="return false"' : ''}>
<div class="kanban-column-header" style="background: ${column.color}">
<h3>${column.title}</h3>
<span class="kanban-count">${column.tasks.length}</span>
</div>
<div class="kanban-column-body" id="kanban-column-${status}"
${status === 'overdue' || status === 'assigned' ? 'style="opacity: 0.6; cursor: not-allowed;"' : ''}>
${renderKanbanCards(column.tasks, filter)}
</div>
</div>
`).join('')}
</div>
`;
// Делаем колонки перетаскиваемыми (кроме 'overdue' и 'assigned')
makeKanbanDraggable();
}
function renderKanbanCards(tasks, filter) {
if (tasks.length === 0) {
return '<div class="kanban-empty">Нет задач</div>';
}
return tasks.map(task => {
// Определяем иконку роли
let roleIcon = '';
let roleTitle = '';
if (task.userRole === 'creator') {
roleIcon = '👤';
roleTitle = 'Вы поставили эту задачу';
} else if (task.userRole === 'assignee') {
roleIcon = '🎯';
roleTitle = 'Вам поставили эту задачу';
}
// Исправление: безопасное получение имени пользователя
const userName = task.assignments && task.assignments.length > 0 && task.assignments[0]?.user_name
? task.assignments[0].user_name
: 'Неизвестно';
// Исправление: безопасное получение первого символа имени
const userInitial = userName && userName.length > 0 ? userName.charAt(0) : '?';
return `
<div class="kanban-card" draggable="true" data-task-id="${task.id}">
<div class="kanban-card-header">
<div class="kanban-task-id">#${task.id}</div>
<div class="kanban-task-role" title="${roleTitle}">${roleIcon}</div>
<div class="kanban-task-actions">
<button onclick="openKanbanTask(${task.id})" title="Открыть">👁️</button>
<button onclick="copyKanbanTask(${task.id})" title="Копировать">📋</button>
</div>
</div>
<div class="kanban-task-title" onclick="openKanbanTask(${task.id})">
${task.title || 'Без названия'}
</div>
<div class="kanban-task-info">
<div class="kanban-deadline">
${task.due_date ? `<span class="kanban-date">📅 ${formatDate(task.due_date)}</span>` : '<span class="kanban-no-date">Без срока</span>'}
</div>
<div class="kanban-assignees">
${task.assignments && task.assignments.length > 0 ?
task.assignments.slice(0, 3).map(a => {
// Исправление: безопасное получение имени исполнителя
const assigneeName = a.user_name || 'Неизвестно';
const assigneeInitial = assigneeName && assigneeName.length > 0 ? assigneeName.charAt(0) : '?';
return `<span class="kanban-assignee" title="${assigneeName}">${assigneeInitial}</span>`;
}).join('') :
'<span class="kanban-no-assignee">👤</span>'
}
${task.assignments && task.assignments.length > 3 ?
`<span class="kanban-more-assignees">+${task.assignments.length - 3}</span>` : ''
}
</div>
</div>
<div class="kanban-task-footer">
<span class="kanban-creator">👤 ${task.creator_name || 'Неизвестно'}</span>
${task.files && task.files.length > 0 ?
`<span class="kanban-files">📎 ${task.files.length}</span>` : ''
}
</div>
</div>
`;
}).join('');
}
function getDayWord(days) {
if (days === 1) return 'день';
if (days >= 2 && days <= 4) return 'дня';
return 'дней';
}
function makeKanbanDraggable() {
const cards = document.querySelectorAll('.kanban-card');
const columns = document.querySelectorAll('.kanban-column-body:not([style*="opacity: 0.6"])');
cards.forEach(card => {
card.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', card.dataset.taskId);
card.classList.add('dragging');
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
});
});
columns.forEach(column => {
const status = column.parentElement.dataset.status;
// Запрещаем перетаскивание в 'overdue' и 'assigned'
if (status === 'overdue' || status === 'assigned') {
return;
}
column.addEventListener('dragover', (e) => {
e.preventDefault();
const draggingCard = document.querySelector('.dragging');
if (draggingCard) {
column.appendChild(draggingCard);
}
});
column.addEventListener('drop', async (e) => {
e.preventDefault();
const taskId = e.dataTransfer.getData('text/plain');
const newStatus = column.parentElement.dataset.status;
if (taskId) {
try {
// Запрещаем установку статуса 'overdue' и 'assigned'
if (newStatus === 'overdue' || newStatus === 'assigned') {
alert('Невозможно изменить статус задачи на "Просрочены" или "Назначены" через Канбан');
// Возвращаем задачу в исходное положение
loadKanbanTasks();
return;
}
// Обновляем статус на сервере
const response = await fetch(`/api/kanban-tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus })
});
if (response.ok) {
// Перезагружаем Канбан
loadKanbanTasks();
} else {
const error = await response.json();
alert(`Ошибка обновления статуса: ${error.error || 'Неизвестная ошибка'}`);
// Возвращаем задачу в исходное положение
loadKanbanTasks();
}
} catch (error) {
console.error('Ошибка обновления статуса:', error);
alert('Ошибка обновления статуса');
loadKanbanTasks();
}
}
});
});
}
function openKanbanTask(taskId) {
// Находим задачу и открываем её в основном интерфейсе
const task = kanbanTasks.find(t => t.id == taskId);
if (task) {
showSection('tasks');
// Прокручиваем к задаче
setTimeout(() => {
const taskElement = document.querySelector(`.task-card[data-task-id="${taskId}"]`);
if (taskElement) {
taskElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Раскрываем задачу если она свернута
if (!expandedTasks.has(taskId)) {
toggleTask(taskId);
}
}
}, 100);
}
}
function copyKanbanTask(taskId) {
openCopyModal(taskId);
}
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU');
}