ознакомление

This commit is contained in:
2026-03-26 17:20:04 +05:00
parent 3a866762a5
commit f5f4f12ff1
9 changed files with 360 additions and 7 deletions

View File

@@ -108,6 +108,7 @@
<option value="">Все типы</option> <option value="">Все типы</option>
<option value="regular">Обычная задача</option> <option value="regular">Обычная задача</option>
<option value="document">Согласование документа</option> <option value="document">Согласование документа</option>
<option value="acquaintance">Ознакомление</option>
<option value="it">ИТ отдел</option> <option value="it">ИТ отдел</option>
<option value="ahch">АХЧ</option> <option value="ahch">АХЧ</option>
<option value="psychologist">Психолог</option> <option value="psychologist">Психолог</option>
@@ -162,6 +163,7 @@
<div class="task-type-selector"> <div class="task-type-selector">
<div class="task-type-buttons"> <div class="task-type-buttons">
<button type="button" class="task-type-btn active" data-type="regular" onclick="selectTaskType('regular')"><i class="fas fa-tasks"></i> Обычная задача</button> <button type="button" class="task-type-btn active" data-type="regular" onclick="selectTaskType('regular')"><i class="fas fa-tasks"></i> Обычная задача</button>
<button type="button" class="task-type-btn" data-type="acquaintance" onclick="selectTaskType('acquaintance')"><i class="fas fa-eye"></i> Ознакомление</button>
<button type="button" class="task-type-btn" data-type="document" onclick="selectTaskType('document')"><i class="fas fa-file-signature"></i> Согласование документа</button> <button type="button" class="task-type-btn" data-type="document" onclick="selectTaskType('document')"><i class="fas fa-file-signature"></i> Согласование документа</button>
<button type="button" class="task-type-btn" data-type="it" onclick="selectTaskType('it')"><i class="fas fa-desktop"></i> Заявка в ИТ отдел</button> <button type="button" class="task-type-btn" data-type="it" onclick="selectTaskType('it')"><i class="fas fa-desktop"></i> Заявка в ИТ отдел</button>
<button type="button" class="task-type-btn" data-type="ahch" onclick="selectTaskType('ahch')"><i class="fas fa-tools"></i> Заявка в АХЧ</button> <button type="button" class="task-type-btn" data-type="ahch" onclick="selectTaskType('ahch')"><i class="fas fa-tools"></i> Заявка в АХЧ</button>
@@ -522,7 +524,42 @@
</form> </form>
</div> </div>
</div> </div>
<div id="acquaintance-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeAcquaintanceModal()">&times;</span>
<h3>Создать задачу для ознакомления</h3>
<form id="acquaintance-task-form" enctype="multipart/form-data">
<input type="hidden" id="acquaintance-original-task-id">
<div class="form-group">
<label>Исходная задача:</label>
<div id="acquaintance-original-title"></div>
</div>
<div class="form-group">
<label>Автор задачи (выберите из списка):</label>
<div class="user-search">
<input type="text" id="acquaintance-author-search" placeholder="Поиск авторов..." oninput="filterAcquaintanceAuthors()">
</div>
<div id="acquaintance-authors-checklist" class="checkbox-group"></div>
</div>
<div class="form-group">
<label>Исполнитель:</label>
<div id="acquaintance-executor-info" style="padding: 10px; background: #f0f0f0; border-radius: 5px;">
<!-- сюда будет подставлено имя текущего пользователя -->
</div>
</div>
<div class="form-group">
<label for="acquaintance-due-date">Дата выполнения:</label>
<input type="date" id="acquaintance-due-date" name="dueDate" required>
<input type="hidden" id="acquaintance-due-time" name="dueTime" value="19:00">
</div>
<div class="form-group">
<label for="acquaintance-comment">Комментарий (необязательно):</label>
<textarea id="acquaintance-comment" rows="3" placeholder="Добавьте комментарий к задаче ознакомления"></textarea>
</div>
<button type="submit" class="btn-primary">Создать задачу ознакомления</button>
</form>
</div>
</div>
<div id="kanban-section" class="section kanban-section"> <div id="kanban-section" class="section kanban-section">
<div id="kanban-board" class="kanban-board"> <div id="kanban-board" class="kanban-board">
<div class="loading">Загрузка Канбан-доски...</div> <div class="loading">Загрузка Канбан-доски...</div>

