Files
minicrm/public/nav-task-actions.js
2026-03-26 17:53:56 +05:00

587 lines
26 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.
// nav-task-actions.js модальное окно с действиями задачи (сетка 3 колонки, центрированное)
(function() {
'use strict';
const LOG_ENABLED = 0;
function log(...args) {
if (LOG_ENABLED) console.log(...args);
}
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ РЕКВИЗИТОВ ==========
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function updateTaskCardDisplay(taskId, data) {
const taskCards = document.querySelectorAll(`[data-task-id="${taskId}"]`);
taskCards.forEach(card => {
let docContainer = card.querySelector('.document-fields-display');
if (!docContainer) {
docContainer = document.createElement('div');
docContainer.className = 'document-fields-display';
const header = card.querySelector('.task-header');
if (header) header.after(docContainer);
else card.prepend(docContainer);
}
let html = '<div style="margin: 8px 0; padding: 5px; background-color: #f5f5f5; border-radius: 4px; font-size: 0.9em;">';
if (data.document_n) html += `<span style="margin-right: 15px;"><strong>№:</strong> ${escapeHtml(data.document_n)}</span>`;
if (data.document_d) html += `<span style="margin-right: 15px;"><strong>Дата:</strong> ${escapeHtml(data.document_d)}</span>`;
if (data.document_a) html += `<span><strong>Автор:</strong> ${escapeHtml(data.document_a)}</span>`;
if (!data.document_n && !data.document_d) html += '<span style="color: #999;">Нет данных документа</span>';
html += '</div>';
docContainer.innerHTML = html;
});
}
async function openDocumentFieldsModal(taskId) {
const existing = document.getElementById('doc-fields-modal');
if (existing) existing.remove();
// Загружаем текущие поля
let fields = { document_n: '', document_d: '', document_a: '' };
try {
const response = await fetch(`/api/tasks/${taskId}/document-fields`);
if (response.ok) {
const result = await response.json();
fields = result.data || {};
}
} catch (error) {
console.error('Ошибка загрузки реквизитов:', error);
}
// Преобразуем дату из ДД.ММ.ГГГГ в YYYY-MM-DD для input type="date"
let dateValue = '';
if (fields.document_d) {
const parts = fields.document_d.split('.');
if (parts.length === 3) {
dateValue = `${parts[2]}-${parts[1]}-${parts[0]}`;
}
}
// Проверяем права на редактирование
let canEdit = false;
const hasFields = !!(fields.document_n || fields.document_d);
if (hasFields) {
canEdit = fields.document_a === currentUser?.login;
} else {
try {
const groupsRes = await fetch(`/api2/idusers/user/${currentUser.id}/groups`);
if (groupsRes.ok) {
const groups = await groupsRes.json();
canEdit = groups.some(g => g === 'Подписант' || g === 'Секретарь' || g.includes('Подписант') || g.includes('Секретарь'));
}
} catch (error) {
console.error('Ошибка проверки групп:', error);
}
}
// Создаём модальное окно
const modal = document.createElement('div');
modal.id = 'doc-fields-modal';
modal.className = 'modal';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.zIndex = '10001';
modal.innerHTML = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>📄 Реквизиты документа</h3>
<span class="close" onclick="window.closeDocFieldsModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="doc-number">Номер документа</label>
<input type="text" id="doc-number" class="form-control" value="${escapeHtml(fields.document_n || '')}" ${!canEdit ? 'readonly' : ''}>
</div>
<div class="form-group">
<label for="doc-date">Дата документа</label>
<input type="date" id="doc-date" class="form-control" value="${dateValue}" ${!canEdit ? 'readonly' : ''}>
</div>
<div class="form-group">
<label for="doc-author">Автор (логин)</label>
<input type="text" id="doc-author" class="form-control" value="${escapeHtml(fields.document_a || currentUser?.login || '')}" readonly>
</div>
${!canEdit && hasFields ? '<div class="alert alert-warning">⚠️ Вы не можете редактировать реквизиты, так как они уже заполнены другим пользователем.</div>' : ''}
${!canEdit && !hasFields ? '<div class="alert alert-warning">⚠️ У вас нет прав для указания реквизитов.</div>' : ''}
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="window.closeDocFieldsModal()">Отмена</button>
${canEdit ? `<button class="btn-primary" id="save-doc-fields">Сохранить</button>` : ''}
</div>
</div>
`;
document.body.appendChild(modal);
window.closeDocFieldsModal = function() {
const m = document.getElementById('doc-fields-modal');
if (m) m.remove();
delete window.closeDocFieldsModal;
};
modal.addEventListener('click', (e) => {
if (e.target === modal) window.closeDocFieldsModal();
});
if (canEdit) {
const saveBtn = document.getElementById('save-doc-fields');
saveBtn.addEventListener('click', async () => {
const number = document.getElementById('doc-number').value.trim();
const dateInput = document.getElementById('doc-date');
let formattedDate = '';
if (dateInput.value) {
// Преобразуем YYYY-MM-DD в ДД.ММ.ГГГГ
const parts = dateInput.value.split('-');
if (parts.length === 3) {
formattedDate = `${parts[2]}.${parts[1]}.${parts[0]}`;
}
}
const author = document.getElementById('doc-author').value.trim() || currentUser?.login;
saveBtn.disabled = true;
saveBtn.textContent = 'Сохранение...';
try {
const response = await fetch(`/api/tasks/${taskId}/document-fields`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document_n: number || null,
document_d: formattedDate || null,
document_a: author
})
});
const result = await response.json();
if (response.ok && result.success) {
alert('✅ Реквизиты сохранены');
window.closeDocFieldsModal();
const task = window.tasks?.find(t => t.id == taskId);
if (task) task.document_fields = result.data;
updateTaskCardDisplay(taskId, result.data);
if (typeof loadTasks === 'function') loadTasks();
} else {
alert('❌ Ошибка: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка сохранения:', error);
alert('Сетевая ошибка');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Сохранить';
}
});
}
}
// ========== ОСНОВНАЯ ЛОГИКА МЕНЮ ДЕЙСТВИЙ ==========
// Сбор доступных действий для конкретной задачи
function buildActionsForTask(task) {
const taskId = task.id;
const isDeleted = task.status === 'deleted';
const isClosed = task.closed_at !== null;
const userRole = window.getUserRoleInTask ? window.getUserRoleInTask(task) : 'Наблюдатель';
const canEdit = window.canUserEditTask ? window.canUserEditTask(task) : false;
const actions = [];
// ==== ДОБАВЛЕННЫЙ БЛОК: кнопки для текущего исполнителя
if (currentUser && !isDeleted && !isClosed) {
const myAssignment = task.assignments?.find(a => parseInt(a.user_id) === currentUser.id);
if (myAssignment) {
if (myAssignment.status === 'assigned') {
actions.push({
label: '▶️ Приступить',
handler: () => window.updateStatus(taskId, currentUser.id, 'in_progress'),
primary: true
});
}
if (['in_progress', 'overdue', 'rework'].includes(myAssignment.status)) {
const isDocumentTask = task.task_type === 'document';
const handler = isDocumentTask
? () => window.openDocumentCompleteModal(taskId, currentUser.id)
: () => window.updateStatus(taskId, currentUser.id, 'completed');
actions.push({
label: '✅ Выполнено',
handler: handler,
primary: true
});
}
}
}
// Копия доступна всем, у кого есть кнопка
if (typeof openCopyModal === 'function') {
actions.push({
label: '📋 Создать копию',
handler: () => openCopyModal(taskId),
primary: true
});
}
// Действия для активных (не удалённых, не закрытых) задач
if (!isDeleted && !isClosed && currentUser.login === 'minicrm') {
if (typeof openTaskChat === 'function') {
actions.push({ label: '💬 Чат', handler: () => openTaskChat(taskId) });
}
if (typeof openAddFileModal === 'function') {
actions.push({ label: '📎 Добавить файл', handler: () => openAddFileModal(taskId) });
}
}
// Специальные пользователи
if (currentUser && currentUser.login === 'minicrm') {
if (typeof openEditModal === 'function') {
actions.push({ label: '✏️ Редактировать', handler: () => openEditModal(taskId) });
}
if (typeof openManageAssigneesModal === 'function') {
actions.push({ label: '👥 Управление исполнителями', handler: () => openManageAssigneesModal(taskId) });
}
}
// Подписание (только для документов, если текущий пользователь - исполнитель)
if (!isDeleted && !isClosed && task.task_type === 'document' && currentUser && task.assignments?.some(a => parseInt(a.user_id) === currentUser.id)) {
if (typeof window.signTask === 'function') {
actions.push({
label: '✍️ Подписать',
handler: () => window.signTask(taskId, currentUser.id),
admin: true
});
}
}
// Реквизиты документа (для задач типа document)
if (!isDeleted && !isClosed && task.task_type === 'document' && currentUser && task.assignments?.some(a => parseInt(a.user_id) === currentUser.id)) {
actions.push({
label: '📄 Реквизиты',
handler: () => openDocumentFieldsModal(taskId),
admin: false
});
}
// Администраторы и роль tasks
if (currentUser && (currentUser.role === 'admin' || (currentUser.role === 'tasks' && canEdit))) {
if (typeof assignAdd_openModal === 'function') {
actions.push({
label: '🧑‍💼➕ Добавить исполнителя',
handler: () => assignAdd_openModal(taskId),
admin: true
});
}
if (typeof assignRemove_openModal === 'function') {
actions.push({
label: '🧑‍💼❌ Удалить исполнителя',
handler: () => assignRemove_openModal(taskId),
admin: true
});
}
if (currentUser && (currentUser.role === 'admin' && currentUser.login === 'minicrm')) {
if (typeof openReworkModal === 'function') {
actions.push({ label: '🔄 Переделать документ', handler: () => openReworkModal(taskId) });
}
}
}
// Кнопка "Создать ознакомление" для админов и tasks
if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'tasks')) {
actions.push({
label: '📖 Создать ознакомление',
handler: () => openAcquaintanceModal(task.id),
primary: true
});
}
// Доработка и изменение срока для необычных задач (исполнитель)
if (!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser?.id)) {
if (typeof openReworkModal === 'function') {
actions.push({ label: '🔄 Доработка', handler: () => openReworkModal(taskId), primary_task: true });
}
if (typeof openChangeDeadlineModal === 'function') {
actions.push({ label: '📅 Изменить срок', handler: () => openChangeDeadlineModal(taskId), primary_task: true });
}
}
if (!isDeleted && !isClosed && task.task_type == 'regular') {
if (typeof closeTask === 'function') {
actions.push({ label: '🔒 Закрыть задачу', handler: () => closeTask(taskId), admin: true });
}
}
if (canEdit && !isDeleted && !isClosed && currentUser.role === 'admin') {
if (typeof deleteTask === 'function') actions.push({ label: '🗑️ Удалить', handler: () => deleteTask(taskId) });
}
if (currentUser && currentUser.login === 'minicrm') {
if (typeof closeTask === 'function') actions.push({ label: '🔒 Закрыть задачу', handler: () => closeTask(taskId) });
if (canEdit && !isDeleted && !isClosed && currentUser.role === 'admin') {
if (typeof deleteTask === 'function') actions.push({ label: '🗑️ Удалить', handler: () => deleteTask(taskId) });
}
if (isClosed && canEdit) {
if (typeof reopenTask === 'function') actions.push({ label: '🔓 Открыть задачу', handler: () => reopenTask(taskId) });
}
if (isDeleted && window.currentUser?.role === 'admin') {
if (typeof restoreTask === 'function') actions.push({ label: '↶ Восстановить', handler: () => restoreTask(taskId) });
}
}
return actions;
}
// Рендеринг кнопки меню
window.renderTaskActions = function(task) {
if (!task) return '';
return `
<div class="task-actions-menu-container">
<button class="task-actions-menu-btn" data-task-id="${task.id}">⋮ Действия</button>
</div>
`;
};
// Удалить модальное окно, если оно есть
function removeModal() {
const existing = document.getElementById('task-action-modal');
if (existing) existing.remove();
}
// Обработчик клика на кнопку меню
document.addEventListener('click', function(e) {
const menuBtn = e.target.closest('.task-actions-menu-btn');
if (!menuBtn) return;
e.preventDefault();
e.stopPropagation();
removeModal();
const taskId = menuBtn.dataset.taskId;
if (!taskId) {
console.error('No task id found on button');
return;
}
const task = window.tasks?.find(t => t.id == taskId);
if (!task) {
console.error('Task not found for id', taskId);
return;
}
const actions = buildActionsForTask(task);
if (actions.length === 0) return;
const primaryActions = actions.filter(a => a.primary);
const adminActions = actions.filter(a => a.admin && !a.primary);
const taskActions = actions.filter(a => a.primary_task && !a.primary && !a.primary);
const userActions = actions.filter(a => !a.primary && !a.admin && !a.primary_task);
const overlay = document.createElement('div');
overlay.id = 'task-action-modal';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
overlay.style.zIndex = '10000';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
const modal = document.createElement('div');
modal.style.backgroundColor = 'white';
modal.style.borderRadius = '12px';
modal.style.padding = '25px';
modal.style.maxWidth = '500px';
modal.style.width = '90%';
modal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.2)';
modal.style.position = 'relative';
modal.style.maxHeight = '85vh';
modal.style.overflowY = 'auto';
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.marginBottom = '20px';
header.style.paddingBottom = '10px';
header.style.borderBottom = '1px solid #eee';
const title = document.createElement('h3');
title.style.margin = '0';
title.style.fontSize = '18px';
title.style.color = '#333';
title.style.flex = '1';
title.style.textAlign = 'center';
title.textContent = `Действия для задачи #${taskId}`;
const closeBtn = document.createElement('span');
closeBtn.innerHTML = '&times;';
closeBtn.style.fontSize = '28px';
closeBtn.style.fontWeight = 'bold';
closeBtn.style.cursor = 'pointer';
closeBtn.style.color = '#999';
closeBtn.style.transition = 'color 0.2s';
closeBtn.onmouseover = () => closeBtn.style.color = '#e74c3c';
closeBtn.onmouseout = () => closeBtn.style.color = '#999';
closeBtn.onclick = () => removeModal();
header.appendChild(title);
header.appendChild(closeBtn);
modal.appendChild(header);
// БЛОК: primary actions
if (primaryActions.length > 0) {
const primaryContainer = document.createElement('div');
primaryContainer.style.marginBottom = '20px';
primaryActions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
btn.style.cssText = `
width: 100%;
padding: 14px;
background-color: #27ae60;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 10px;
`;
btn.onmouseover = () => { btn.style.backgroundColor = '#229954'; };
btn.onmouseout = () => { btn.style.backgroundColor = '#27ae60'; };
btn.onclick = (event) => {
event.stopPropagation();
a.handler();
removeModal();
};
primaryContainer.appendChild(btn);
});
modal.appendChild(primaryContainer);
}
// БЛОК: taskActions
if (taskActions.length > 0) {
const taskContainer = document.createElement('div');
taskContainer.style.marginBottom = '20px';
taskActions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
btn.style.cssText = `
width: 100%;
padding: 14px;
background-color: #dfc63cff;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 10px;
`;
btn.onmouseover = () => btn.style.backgroundColor = '#dfc63cff';
btn.onmouseout = () => btn.style.backgroundColor = '#dfc63cff';
btn.onclick = (event) => {
event.stopPropagation();
a.handler();
removeModal();
};
taskContainer.appendChild(btn);
});
modal.appendChild(taskContainer);
}
// БЛОК: admin
if (adminActions.length > 0) {
const adminContainer = document.createElement('div');
adminContainer.style.marginBottom = '20px';
adminActions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
btn.style.cssText = `
width: 100%;
padding: 14px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 10px;
`;
btn.onmouseover = () => btn.style.backgroundColor = '#c0392b';
btn.onmouseout = () => btn.style.backgroundColor = '#e74c3c';
btn.onclick = (event) => {
event.stopPropagation();
a.handler();
removeModal();
};
adminContainer.appendChild(btn);
});
modal.appendChild(adminContainer);
}
// БЛОК: остальные (user)
if (userActions.length > 0) {
const grid = document.createElement('div');
grid.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
`;
userActions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
btn.style.cssText = `
flex: 0 0 calc(33.333% - 10px);
min-width: 120px;
padding: 12px 8px;
border: none;
border-radius: 8px;
background-color: #3498db;
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
`;
btn.onmouseover = () => btn.style.backgroundColor = '#2980b9';
btn.onmouseout = () => btn.style.backgroundColor = '#3498db';
btn.onclick = (event) => {
event.stopPropagation();
a.handler();
removeModal();
};
grid.appendChild(btn);
});
modal.appendChild(grid);
}
overlay.appendChild(modal);
document.body.appendChild(overlay);
overlay.addEventListener('click', function(event) {
if (event.target === overlay) {
removeModal();
}
});
});
// Экспортируем функции для использования в HTML (например, closeDocFieldsModal)
window.closeDocFieldsModal = function() {
const m = document.getElementById('doc-fields-modal');
if (m) m.remove();
};
//console.log('nav-task-actions.js loaded (centered modal)');
})();