Files
minicrm/public/tasks-type.js
2026-02-17 17:35:56 +05:00

949 lines
34 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.
// tasks-type.js - Управление отображением задач по типам
// Не конфликтует с ui.js, использует собственные функции и пространство имен
const TasksType = (function() {
// Приватные переменные
let currentTasks = [];
let expandedTasks = new Set();
let currentType = 'document'; // Тип по умолчанию
// Конфигурация типов задач
const taskTypeConfig = {
'document': {
endpoint: '/api/tasks_by_type?task_type=document',
title: 'Документы',
icon: '📄',
badgeClass: 'document',
emptyMessage: 'Нет задач по документам'
},
'it': {
endpoint: '/api/tasks_by_type?task_type=it',
title: 'ИТ задачи',
icon: '💻',
badgeClass: 'it',
emptyMessage: 'Нет ИТ задач'
},
'ahch': {
endpoint: '/api/tasks_by_type?task_type=ahch',
title: 'АХЧ задачи',
icon: '🔧',
badgeClass: 'ahch',
emptyMessage: 'Нет задач АХЧ'
},
'psychologist': {
endpoint: '/api/tasks_by_type?task_type=psychologist',
title: 'Психолог',
icon: '🧠',
badgeClass: 'psychologist',
emptyMessage: 'Нет задач для психолога'
},
'speech_therapist': {
endpoint: '/api/tasks_by_type?task_type=speech_therapist',
title: 'Логопед',
icon: '🗣️',
badgeClass: 'speech_therapist',
emptyMessage: 'Нет задач для логопеда'
},
'hr': {
endpoint: '/api/tasks_by_type?task_type=hr',
title: 'Кадры',
icon: '👥',
badgeClass: 'hr',
emptyMessage: 'Нет кадровых задач'
},
'certificate': {
endpoint: '/api/tasks_by_type?task_type=certificate',
title: 'Справки',
icon: '📜',
badgeClass: 'certificate',
emptyMessage: 'Нет задач по справкам'
},
'e_journal': {
endpoint: '/api/tasks_by_type?task_type=e_journal',
title: 'Электронный журнал',
icon: '📊',
badgeClass: 'e_journal',
emptyMessage: 'Нет задач по ЭЖ'
}
};
// Инициализация
function init() {
createTaskTypeSection();
setupEventListeners();
}
// Создание секции для задач по типам
function createTaskTypeSection() {
// Проверяем, существует ли уже секция
if (document.getElementById('tasks-type-section')) {
return;
}
const section = document.createElement('div');
section.id = 'tasks-type-section';
section.className = 'section';
section.innerHTML = `
<div class="tasks-type-container">
<div class="tasks-type-header">
<h2 id="tasks-type-title">Документы</h2>
<div class="form-group hidden">
<div class="tasks-type-controls">
<select id="tasks-type-selector" class="tasks-type-select">
<option value="document" selected>📄 Документы</option>
<option value="it">💻 ИТ задачи</option>
<option value="ahch">🔧 АХЧ задачи</option>
<option value="psychologist">🧠 Психолог</option>
<option value="speech_therapist">🗣️ Логопед</option>
<option value="hr">👥 Кадры</option>
<option value="certificate">📜 Справки</option>
<option value="e_journal">📊 Электронный журнал</option>
</select>
<button id="tasks-type-refresh" class="tasks-type-refresh-btn" title="Обновить">🔄</button>
</div>
</div>
</div>
<div class="tasks-type-filters">
<div class="filter-group">
<input type="text" id="tasks-type-search" placeholder="Поиск по задачам..." class="tasks-type-search-input">
</div>
<div class="filter-group">
<select id="tasks-type-status-filter" class="tasks-type-filter-select">
<option value="all">Все статусы</option>
<option value="active">Активные</option>
<option value="assigned">Назначенные</option>
<option value="in_progress">В работе</option>
<option value="overdue">Просроченные</option>
<option value="rework">На доработке</option>
<option value="completed">Выполненные</option>
</select>
</div>
<div class="filter-group">
<label class="tasks-type-checkbox">
<input type="checkbox" id="tasks-type-show-deleted"> Показывать удаленные
</label>
</div>
</div>
<div id="tasks-type-list" class="tasks-type-list">
<div class="loading-spinner">⏳ Загрузка задач...</div>
</div>
</div>
`;
// Добавляем секцию после основного контента или в указанное место
const container = document.querySelector('.container') || document.body;
container.appendChild(section);
// Добавляем стили
addStyles();
}
// Добавление стилей
function addStyles() {
if (document.getElementById('tasks-type-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'tasks-type-styles';
style.textContent = `
.tasks-type-container {
padding: 20px;
max-width: 95%;
margin: 0 auto;
}
.tasks-type-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.tasks-type-header h2 {
margin: 0;
font-size: 24px;
color: #333;
}
.tasks-type-controls {
display: flex;
gap: 10px;
}
.tasks-type-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background-color: white;
cursor: pointer;
}
.tasks-type-select:hover {
border-color: #999;
}
.tasks-type-refresh-btn {
padding: 8px 12px;
background-color: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.tasks-type-refresh-btn:hover {
background-color: #e0e0e0;
}
.tasks-type-filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-group {
flex: 1;
min-width: 200px;
}
.tasks-type-search-input,
.tasks-type-filter-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.tasks-type-checkbox {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.tasks-type-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.tasks-type-card {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}
.tasks-type-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.tasks-type-card.deleted {
opacity: 0.7;
background-color: #f9f9f9;
}
.tasks-type-card.closed {
background-color: #f5f5f5;
}
.tasks-type-header-card {
padding: 15px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
}
.tasks-type-title-info {
flex: 1;
}
.tasks-type-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-right: 10px;
}
.tasks-type-badge.document { background-color: #e3f2fd; color: #1976d2; }
.tasks-type-badge.it { background-color: #f3e5f5; color: #7b1fa2; }
.tasks-type-badge.ahch { background-color: #fff3e0; color: #f57c00; }
.tasks-type-badge.psychologist { background-color: #e8f5e8; color: #388e3c; }
.tasks-type-badge.speech_therapist { background-color: #ffebee; color: #d32f2f; }
.tasks-type-badge.hr { background-color: #e1f5fe; color: #0288d1; }
.tasks-type-badge.certificate { background-color: #fce4ec; color: #c2185b; }
.tasks-type-badge.e_journal { background-color: #ede7f6; color: #512da8; }
.tasks-type-status {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin: 0 10px;
}
.tasks-type-status.status-red { background-color: #ffebee; color: #c62828; }
.tasks-type-status.status-orange { background-color: #fff3e0; color: #ef6c00; }
.tasks-type-status.status-green { background-color: #e8f5e8; color: #2e7d32; }
.tasks-type-status.status-yellow { background-color: #fff9c4; color: #fbc02d; }
.tasks-type-status.status-darkred { background-color: #ffcdd2; color: #b71c1c; }
.tasks-type-status.status-purple { background-color: #f3e5f5; color: #7b1fa2; }
.tasks-type-status.status-gray { background-color: #eeeeee; color: #616161; }
.tasks-type-expand-icon {
margin-left: 10px;
transition: transform 0.3s;
}
.tasks-type-content {
display: none;
padding: 15px;
}
.tasks-type-content.expanded {
display: block;
}
.tasks-type-actions {
display: flex;
gap: 8px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.tasks-type-btn {
padding: 6px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background-color 0.2s;
}
.tasks-type-btn:hover {
opacity: 0.9;
}
.tasks-type-description {
background-color: #f9f9f9;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.tasks-type-rework {
background-color: #fff3e0;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
border-left: 4px solid #ff9800;
}
.tasks-type-files {
margin: 10px 0;
}
.tasks-type-assignments {
margin: 10px 0;
}
.tasks-type-assignment {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #f0f0f0;
}
.tasks-type-assignment:last-child {
border-bottom: none;
}
.tasks-type-assignment-status {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
}
.tasks-type-meta {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 12px;
}
.tasks-type-empty {
text-align: center;
padding: 40px;
color: #999;
font-size: 16px;
background-color: #f9f9f9;
border-radius: 8px;
}
.tasks-type-loading {
text-align: center;
padding: 40px;
color: #666;
}
.tasks-type-error {
text-align: center;
padding: 40px;
color: #d32f2f;
background-color: #ffebee;
border-radius: 8px;
}
.file-group {
margin: 10px 0;
}
.file-icons-container {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 5px;
}
.file-icon-link {
text-decoration: none;
color: #1976d2;
padding: 4px 8px;
background-color: #e3f2fd;
border-radius: 4px;
font-size: 13px;
}
.file-icon-link:hover {
background-color: #bbdefb;
}
.deadline-indicator {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
margin-left: 5px;
}
.deadline-indicator.deadline-24h {
background-color: #ffebee;
color: #c62828;
}
.deadline-indicator.deadline-48h {
background-color: #fff3e0;
color: #ef6c00;
}
`;
document.head.appendChild(style);
}
// Настройка обработчиков событий
function setupEventListeners() {
// Селектор типа задач
const selector = document.getElementById('tasks-type-selector');
if (selector) {
selector.addEventListener('change', function(e) {
currentType = e.target.value;
updateTitle();
loadTasks();
});
}
// Кнопка обновления
const refreshBtn = document.getElementById('tasks-type-refresh');
if (refreshBtn) {
refreshBtn.addEventListener('click', function() {
loadTasks();
});
}
// Поиск
const searchInput = document.getElementById('tasks-type-search');
if (searchInput) {
searchInput.addEventListener('input', debounce(function() {
loadTasks();
}, 300));
}
// Фильтр статуса
const statusFilter = document.getElementById('tasks-type-status-filter');
if (statusFilter) {
statusFilter.addEventListener('change', function() {
loadTasks();
});
}
// Показ удаленных
const showDeleted = document.getElementById('tasks-type-show-deleted');
if (showDeleted) {
showDeleted.addEventListener('change', function() {
loadTasks();
});
}
}
// Debounce функция для поиска
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Обновление заголовка
function updateTitle() {
const title = document.getElementById('tasks-type-title');
const config = taskTypeConfig[currentType] || taskTypeConfig.document;
if (title) {
title.textContent = `${config.icon} ${config.title}`;
}
}
// Загрузка задач
async function loadTasks() {
const listContainer = document.getElementById('tasks-type-list');
if (!listContainer) return;
listContainer.innerHTML = '<div class="loading-spinner">⏳ Загрузка задач...</div>';
try {
const config = taskTypeConfig[currentType] || taskTypeConfig.document;
let url = config.endpoint;
// Добавляем параметры фильтрации
const params = new URLSearchParams();
const search = document.getElementById('tasks-type-search')?.value;
if (search) {
params.append('search', search);
}
const status = document.getElementById('tasks-type-status-filter')?.value;
if (status && status !== 'all') {
if (status === 'active') {
params.append('status', 'active');
} else {
params.append('status', status);
}
}
const showDeleted = document.getElementById('tasks-type-show-deleted')?.checked;
if (showDeleted) {
params.append('showDeleted', 'true');
}
const queryString = params.toString();
if (queryString) {
url += '&' + queryString;
}
const response = await fetch(url);
const data = await response.json();
// Проверяем структуру ответа
if (data.tasks) {
currentTasks = data.tasks;
} else if (Array.isArray(data)) {
currentTasks = data;
} else {
currentTasks = [];
}
renderTasks();
} catch (error) {
console.error('Ошибка загрузки задач:', error);
listContainer.innerHTML = '<div class="tasks-type-error">❌ Ошибка загрузки задач</div>';
}
}
// Рендеринг задач
function renderTasks() {
const container = document.getElementById('tasks-type-list');
if (!container) return;
if (!currentTasks || currentTasks.length === 0) {
const config = taskTypeConfig[currentType] || taskTypeConfig.document;
container.innerHTML = `<div class="tasks-type-empty">${config.emptyMessage}</div>`;
return;
}
container.innerHTML = currentTasks.map(task => {
const isExpanded = expandedTasks.has(task.id);
const overallStatus = getTaskOverallStatus(task);
const statusClass = getStatusClass(overallStatus);
const isDeleted = task.status === 'deleted';
const isClosed = task.closed_at !== null;
const timeLeftInfo = getTimeLeftInfo(task);
const config = taskTypeConfig[task.task_type] || taskTypeConfig.document;
return `
<div class="tasks-type-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}" data-task-id="${task.id}">
<div class="tasks-type-header-card" onclick="TasksType.toggleTask(${task.id})">
<div class="tasks-type-title-info">
<span class="tasks-type-badge ${task.task_type || 'document'}">
${config.icon} ${getTaskTypeDisplayName(task.task_type)}
</span>
<span class="task-number">№${task.id}</span>
<strong>${escapeHtml(task.title)}</strong>
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
${timeLeftInfo ? `<span class="deadline-indicator ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
</div>
<span class="tasks-type-status ${statusClass}">
Выполнить до: ${formatDateTime(task.due_date || task.created_at)}
</span>
<div class="tasks-type-expand-icon" style="transform: rotate(${isExpanded ? '180deg' : '0deg'});">
</div>
</div>
<div class="tasks-type-content ${isExpanded ? 'expanded' : ''}">
${isExpanded ? renderExpandedContent(task) : ''}
</div>
</div>
`;
}).join('');
}
// Рендеринг развернутого содержимого
function renderExpandedContent(task) {
return `
<div class="tasks-type-actions">
${currentUser && currentUser.login === 'minicrm' ? `
<button class="tasks-type-btn" onclick="TasksType.openTaskChat(${task.id})" title="Открыть чат">💬</button>
<button class="tasks-type-btn" onclick="TasksType.openAddFileModal(${task.id})" title="Добавить файл">📎</button>
<button class="tasks-type-btn" onclick="TasksType.openEditModal(${task.id})" title="Редактировать">✏️</button>
<button class="tasks-type-btn" onclick="TasksType.openCopyModal(${task.id})" title="Создать копию">📋</button>
` : ''}
</div>
<div class="tasks-type-description">
${task.description || 'Нет описания'}
</div>
${task.rework_comment ? `
<div class="tasks-type-rework">
<strong>Комментарий к доработке:</strong> ${escapeHtml(task.rework_comment)}
</div>
` : ''}
<div class="tasks-type-files">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ?
renderGroupedFiles(task) :
'<span>нет файлов</span>'}
</div>
<div class="tasks-type-assignments">
<strong>Исполнители:</strong>
${task.assignments && task.assignments.length > 0 ?
renderAssignments(task.assignments, task.id) :
'<div>Не назначены</div>'}
</div>
<div class="tasks-type-meta">
<small>
Создана: ${formatDateTime(task.start_date || task.created_at)}
| Автор: ${task.creator_name || 'Неизвестно'}
${task.due_date ? `| Срок: ${formatDateTime(task.due_date)}` : ''}
</small>
</div>
`;
}
// Рендеринг файлов
function renderGroupedFiles(task) {
if (!task.files || task.files.length === 0) {
return '<span>нет файлов</span>';
}
const filesByUploader = {};
task.files.forEach(file => {
const uploaderId = file.user_id;
const uploaderName = file.user_name || 'Неизвестный пользователь';
if (!filesByUploader[uploaderId]) {
filesByUploader[uploaderId] = {
name: uploaderName,
files: []
};
}
filesByUploader[uploaderId].files.push(file);
});
return Object.values(filesByUploader).map(uploader => `
<div class="file-group">
<div><strong>${escapeHtml(uploader.name)}:</strong></div>
<div class="file-icons-container">
${uploader.files.map(file => `
<a href="/api/files/${file.id}" target="_blank" class="file-icon-link" title="${escapeHtml(file.original_name)}">
📎 ${escapeHtml(file.original_name).substring(0, 15)}${file.original_name.length > 15 ? '...' : ''}
</a>
`).join('')}
</div>
</div>
`).join('');
}
// Рендеринг исполнителей
function renderAssignments(assignments, taskId) {
return assignments.map(assignment => `
<div class="tasks-type-assignment">
<span class="tasks-type-assignment-status ${getStatusClass(assignment.status)}"></span>
<div style="flex: 1;">
<strong>${escapeHtml(assignment.user_name)}</strong>
${assignment.user_id === currentUser?.id ? '<small>(Вы)</small>' : ''}
${assignment.due_date ? `
<small>Срок: ${formatDateTime(assignment.due_date)}</small>
` : ''}
</div>
<div>
${assignment.user_id === currentUser?.id ? `
<button class="tasks-type-btn" onclick="TasksType.updateStatus(${taskId}, ${assignment.user_id}, 'completed')">
✅ Выполнено
</button>
` : ''}
</div>
</div>
`).join('');
}
// Публичные методы
return {
init: init,
show: function(type = 'document') {
currentType = type;
const selector = document.getElementById('tasks-type-selector');
if (selector) {
selector.value = type;
}
updateTitle();
loadTasks();
// Показываем секцию
const section = document.getElementById('tasks-type-section');
if (section) {
// Скрываем другие секции
document.querySelectorAll('.section').forEach(s => {
s.classList.remove('active');
});
section.classList.add('active');
}
},
loadTasks: loadTasks,
toggleTask: function(taskId) {
if (expandedTasks.has(taskId)) {
expandedTasks.delete(taskId);
} else {
expandedTasks.add(taskId);
}
renderTasks();
},
updateStatus: async function(taskId, userId, status) {
try {
const response = await fetch(`/api/tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId, status })
});
if (response.ok) {
await loadTasks();
} else {
const error = await response.json();
alert('Ошибка: ' + (error.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка');
}
},
openTaskChat: function(taskId) {
window.open(`/chat?task_id=${taskId}`, '_blank');
},
openAddFileModal: function(taskId) {
if (typeof openAddFileModal === 'function') {
openAddFileModal(taskId);
} else {
alert('Функция добавления файлов недоступна');
}
},
openEditModal: function(taskId) {
if (typeof openEditModal === 'function') {
openEditModal(taskId);
} else {
alert('Функция редактирования недоступна');
}
},
openCopyModal: function(taskId) {
if (typeof openCopyModal === 'function') {
openCopyModal(taskId);
} else {
alert('Функция копирования недоступна');
}
}
};
})();
// Вспомогательные функции (не конфликтуют с ui.js)
function getTaskOverallStatus(task) {
if (task.status === 'deleted') return 'deleted';
if (task.closed_at) return 'closed';
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
const assignments = task.assignments;
let hasAssigned = false;
let hasInProgress = false;
let hasOverdue = false;
let hasRework = false;
let allCompleted = true;
for (let assignment of assignments) {
if (assignment.status === 'assigned') {
hasAssigned = true;
allCompleted = false;
} else if (assignment.status === 'in_progress') {
hasInProgress = true;
allCompleted = false;
} else if (assignment.status === 'overdue') {
hasOverdue = true;
allCompleted = false;
} else if (assignment.status === 'rework') {
hasRework = true;
allCompleted = false;
} else if (assignment.status !== 'completed') {
allCompleted = false;
}
}
if (allCompleted) return 'completed';
if (hasRework) return 'rework';
if (hasOverdue) return 'overdue';
if (hasInProgress) return 'in_progress';
if (hasAssigned) return 'assigned';
return 'unassigned';
}
function getStatusClass(status) {
switch (status) {
case 'deleted': return 'status-gray';
case 'closed': return 'status-gray';
case 'unassigned': return 'status-purple';
case 'assigned': return 'status-red';
case 'in_progress': return 'status-orange';
case 'rework': return 'status-yellow';
case 'overdue': return 'status-darkred';
case 'completed': return 'status-green';
default: return 'status-purple';
}
}
function getTimeLeftInfo(task) {
if (!task.due_date || task.closed_at) return null;
const dueDate = new Date(task.due_date);
const now = new Date();
const timeLeft = dueDate.getTime() - now.getTime();
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
if (hoursLeft <= 0) return null;
if (hoursLeft <= 24) {
return {
text: `Осталось ${hoursLeft}ч`,
class: 'deadline-24h'
};
} else if (hoursLeft <= 48) {
return {
text: `Осталось ${hoursLeft}ч`,
class: 'deadline-48h'
};
}
return null;
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '';
const date = new Date(dateTimeString);
return date.toLocaleString('ru-RU');
}
function getTaskTypeDisplayName(type) {
const typeNames = {
'regular': 'Задача',
'document': 'Документ',
'it': 'ИТ',
'ahch': 'АХЧ',
'psychologist': 'Психолог',
'speech_therapist': 'Логопед',
'hr': 'Кадры',
'certificate': 'Справка',
'e_journal': 'Эл. журнал'
};
return typeNames[type] || type;
}
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
// Проверяем, что currentUser определен (из основного скрипта)
if (typeof currentUser !== 'undefined') {
TasksType.init();
} else {
// Ждем загрузки currentUser
const checkUser = setInterval(function() {
if (typeof currentUser !== 'undefined') {
clearInterval(checkUser);
TasksType.init();
}
}, 100);
}
});
// Экспортируем в глобальную область
window.TasksType = TasksType;