View File

@@ -85,6 +85,12 @@ function setupEventListeners() {
notificationForm._hasSubmitListener = true; notificationForm._hasSubmitListener = true;
} }
// Ознакомление
const acquaintanceForm = document.getElementById('acquaintance-task-form');
if (acquaintanceForm && !acquaintanceForm._hasSubmitListener) {
acquaintanceForm.addEventListener('submit', createAcquaintanceTask);
acquaintanceForm._hasSubmitListener = true;
}
// Инициализация загрузки файлов // Инициализация загрузки файлов
initializeFileUploads(); initializeFileUploads();
} }

View File

@@ -131,7 +131,16 @@ if (currentUser && (currentUser.role === 'admin' || (currentUser.role === 'tasks
} }
} }
} }
// Кнопка "Создать ознакомление" для админов и 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 (!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 openReworkModal === 'function') { actions.push({ label: '🔄 Доработка', handler: () => openReworkModal(taskId),primary_task: true});

View File

@@ -27,7 +27,8 @@ const TASK_TYPE_OPTIONS = [
{ value: 'speech_therapist', label: 'Логопед' }, { value: 'speech_therapist', label: 'Логопед' },
{ value: 'hr', label: 'Диспетчер расписания' }, { value: 'hr', label: 'Диспетчер расписания' },
{ value: 'certificate', label: 'Справка' }, { value: 'certificate', label: 'Справка' },
{ value: 'e_journal', label: 'Эл. журнал' } { value: 'e_journal', label: 'Эл. журнал' },
{ value: 'acquaintance', label: 'Ознакомление' }
]; ];
// Функция показа секции отчёта // Функция показа секции отчёта

View File

@@ -5201,4 +5201,8 @@ button.btn-primary {
} }
.btn-reset:hover { .btn-reset:hover {
background: #5a6268; background: #5a6268;
}
.task-type-badge.acquaintance {
background: #3498db; /* или любой подходящий цвет */
color: white;
} }

View File

