Files
minicrm/public/tasks-type.js

1213 lines
46 KiB
JavaScript
Raw Permalink 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'; // Тип по умолчанию
let currentUser = null;
// Конфигурация типов задач
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: 'Нет задач по ЭЖ'
},
'Social_educator': {
endpoint: '/api/tasks_by_type?task_type=Social_educator',
title: 'Социальный педагог',
icon: '📊',
badgeClass: 'Social_educator',
emptyMessage: 'Нет задач по Социальный педагог'
}
};
// Функция для получения подписантов из idgroups
async function getSigners() {
try {
// Сначала пробуем получить через новый API
const response = await fetch('/api2/idusers');
if (!response.ok) {
console.warn('API idusers не доступен, пробуем альтернативный метод');
return await getSignersAlternative();
}
const data = await response.json();
// Фильтруем только активных пользователей
// Проверяем наличие группы "Подписант" в group_name или в metadata
const signers = data.filter(user => {
if (!user.is_active) return false;
// Проверяем group_name (если есть)
if (user.group_name && user.group_name.toLowerCase().includes('подписант')) {
return true;
}
// Проверяем metadata (если есть)
if (user.metadata && typeof user.metadata === 'object') {
const groups = user.metadata.groups || [];
if (groups.some(g => g.toLowerCase().includes('подписант'))) {
return true;
}
}
// Проверяем ldap_group (если есть)
if (user.ldap_group && user.ldap_group.toLowerCase().includes('подписант')) {
return true;
}
return false;
});
console.log('Найдены подписанты через API:', signers);
// Преобразуем в нужный формат
return signers.map(s => ({
id: s.user_id,
name: s.user_name || 'Неизвестно',
login: s.user_login || s.login || '',
external_id: s.external_id || ''
}));
} catch (error) {
console.error('Ошибка получения подписантов:', error);
return await getSignersAlternative();
}
}
// Альтернативный метод получения подписантов
async function getSignersAlternative() {
try {
// Пробуем получить через обычный API пользователей
const response = await fetch('/api/users');
if (!response.ok) return [];
const users = await response.json();
// Фильтруем по роли или другим признакам
const signers = users.filter(user => {
// Например, пользователи с ролью 'admin' или специальным полем
return user.role === 'admin' || user.role === 'signer';
});
return signers.map(s => ({
id: s.id,
name: s.name || s.login,
login: s.login,
external_id: s.external_id || ''
}));
} catch (error) {
console.error('Ошибка альтернативного получения подписантов:', error);
return [];
}
}
// Функция для замены исполнителя на подписанта
async function replaceWithSigner(taskId, currentUserId) {
try {
const signers = await getSigners();
if (signers.length === 0) {
alert('❌ Подписанты не найдены в системе.\n\n' +
'Проверьте:\n' +
'1. В таблице idgroups создана группа "Подписант"\n' +
'2. В таблице idusers есть пользователи в этой группе\n' +
'3. Пользователи активны (is_active = true)');
return false;
}
// Показываем диалог подтверждения с информацией о подписантах
const signerNames = signers.map(s => `${s.name} (${s.login})`).join('\n');
const message = signers.length === 1
? `✍️ Назначить подписанта:\n\n${signerNames}\n\nТекущий исполнитель будет заменен.`
: `✍️ Назначить подписантов:\n\n${signerNames}\n\nБудут назначены ВСЕ найденные подписанты.`;
if (!confirm(message)) {
return false;
}
let response;
// Если несколько подписантов, назначаем всех
if (signers.length > 1) {
const signerIds = signers.map(s => s.id);
response = await fetch(`/api/tasks/${taskId}/replace-all-assignees`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
newAssigneeIds: signerIds
})
});
if (response.ok) {
const signerNames = signers.map(s => s.name).join(', ');
alert(`✅ Задача назначена подписантам:\n${signerNames}`);
}
} else {
// Если один подписант, заменяем текущего
const signer = signers[0];
response = await fetch(`/api/tasks/${taskId}/replace-assignee`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
oldAssigneeId: currentUserId,
newAssigneeId: signer.id
})
});
if (response.ok) {
alert(`✅ Задача переназначена подписанту:\n${signer.name} (${signer.login})`);
}
}
if (response && response.ok) {
// Перезагружаем задачи
await loadTasks();
return true;
} else if (response) {
const error = await response.json();
alert('❌ Ошибка: ' + (error.error || 'Неизвестная ошибка'));
}
return false;
} catch (error) {
console.error('Ошибка при переназначении:', error);
alert('❌ Сетевая ошибка при переназначении: ' + error.message);
return false;
}
}
// Инициализация
function init() {
// Получаем текущего пользователя
fetch('/api/user')
.then(response => response.json())
.then(data => {
if (data.user) {
currentUser = data.user;
console.log('Текущий пользователь:', currentUser);
}
})
.catch(error => console.error('Ошибка получения пользователя:', error));
createTaskTypeSection();
setupEventListeners();
loadTasks();
}
// Создание секции для задач по типам
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="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 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;
flex-wrap: wrap;
gap: 15px;
}
.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;
flex-wrap: wrap;
gap: 10px;
}
.tasks-type-title-info {
flex: 1;
min-width: 250px;
}
.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; }
.task-number {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.tasks-type-status {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin: 0 10px;
white-space: nowrap;
}
.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;
align-items: center;
}
.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-btn.sign-btn {
background-color: #4a90e2;
color: white;
}
.tasks-type-btn.sign-btn:hover {
background-color: #357abd;
}
.tasks-type-btn.complete-btn {
background-color: #27ae60;
color: white;
}
.tasks-type-btn.complete-btn:hover {
background-color: #219a52;
}
.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: 10px;
border-bottom: 1px solid #f0f0f0;
gap: 10px;
}
.tasks-type-assignment:last-child {
border-bottom: none;
}
.tasks-type-assignment-status {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.tasks-type-assignment-status.status-red { background-color: #c62828; }
.tasks-type-assignment-status.status-orange { background-color: #ef6c00; }
.tasks-type-assignment-status.status-green { background-color: #2e7d32; }
.tasks-type-assignment-status.status-yellow { background-color: #fbc02d; }
.tasks-type-assignment-status.status-darkred { background-color: #b71c1c; }
.tasks-type-assignment-status.status-purple { background-color: #7b1fa2; }
.tasks-type-assignment-status.status-gray { background-color: #616161; }
.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;
}
.deleted-badge, .closed-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
margin-left: 5px;
}
.deleted-badge {
background-color: #ffebee;
color: #c62828;
}
.closed-badge {
background-color: #eeeeee;
color: #616161;
}
`;
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,assigned,in_progress,overdue,rework');
} 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) {
// Проверяем, является ли текущий пользователь исполнителем задачи
const isCurrentUserAssignee = task.assignments && task.assignments.some(
assignment => assignment.user_id === currentUser?.id
);
return `
<div class="tasks-type-actions">
${currentUser && (currentUser.role === 'admin' || currentUser.role === 'tasks') ? `
<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>
` : ''}
${isCurrentUserAssignee ? `
<button class="tasks-type-btn sign-btn" onclick="TasksType.replaceWithSigner(${task.id}, ${currentUser.id})" title="Передать подписанту">✍️ Подписать</button>
<button class="tasks-type-btn complete-btn" onclick="TasksType.updateStatus(${task.id}, ${currentUser.id}, 'completed')" title="Отметить как выполненное">
✅ Выполнено
</button>
` : ''}
</div>
<div class="tasks-type-description">
<strong>Описание:</strong><br>
${escapeHtml(task.description) || 'Нет описания'}
</div>
${task.rework_comment ? `
<div class="tasks-type-rework">
<strong>🔄 Комментарий к доработке:</strong><br>
${escapeHtml(task.rework_comment)}
</div>
` : ''}
<div class="tasks-type-files">
<strong>📎 Файлы:</strong>
${task.files && task.files.length > 0 ?
renderGroupedFiles(task) :
'<div>нет файлов</div>'}
</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}/download" target="_blank" class="file-icon-link" title="${escapeHtml(file.original_name)}">
📎 ${escapeHtml(file.original_name).substring(0, 20)}${file.original_name.length > 20 ? '...' : ''}
</a>
`).join('')}
</div>
</div>
`).join('');
}
// Рендеринг исполнителей (теперь только информация, без кнопок)
function renderAssignments(assignments, taskId) {
return assignments.map(assignment => {
return `
<div class="tasks-type-assignment" data-user-id="${assignment.user_id}">
<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>
`;
}).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 => {
if (s.id !== 'tasks-type-section') {
s.style.display = 'none';
}
});
section.style.display = 'block';
}
},
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('❌ Сетевая ошибка');
}
},
replaceWithSigner: replaceWithSigner,
openTaskChat: function(taskId) {
window.open(`/chat?task_id=${taskId}`, '_blank');
},
openAddFileModal: function(taskId) {
if (typeof window.openAddFileModal === 'function') {
window.openAddFileModal(taskId);
} else {
alert('Функция добавления файлов недоступна');
}
},
openEditModal: function(taskId) {
if (typeof window.openEditModal === 'function') {
window.openEditModal(taskId);
} else {
alert('Функция редактирования недоступна');
}
},
openCopyModal: function(taskId) {
if (typeof window.openCopyModal === 'function') {
window.openCopyModal(taskId);
} else {
alert('Функция копирования недоступна');
}
},
// Метод для проверки наличия подписантов
checkSigners: async function() {
const signers = await getSigners();
console.log('Найдено подписантов:', signers.length, signers);
return signers;
}
};
})();
// Вспомогательные функции
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', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
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 из основного скрипта
const checkUser = setInterval(function() {
if (typeof window.currentUser !== 'undefined') {
clearInterval(checkUser);
TasksType.init();
}
}, 100);
// Таймаут на случай, если currentUser так и не появится
setTimeout(function() {
clearInterval(checkUser);
TasksType.init();
}, 5000);
});
// Экспортируем в глобальную область
window.TasksType = TasksType;