diff --git a/public/index.html b/public/index.html
index b3bcf3f..cb88a74 100644
--- a/public/index.html
+++ b/public/index.html
@@ -422,6 +422,7 @@
+
diff --git a/public/navbar.js b/public/navbar.js
index d9c98b5..d950890 100644
--- a/public/navbar.js
+++ b/public/navbar.js
@@ -1,3 +1,47 @@
+// Функция для проверки наличия указанной группы у текущего пользователя
+function navbar_checkUserGroup(navbar_groupName) {
+ try {
+ // Проверяем, есть ли данные пользователя
+ if (!currentUser || !currentUser.id) {
+ console.error('Пользователь не аутентифицирован или данные отсутствуют');
+ return false;
+ }
+
+ console.log('Текущий пользователь:', currentUser.login || currentUser.name);
+ const navbar_currentUserId = currentUser.id;
+
+ // Делаем синхронный запрос с помощью XMLHttpRequest
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', `/api2/idusers/user/${navbar_currentUserId}/groups`, false); // false = синхронный запрос
+ xhr.send();
+
+ if (xhr.status !== 200) {
+ console.error('Ошибка получения групп пользователя');
+ return false;
+ }
+
+ const navbar_groups = JSON.parse(xhr.responseText);
+
+ // Проверяем наличие указанной группы
+ const navbar_hasGroup = navbar_groups.some(userGroup => {
+ return userGroup === navbar_groupName ||
+ userGroup.includes(navbar_groupName) ||
+ userGroup.toLowerCase().includes(navbar_groupName.toLowerCase());
+ });
+
+ if (navbar_hasGroup) {
+ console.log(`✓ Пользователь состоит в группе "${navbar_groupName}"`);
+ return true;
+ } else {
+ console.log(`✗ Пользователь НЕ состоит в группе "${navbar_groupName}"`);
+ return false;
+ }
+
+ } catch (error) {
+ console.error(`Ошибка при проверке группы "${navbar_groupName}":`, error);
+ return false;
+ }
+}
// Функция для создания навигационной панели
function createNavigation() {
const navbar = document.getElementById('navbar-container');
@@ -38,6 +82,15 @@ navButtons.push(
id: "create-task-btn"
}
);
+ if (currentUser && navbar_checkUserGroup('Секретарь') || currentUser && currentUser.role === 'admin') {
+ navButtons.push({
+ onclick: "TasksType.show('document')",
+ className: "nav-btn tasks",
+ icon: "fas fa-list",
+ text: "Согласование",
+ id: "create-task-btn"
+ });
+ }
navButtons.push(
{
onclick: "showKanbanSection()",
diff --git a/public/tasks-type.js b/public/tasks-type.js
new file mode 100644
index 0000000..debc877
--- /dev/null
+++ b/public/tasks-type.js
@@ -0,0 +1,949 @@
+// 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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Добавляем секцию после основного контента или в указанное место
+ 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 = '⏳ Загрузка задач...
';
+
+ 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 = '❌ Ошибка загрузки задач
';
+ }
+ }
+
+ // Рендеринг задач
+ 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 = `${config.emptyMessage}
`;
+ 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 `
+
+
+
+
+ ${isExpanded ? renderExpandedContent(task) : ''}
+
+
+ `;
+ }).join('');
+ }
+
+ // Рендеринг развернутого содержимого
+ function renderExpandedContent(task) {
+ return `
+
+ ${currentUser && currentUser.login === 'minicrm' ? `
+
+
+
+
+
+ ` : ''}
+
+
+
+ ${task.description || 'Нет описания'}
+
+
+ ${task.rework_comment ? `
+
+ Комментарий к доработке: ${escapeHtml(task.rework_comment)}
+
+ ` : ''}
+
+
+ Файлы:
+ ${task.files && task.files.length > 0 ?
+ renderGroupedFiles(task) :
+ 'нет файлов'}
+
+
+
+
Исполнители:
+ ${task.assignments && task.assignments.length > 0 ?
+ renderAssignments(task.assignments, task.id) :
+ '
Не назначены
'}
+
+
+
+
+ Создана: ${formatDateTime(task.start_date || task.created_at)}
+ | Автор: ${task.creator_name || 'Неизвестно'}
+ ${task.due_date ? `| Срок: ${formatDateTime(task.due_date)}` : ''}
+
+
+ `;
+ }
+
+ // Рендеринг файлов
+ function renderGroupedFiles(task) {
+ if (!task.files || task.files.length === 0) {
+ return 'нет файлов';
+ }
+
+ 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 => `
+
+
${escapeHtml(uploader.name)}:
+
+
+ `).join('');
+ }
+
+ // Рендеринг исполнителей
+ function renderAssignments(assignments, taskId) {
+ return assignments.map(assignment => `
+
+
+
+ ${escapeHtml(assignment.user_name)}
+ ${assignment.user_id === currentUser?.id ? '(Вы)' : ''}
+ ${assignment.due_date ? `
+ Срок: ${formatDateTime(assignment.due_date)}
+ ` : ''}
+
+
+ ${assignment.user_id === currentUser?.id ? `
+
+ ` : ''}
+
+
+ `).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, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+// Инициализация при загрузке страницы
+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;
\ No newline at end of file
diff --git a/task-endpoints.js b/task-endpoints.js
index 1082657..b0c07c2 100644
--- a/task-endpoints.js
+++ b/task-endpoints.js
@@ -209,7 +209,185 @@ function setupTaskEndpoints(app, db, upload) {
});
});
});
+ // API для задач по типу
+app.get('/api/tasks_by_type', requireAuth, (req, res) => {
+ const userId = req.session.user.id;
+ const userRole = req.session.user.role;
+
+ // Получаем параметры фильтрации из query string
+ const showDeleted = userRole === 'admin' && req.query.showDeleted === 'true';
+ const search = req.query.search || '';
+ const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework';
+ const creatorFilter = req.query.creator || '';
+ const assigneeFilter = req.query.assignee || '';
+ const deadlineFilter = req.query.deadline || '';
+ const taskType = req.query.task_type || ''; // Новый параметр для фильтрации по типу задачи
+ let query = `
+ SELECT DISTINCT
+ t.*,
+ u.name as creator_name,
+ u.login as creator_login,
+ ot.title as original_task_title,
+ ou.name as original_creator_name,
+ GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
+ GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
+ FROM tasks t
+ LEFT JOIN users u ON t.created_by = u.id
+ LEFT JOIN tasks ot ON t.original_task_id = ot.id
+ LEFT JOIN users ou ON ot.created_by = ou.id
+ LEFT JOIN task_assignments ta ON t.id = ta.task_id
+ LEFT JOIN users u2 ON ta.user_id = u2.id
+ WHERE 1=1
+ `;
+
+ const params = [];
+
+ // Фильтрация по типу задачи
+ if (taskType) {
+ if (taskType.includes(',')) {
+ // Если перечислено несколько типов через запятую
+ const types = taskType.split(',');
+ query += ` AND t.task_type IN (${types.map(() => '?').join(',')})`;
+ types.forEach(type => params.push(type));
+ } else {
+ // Один конкретный тип
+ query += ` AND t.task_type = ?`;
+ params.push(taskType);
+ }
+ }
+
+ // Фильтрация по правам доступа
+ if (userRole !== 'admin') {
+ query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
+ params.push(userId, userId);
+ }
+
+ // Фильтрация по удаленным задачам
+ if (!showDeleted) {
+ query += " AND t.status = 'active'";
+ }
+
+ // Фильтрация по статусу
+ if (statusFilter && statusFilter !== 'all') {
+ const statuses = statusFilter.split(',');
+
+ if (statuses.includes('closed')) {
+ if (userRole !== 'admin') {
+ query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`;
+ params.push(userId);
+ } else {
+ query += ` AND t.closed_at IS NOT NULL`;
+ }
+ } else {
+ query += ` AND t.closed_at IS NULL`;
+
+ if (statuses.length > 0 && !statuses.includes('all')) {
+ query += ` AND EXISTS (
+ SELECT 1 FROM task_assignments ta2
+ WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')})
+ )`;
+ statuses.forEach(status => params.push(status));
+ }
+ }
+ } else {
+ if (userRole !== 'admin') {
+ query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`;
+ params.push(userId);
+ }
+ }
+
+ // Фильтрация по создателю
+ if (creatorFilter) {
+ query += ` AND t.created_by = ?`;
+ params.push(creatorFilter);
+ }
+
+ // Фильтрация по исполнителю
+ if (assigneeFilter) {
+ query += ` AND ta.user_id = ?`;
+ params.push(assigneeFilter);
+ }
+
+ // Фильтрация по дедлайну
+ if (deadlineFilter) {
+ const now = new Date();
+ let hours = 48;
+ if (deadlineFilter === '24h') hours = 24;
+
+ const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000);
+ const deadlineISO = deadlineTime.toISOString();
+ const nowISO = now.toISOString();
+
+ query += ` AND ta.due_date IS NOT NULL
+ AND ta.due_date > ?
+ AND ta.due_date <= ?
+ AND ta.status NOT IN ('completed', 'overdue')`;
+ params.push(nowISO, deadlineISO);
+ }
+
+ // Поиск по тексту
+ if (search) {
+ query += ` AND (t.title LIKE ? OR t.description LIKE ?)`;
+ const searchPattern = `%${search}%`;
+ params.push(searchPattern, searchPattern);
+ }
+
+ query += " GROUP BY t.id ORDER BY t.created_at DESC";
+
+ db.all(query, params, (err, tasks) => {
+ if (err) {
+ res.status(500).json({ error: err.message });
+ return;
+ }
+
+ const taskPromises = tasks.map(task => {
+ return new Promise((resolve) => {
+ db.all(`
+ SELECT ta.*, u.name as user_name, u.login as user_login
+ FROM task_assignments ta
+ LEFT JOIN users u ON ta.user_id = u.id
+ WHERE ta.task_id = ?
+ `, [task.id], (err, assignments) => {
+ if (err) {
+ task.assignments = [];
+ resolve(task);
+ return;
+ }
+
+ assignments.forEach(assignment => {
+ if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
+ assignment.status = 'overdue';
+ }
+ });
+
+ task.assignments = assignments || [];
+ resolve(task);
+ });
+ });
+ });
+
+ Promise.all(taskPromises).then(completedTasks => {
+ // Добавляем мета-информацию о фильтрации
+ const response = {
+ tasks: completedTasks,
+ meta: {
+ total: completedTasks.length,
+ filters: {
+ task_type: taskType || 'all',
+ status: statusFilter,
+ creator: creatorFilter || 'all',
+ assignee: assigneeFilter || 'all',
+ deadline: deadlineFilter || 'none',
+ search: search || 'none'
+ }
+ }
+ };
+
+ res.json(response);
+ });
+ });
+});
app.get('/api/tasks/no-date', requireAuth, (req, res) => {
const userId = req.session.user.id;