@@ -628,6 +628,106 @@ function canUserAddFilesToTask(task) {
return false; return false;
} }
// ==================== ФУНКЦИИ ДЛЯ ОЗНАКОМЛЕНИЯ ====================
async function openAcquaintanceModal(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}`);
if (!response.ok) throw new Error('Ошибка загрузки задачи');
const task = await response.json();
// Заполняем модальное окно
document.getElementById('acquaintance-original-task-id').value = task.id;
document.getElementById('acquaintance-original-title').innerHTML = `
<strong>№${task.id}</strong> ${task.title}<br>
<small>Автор: ${task.creator_name}</small>
`;
// Устанавливаем дату выполнения по умолчанию (завтра)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
document.getElementById('acquaintance-due-date').value = tomorrow.toISOString().split('T')[0];
document.getElementById('acquaintance-due-time').value = '19:00';
// Загружаем пользователей для выбора автора
await loadUsers(); // гарантируем, что users загружены
renderAcquaintanceAuthorsChecklist(users);
// Информация об исполнителе (текущий пользователь)
const executorInfo = document.getElementById('acquaintance-executor-info');
if (executorInfo) {
executorInfo.innerHTML = `Исполнитель: ${currentUser.name} (${currentUser.login})`;
}
// Отображаем модальное окно
document.getElementById('acquaintance-task-modal').style.display = 'block';
} catch (error) {
console.error('Ошибка открытия модального окна ознакомления:', error);
alert('Не удалось загрузить задачу');
}
}
function closeAcquaintanceModal() {
const modal = document.getElementById('acquaintance-task-modal');
if (modal) modal.style.display = 'none';
const authorSearch = document.getElementById('acquaintance-author-search');
if (authorSearch) authorSearch.value = '';
const userSearch = document.getElementById('acquaintance-user-search');
if (userSearch) userSearch.value = '';
acquaintanceSelectedUsers = [];
acquaintanceSelectedAuthor = null;
}
async function createAcquaintanceTask(event) {
event.preventDefault();
const originalTaskId = document.getElementById('acquaintance-original-task-id').value;
const dueDate = document.getElementById('acquaintance-due-date').value;
const dueTime = document.getElementById('acquaintance-due-time').value;
const fullDueDateTime = `${dueDate}T${dueTime}:00`;
const comment = document.getElementById('acquaintance-comment').value.trim();
const assignedUserIds = [currentUser.id]; // исполнитель текущий пользователь
const creatorId = document.querySelector('input[name="acquaintance-author"]:checked')?.value;
if (!creatorId) {
alert('Выберите автора задачи');
return;
}
try {
const response = await fetch('/api/tasks/acquaintance', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
originalTaskId,
dueDate: fullDueDateTime,
assignedUserIds,
creatorId,
comment
})
});
const result = await response.json();
if (response.ok) {
alert('Задача ознакомления успешно создана!');
closeAcquaintanceModal();
loadTasks();
loadActivityLogs();
} else {
alert(`Ошибка: ${result.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка создания задачи ознакомления:', error);
alert('Сетевая ошибка');
}
}
// Добавляем отладочную функцию // Добавляем отладочную функцию
function debugDocumentFields() { function debugDocumentFields() {
console.log('=== ОТЛАДКА ПОЛЕЙ ДОКУМЕНТОВ ==='); console.log('=== ОТЛАДКА ПОЛЕЙ ДОКУМЕНТОВ ===');
@@ -647,4 +747,7 @@ function debugDocumentFields() {
window.debugDocumentFields = debugDocumentFields; window.debugDocumentFields = debugDocumentFields;
window.loadTasks = loadTasks; window.loadTasks = loadTasks;
window.updateAssignment = updateAssignment; window.updateAssignment = updateAssignment;
window.renderTasksForActiveSection = renderTasksForActiveSection; window.renderTasksForActiveSection = renderTasksForActiveSection;
window.openAcquaintanceModal = openAcquaintanceModal;
window.closeAcquaintanceModal = closeAcquaintanceModal;
window.createAcquaintanceTask = createAcquaintanceTask;

View File

@@ -1406,7 +1406,8 @@ function getTaskTypeName(type) {
'Social_educator': 'повод для обращения к cоциальному педагогу: ', 'Social_educator': 'повод для обращения к cоциальному педагогу: ',
'hr': 'вопрос к кадровой службе', 'hr': 'вопрос к кадровой службе',
'certificate': 'тип необходимой справки', 'certificate': 'тип необходимой справки',
'e_journal': 'информацию для доступа к журналу' 'e_journal': 'информацию для доступа к журналу',
'acquaintance': 'Ознакомление с документом'
}; };
return typeNames[type] || 'задачу'; return typeNames[type] || 'задачу';
} }
@@ -1422,7 +1423,8 @@ function getTaskTypeDisplayName(type) {
'Social_educator': 'Социальный педагог: ', 'Social_educator': 'Социальный педагог: ',
'hr': 'Кадры', 'hr': 'Кадры',
'certificate': 'Справка', 'certificate': 'Справка',
'e_journal': 'Эл. журнал' 'e_journal': 'Эл. журнал',
'acquaintance': 'Ознакомление'
}; };
return typeNames[type] || type; return typeNames[type] || type;
} }
@@ -1438,7 +1440,8 @@ function getTaskTypeIcon(type) {
'Social_educator': 'fas fa-brain', 'Social_educator': 'fas fa-brain',
'hr': 'fas fa-users', 'hr': 'fas fa-users',
'certificate': 'fas fa-file-certificate', 'certificate': 'fas fa-file-certificate',
'e_journal': 'fas fa-book' 'e_journal': 'fas fa-book',
'acquaintance': 'fas fa-eye'
}; };
return icons[type] || 'fas fa-tasks'; return icons[type] || 'fas fa-tasks';
} }

View File

@@ -7,6 +7,8 @@ let filteredUsers = [];
let selectedUsers = []; let selectedUsers = [];
let editSelectedUsers = []; let editSelectedUsers = [];
let copySelectedUsers = []; let copySelectedUsers = [];
let acquaintanceSelectedUsers = []; // исполнители для ознакомления
let acquaintanceSelectedAuthor = null; // выбранный автор для ознакомления
// Переменные для пользовательских списков // Переменные для пользовательских списков
let userLists = []; let userLists = [];
@@ -662,6 +664,76 @@ async function saveListFromModal() {
closeListModal(); closeListModal();
} }
// ==================== ФУНКЦИИ ДЛЯ ОЗНАКОМЛЕНИЯ (выбор исполнителей) ====================
function renderAcquaintanceUsersChecklist(filtered = users) {
const container = document.getElementById('acquaintance-users-checklist');
if (!container) return;
container.innerHTML = filtered
.filter(user => user.id !== currentUser?.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}"
onchange="toggleAcquaintanceUserSelection(this, ${user.id})"
${acquaintanceSelectedUsers.includes(user.id) ? 'checked' : ''}>
${escapeHtml(user.name)} (${user.login})
</label>
</div>
`).join('');
}
function toggleAcquaintanceUserSelection(checkbox, userId) {
if (checkbox.checked) {
if (!acquaintanceSelectedUsers.includes(userId)) acquaintanceSelectedUsers.push(userId);
} else {
acquaintanceSelectedUsers = acquaintanceSelectedUsers.filter(id => id !== userId);
}
}
async function filterAcquaintanceUsers() {
const search = document.getElementById('acquaintance-user-search')?.value.toLowerCase() || '';
let filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
renderAcquaintanceUsersChecklist(filtered);
}
// ==================== ФУНКЦИИ ДЛЯ ВЫБОРА АВТОРА В ЗАДАЧЕ ОЗНАКОМЛЕНИЯ ====================
function renderAcquaintanceAuthorsChecklist(filtered = users) {
const container = document.getElementById('acquaintance-authors-checklist');
if (!container) return;
container.innerHTML = filtered
.filter(user => user.id !== currentUser?.id) // можно разрешить выбирать себя, если нужно
.map(user => `
<div class="checkbox-item">
<label>
<input type="radio" name="acquaintance-author" value="${user.id}"
onchange="toggleAcquaintanceAuthorSelection(this, ${user.id})"
${acquaintanceSelectedAuthor === user.id ? 'checked' : ''}>
${escapeHtml(user.name)} (${user.login})
</label>
</div>
`).join('');
}
function toggleAcquaintanceAuthorSelection(radio, userId) {
acquaintanceSelectedAuthor = radio.checked ? userId : null;
}
async function filterAcquaintanceAuthors() {
const search = document.getElementById('acquaintance-author-search')?.value.toLowerCase() || '';
let filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
renderAcquaintanceAuthorsChecklist(filtered);
}
// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==================== // ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
function escapeHtml(text) { function escapeHtml(text) {
@@ -705,6 +777,18 @@ window.closeListModal = closeListModal;
window.saveListFromModal = saveListFromModal; window.saveListFromModal = saveListFromModal;
window.filterListUsers = filterListUsers; window.filterListUsers = filterListUsers;
// Экспорт функций для ознакомления (исполнители)
window.renderAcquaintanceUsersChecklist = renderAcquaintanceUsersChecklist;
window.toggleAcquaintanceUserSelection = toggleAcquaintanceUserSelection;
window.filterAcquaintanceUsers = filterAcquaintanceUsers;
window.acquaintanceSelectedUsers = acquaintanceSelectedUsers;
// Экспорт функций для выбора автора
window.renderAcquaintanceAuthorsChecklist = renderAcquaintanceAuthorsChecklist;
window.toggleAcquaintanceAuthorSelection = toggleAcquaintanceAuthorSelection;
window.filterAcquaintanceAuthors = filterAcquaintanceAuthors;
window.acquaintanceSelectedAuthor = acquaintanceSelectedAuthor;
// Также экспортируем переменные, которые могут понадобиться в других скриптах // Также экспортируем переменные, которые могут понадобиться в других скриптах
window.selectedUsers = selectedUsers; window.selectedUsers = selectedUsers;
window.editSelectedUsers = editSelectedUsers; window.editSelectedUsers = editSelectedUsers;

View File

@@ -2056,6 +2056,112 @@ app.post('/api/tasks/:taskId/copy', requireAuth, checkTaskCreationTimeout, (req,
); );
}); });
}); });
// API для создания задачи ознакомления с указанием автора
app.post('/api/tasks/acquaintance', requireAuth, checkTaskCreationTimeout, (req, res) => {
const { originalTaskId, dueDate, assignedUserIds, creatorId, comment } = req.body;
const currentUserId = req.session.user.id;
const currentUser = req.session.user;
// Валидация
if (!originalTaskId) return res.status(400).json({ error: 'Не указана исходная задача' });
if (!dueDate) return res.status(400).json({ error: 'Дата выполнения обязательна' });
if (!assignedUserIds || assignedUserIds.length === 0) return res.status(400).json({ error: 'Не указаны исполнители' });
if (!creatorId) return res.status(400).json({ error: 'Не указан автор задачи' });
// Проверка прав: только admin и tasks могут создавать задачу от имени другого пользователя
if (currentUser.role !== 'admin' && currentUser.role !== 'tasks') {
return res.status(403).json({ error: 'Недостаточно прав для создания задачи от имени другого пользователя' });
}
// Исполнителем должен быть только текущий пользователь
if (assignedUserIds.length !== 1 || parseInt(assignedUserIds[0]) !== currentUserId) {
return res.status(400).json({ error: 'Исполнителем может быть только текущий пользователь' });
}
if (!req.taskCreationCheckPassed) {
return res.status(429).json({ error: 'Слишком частое создание задач' });
}
// Получаем исходную задачу с именем автора
db.get(`
SELECT t.*, u.name as creator_name
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
WHERE t.id = ?
`, [originalTaskId], (err, originalTask) => {
if (err || !originalTask) {
return res.status(404).json({ error: 'Исходная задача не найдена' });
}
const startDate = new Date().toISOString();
const taskType = 'acquaintance';
const newTitle = `Ознакомление: ${originalTask.title}`;
const newDescription = `Задача для ознакомления
Оригинал: задача №${originalTask.id} "${originalTask.title}"
Автор оригинала: ${originalTask.creator_name || 'Неизвестно'}
${comment ? `Комментарий: ${comment}\n` : ''}
---
${originalTask.description || ''}`;
// Создаём задачу с указанным автором (creatorId)
db.run(
`INSERT INTO tasks
(title, description, created_by, original_task_id, start_date, due_date, task_type)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[newTitle, newDescription, creatorId, originalTaskId, startDate, dueDate, taskType],
function(err) {
if (err) {
console.error('❌ Ошибка создания задачи ознакомления:', err);
return res.status(500).json({ error: err.message });
}
const newTaskId = this.lastID;
// Обновляем таймаут создания
if (typeof updateLastTaskCreationTime === 'function') {
updateLastTaskCreationTime(currentUserId);
}
// Сохраняем метаданные
if (typeof saveTaskMetadata === 'function') {
saveTaskMetadata(newTaskId, newTitle, newDescription, creatorId, originalTaskId, startDate, dueDate);
}
// Назначаем исполнителя (текущий пользователь)
db.run(
`INSERT INTO task_assignments (task_id, user_id, start_date, due_date, status)
VALUES (?, ?, ?, ?, 'assigned')`,
[newTaskId, currentUserId, startDate, dueDate],
function(err) {
if (err) {
console.error('❌ Ошибка назначения исполнителя:', err);
db.run("DELETE FROM tasks WHERE id = ?", [newTaskId]);
return res.status(500).json({ error: err.message });
}
if (typeof logActivity === 'function') {
logActivity(newTaskId, currentUserId, 'TASK_CREATED',
`Создана задача ознакомления: ${newTitle} (автор: ${creatorId}, исполнитель: ${currentUserId})`);
}
if (typeof sendTaskNotifications === 'function') {
sendTaskNotifications('created', newTaskId, newTitle, newDescription, currentUserId);
}
res.json({
success: true,
taskId: newTaskId,
message: 'Задача ознакомления успешно создана',
timeoutInfo: { nextAllowedIn: 15 }
});
}
);
}
);
});
});
} }
module.exports = { setupTaskEndpoints, getApproverUsers }; module.exports = { setupTaskEndpoints, getApproverUsers };