отчет своё

This commit is contained in:
2026-03-19 15:10:24 +05:00
parent 440b330900
commit 95068742f4
3 changed files with 149 additions and 50 deletions

View File

@@ -335,15 +335,16 @@
</form> </form>
</div> </div>
</section> </section>
<!-- Секция отчётов (обновлена) -->
<section id="reports-section" class="section"> <section id="reports-section" class="section">
<h2><i class="fas fa-chart-pie"></i> Отчёт по задачам</h2> <h2><i class="fas fa-chart-pie"></i> Отчёт по задачам</h2>
<div class="reports-filters"> <div class="reports-filters">
<div class="filter-group"> <div class="filter-group">
<label for="report-user-filter">Исполнитель:</label> <label for="report-task-id-filter">Номер задачи:</label>
<select id="report-user-filter" onchange="applyFilters()"> <select id="report-task-id-filter" onchange="applyFilters()">
<option value="">Все пользователи</option> <option value="">Все номера</option>
<!-- будут загружены динамически -->
</select> </select>
</div> </div>
<div class="filter-group"> <div class="filter-group">
@@ -358,6 +359,12 @@
<option value="deleted">Удалена</option> <option value="deleted">Удалена</option>
</select> </select>
</div> </div>
<div class="filter-group">
<label for="report-user-filter">Исполнитель:</label>
<select id="report-user-filter" onchange="applyFilters()">
<option value="">Все пользователи</option>
</select>
</div>
<div class="filter-group"> <div class="filter-group">
<label for="report-type-filter">Тип задачи:</label> <label for="report-type-filter">Тип задачи:</label>
<select id="report-type-filter" onchange="applyFilters()"> <select id="report-type-filter" onchange="applyFilters()">
@@ -373,12 +380,15 @@
<option value="e_journal">Эл. журнал</option> <option value="e_journal">Эл. журнал</option>
</select> </select>
</div> </div>
<div class="filter-group"> <div class="filter-group buttons-group">
<button class="btn-primary" onclick="printReport()"> <button class="btn-primary" onclick="printReport()" title="Печать">
<i class="fas fa-print"></i> Печать <i class="fas fa-print"></i> Печать
</button> </button> <!--
<button class="btn-secondary" onclick="loadReportData()"> <button class="btn-secondary" onclick="loadReportData()" title="Обновить данные">
<i class="fas fa-sync-alt"></i> Обновить <i class="fas fa-sync-alt"></i> Обновить
</button> -->
<button class="btn-primary" onclick="resetReportFilters()" title="Сбросить все фильтры">
<i class="fas fa-undo-alt"></i> Сбросить
</button> </button>
</div> </div>
</div> </div>
@@ -386,14 +396,13 @@
<!-- Сводка по статусам --> <!-- Сводка по статусам -->
<div id="report-summary" class="report-summary"></div> <div id="report-summary" class="report-summary"></div>
<!-- Таблица с задачами --> <!-- Таблица с задачами (без описания) -->
<div class="table-container report-table-container"> <div class="table-container report-table-container">
<table id="report-table" class="report-table"> <table id="report-table" class="report-table">
<thead> <thead>
<tr> <tr>
<th>№ задачи</th> <th>№ задачи</th>
<th>Название</th> <th>Название</th>
<th>Описание</th>
<th>Срок выполнения</th> <th>Срок выполнения</th>
<th>Исполнитель</th> <th>Исполнитель</th>
<th>Автор</th> <th>Автор</th>
@@ -402,7 +411,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="report-table-body"> <tbody id="report-table-body">
<tr><td colspan="8" class="loading">Загрузка данных...</td></tr> <tr><td colspan="7" class="loading">Загрузка данных...</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -410,7 +419,7 @@
</main> </main>
</div> </div>
<!-- Модальное окно редактирования задачи --> <!-- Модальные окна (без изменений) -->
<div id="edit-task-modal" class="modal"> <div id="edit-task-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span> <span class="close" onclick="closeEditModal()">&times;</span>
@@ -421,16 +430,13 @@
<label for="edit-title">Название задачи:</label> <label for="edit-title">Название задачи:</label>
<input type="text" id="edit-title" name="title" required> <input type="text" id="edit-title" name="title" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-description">Описание:</label> <label for="edit-description">Описание:</label>
<textarea id="edit-description" name="description" rows="4"></textarea> <textarea id="edit-description" name="description" rows="4"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-due-date">Дата выполнения:</label> <label for="edit-due-date">Дата выполнения:</label>
<input type="date" id="edit-due-date" name="dueDate" required> <input type="date" id="edit-due-date" name="dueDate" required>
<div class="time-buttons"> <div class="time-buttons">
<button type="button" class="edit-time-btn" onclick="setEditTaskTime('12:00')"> <button type="button" class="edit-time-btn" onclick="setEditTaskTime('12:00')">
<i class="fas fa-sun"></i> До обеда (12:00) <i class="fas fa-sun"></i> До обеда (12:00)
@@ -441,22 +447,18 @@
</div> </div>
<input type="hidden" id="edit-due-time" name="dueTime" value="12:00"> <input type="hidden" id="edit-due-time" name="dueTime" value="12:00">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Исполнители:</label> <label>Исполнители:</label>
<div class="user-search"> <div class="user-search">
<div id="edit-users-checklist" class="checkbox-group"></div> <div id="edit-users-checklist" class="checkbox-group"></div>
<input type="text" id="edit-user-search" placeholder="Поиск исполнителей..." oninput="filterEditUsers()"> <input type="text" id="edit-user-search" placeholder="Поиск исполнителей..." oninput="filterEditUsers()">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-files">Добавить файлы:</label> <label for="edit-files">Добавить файлы:</label>
<input type="file" id="edit-files" name="files" multiple> <input type="file" id="edit-files" name="files" multiple>
<div id="edit-file-list"></div> <div id="edit-file-list"></div>
</div> </div>
<button type="submit" class="btn-primary"> <button type="submit" class="btn-primary">
<i class="fas fa-save"></i> Сохранить изменения <i class="fas fa-save"></i> Сохранить изменения
</button> </button>
@@ -464,21 +466,18 @@
</div> </div>
</div> </div>
<!-- Модальное окно копирования задачи -->
<div id="copy-task-modal" class="modal"> <div id="copy-task-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeCopyModal()">&times;</span> <span class="close" onclick="closeCopyModal()">&times;</span>
<h3><i class="fas fa-copy"></i> Создать копию задачи</h3> <h3><i class="fas fa-copy"></i> Создать копию задачи</h3>
<form id="copy-task-form"> <form id="copy-task-form">
<input type="hidden" id="copy-task-id"> <input type="hidden" id="copy-task-id">
<div class="form-group"> <div class="form-group">
<label for="copy-due-date">Дата выполнения:</label> <label for="copy-due-date">Дата выполнения:</label>
<input type="date" class="date-btn" id="copy-due-date" name="dueDate" required> <input type="date" class="date-btn" id="copy-due-date" name="dueDate" required>
<input type="hidden" id="copy-due-time" name="dueTime" value="19:00"> <input type="hidden" id="copy-due-time" name="dueTime" value="19:00">
<input type="text" id="copy-user-search" placeholder="Поиск исполнителей..." oninput="filterCopyUsers()"> <input type="text" id="copy-user-search" placeholder="Поиск исполнителей..." oninput="filterCopyUsers()">
</div> </div>
<div class="form-group"> <div class="form-group">
<div id="copy-users-checklist" class="checkbox-group"></div> <div id="copy-users-checklist" class="checkbox-group"></div>
</div> </div>
@@ -489,7 +488,6 @@
</div> </div>
</div> </div>
<!-- Остальные модальные окна остаются без изменений -->
<div id="edit-assignment-modal" class="modal"> <div id="edit-assignment-modal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeEditAssignmentModal()">&times;</span> <span class="close" onclick="closeEditAssignmentModal()">&times;</span>

View File

@@ -7,9 +7,12 @@ let currentReportFiltered = []; // отфильтрованные данные
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: '', label: 'Все статусы' }, { value: '', label: 'Все статусы' },
{ value: 'assigned', label: 'Назначена' }, { value: 'assigned', label: 'Назначена' },
{ value: 'assigned_overdue', label: 'Назначена (просрочена)' },
{ value: 'in_progress', label: 'В работе' }, { value: 'in_progress', label: 'В работе' },
{ value: 'in_progress_overdue', label: 'В работе (просрочена)' },
{ value: 'completed', label: 'Выполнена' }, { value: 'completed', label: 'Выполнена' },
{ value: 'overdue', label: 'Просрочена' }, { value: 'completed_after_due', label: 'Выполнена после срока' },
{ value: 'overdue', label: 'Просрочена (системная)' },
{ value: 'rework', label: 'На доработке' }, { value: 'rework', label: 'На доработке' },
{ value: 'deleted', label: 'Удалена' } { value: 'deleted', label: 'Удалена' }
]; ];
@@ -29,33 +32,75 @@ const TASK_TYPE_OPTIONS = [
// Функция показа секции отчёта // Функция показа секции отчёта
function showReportsSection() { function showReportsSection() {
// Используем глобальный currentUser из auth.js
if (typeof currentUser === 'undefined' || !currentUser) {
console.error('currentUser не определён');
return;
}
showSection('reports'); showSection('reports');
// Если данные ещё не загружены загружаем
if (reportData.length === 0) { if (reportData.length === 0) {
loadReportData(); loadReportData();
} else { } else {
applyFilters(); // применяем текущие фильтры applyFilters();
} }
} }
// Загрузка данных для отчёта (используем существующее API) // Проверка, имеет ли пользователь право видеть все задачи
function canViewAllTasks() {
if (!currentUser) return false;
// Администратор
if (currentUser.role === 'admin') return true;
// Проверка группы "Руководители" (предполагаем, что группы хранятся в currentUser.groups)
if (currentUser.groups && Array.isArray(currentUser.groups)) {
if (currentUser.groups.some(g => g === 'Руководители' || g.includes('Руководители'))) {
return true;
}
}
return false;
}
// Вычисление уточнённого статуса с учётом просрочки
function computeDisplayStatus(item) {
const now = new Date();
const due = item.due_date ? new Date(item.due_date) : null;
const completedAt = item.status === 'completed' && item.status_updated_at ? new Date(item.status_updated_at) : null;
if (item.status === 'assigned') {
if (due && due < now) return 'assigned_overdue';
return 'assigned';
}
if (item.status === 'in_progress') {
if (due && due < now) return 'in_progress_overdue';
return 'in_progress';
}
if (item.status === 'completed') {
if (due && completedAt && completedAt > due) return 'completed_after_due';
return 'completed';
}
return item.status;
}
// Загрузка данных для отчёта
async function loadReportData() { async function loadReportData() {
try { try {
const tbody = document.getElementById('report-table-body'); const tbody = document.getElementById('report-table-body');
tbody.innerHTML = '<tr><td colspan="8" class="loading">Загрузка данных...</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="loading">Загрузка данных...</td></tr>';
// Загружаем все задачи (или через специальный эндпоинт)
const response = await fetch('/api/tasks?status=all&limit=1000'); const response = await fetch('/api/tasks?status=all&limit=1000');
if (!response.ok) throw new Error('Ошибка загрузки задач'); if (!response.ok) throw new Error('Ошибка загрузки задач');
const tasks = await response.json(); const tasks = await response.json();
// Преобразуем задачи в плоский список назначений
reportData = []; reportData = [];
const canViewAll = canViewAllTasks();
tasks.forEach(task => { tasks.forEach(task => {
// Если не админ и не руководитель, показываем только задачи, где пользователь - автор
if (!canViewAll && task.created_by !== currentUser.id) {
return;
}
if (task.assignments && task.assignments.length) { if (task.assignments && task.assignments.length) {
task.assignments.forEach(ass => { task.assignments.forEach(ass => {
reportData.push({ const item = {
task_id: task.id, task_id: task.id,
task_title: task.title, task_title: task.title,
task_description: task.description || '', task_description: task.description || '',
@@ -65,22 +110,23 @@ async function loadReportData() {
user_name: ass.user_name, user_name: ass.user_name,
status: ass.status, status: ass.status,
status_updated_at: ass.updated_at || task.updated_at, status_updated_at: ass.updated_at || task.updated_at,
creator_name: task.creator_name creator_name: task.creator_name,
}); created_by: task.created_by
};
item.displayStatus = computeDisplayStatus(item);
reportData.push(item);
}); });
} }
}); });
// Заполняем фильтр пользователей
populateUserFilter(reportData); populateUserFilter(reportData);
populateTaskIdFilter(reportData);
// Применяем фильтры и отображаем
applyFilters(); applyFilters();
} catch (error) { } catch (error) {
console.error('Ошибка загрузки отчёта:', error); console.error('Ошибка загрузки отчёта:', error);
document.getElementById('report-table-body').innerHTML = document.getElementById('report-table-body').innerHTML =
'<tr><td colspan="8" class="error">Ошибка загрузки данных</td></tr>'; '<tr><td colspan="7" class="error">Ошибка загрузки данных</td></tr>';
} }
} }
@@ -89,13 +135,11 @@ function populateUserFilter(data) {
const select = document.getElementById('report-user-filter'); const select = document.getElementById('report-user-filter');
const usersMap = new Map(); const usersMap = new Map();
data.forEach(item => { data.forEach(item => {
// Убедимся, что имя не null
const name = item.user_name || 'Без имени'; const name = item.user_name || 'Без имени';
usersMap.set(item.user_id, name); usersMap.set(item.user_id, name);
}); });
let options = '<option value="">Все пользователи</option>'; let options = '<option value="">Все пользователи</option>';
// Сортируем по имени, защищая от null/undefined
const sorted = Array.from(usersMap.entries()).sort((a, b) => { const sorted = Array.from(usersMap.entries()).sort((a, b) => {
const nameA = a[1] || ''; const nameA = a[1] || '';
const nameB = b[1] || ''; const nameB = b[1] || '';
@@ -107,44 +151,72 @@ function populateUserFilter(data) {
select.innerHTML = options; select.innerHTML = options;
} }
// Заполнение выпадающего списка номеров задач (по убыванию)
function populateTaskIdFilter(data) {
const select = document.getElementById('report-task-id-filter');
const taskIds = [...new Set(data.map(item => item.task_id))];
taskIds.sort((a, b) => b - a);
let options = '<option value="">Все номера</option>';
taskIds.forEach(id => {
options += `<option value="${id}">${id}</option>`;
});
select.innerHTML = options;
}
// Применение всех фильтров и рендеринг // Применение всех фильтров и рендеринг
function applyFilters() { function applyFilters() {
const userId = document.getElementById('report-user-filter').value; const userId = document.getElementById('report-user-filter').value;
const statusFilter = document.getElementById('report-status-filter').value; const statusFilter = document.getElementById('report-status-filter').value;
const typeFilter = document.getElementById('report-type-filter').value; const typeFilter = document.getElementById('report-type-filter').value;
const taskIdFilter = document.getElementById('report-task-id-filter').value;
currentReportFiltered = reportData.filter(item => { currentReportFiltered = reportData.filter(item => {
if (userId && item.user_id != userId) return false; if (userId && item.user_id != userId) return false;
if (statusFilter && item.status !== statusFilter) return false;
if (typeFilter && item.task_type !== typeFilter) return false; if (typeFilter && item.task_type !== typeFilter) return false;
if (taskIdFilter && item.task_id != taskIdFilter) return false;
if (statusFilter) {
if (item.displayStatus !== statusFilter) return false;
}
return true; return true;
}); });
renderReport(currentReportFiltered); renderReport(currentReportFiltered);
} }
// Сброс всех фильтров
function resetReportFilters() {
document.getElementById('report-user-filter').value = '';
document.getElementById('report-status-filter').value = '';
document.getElementById('report-type-filter').value = '';
document.getElementById('report-task-id-filter').value = '';
applyFilters();
}
// Рендеринг таблицы и сводки // Рендеринг таблицы и сводки
function renderReport(data) { function renderReport(data) {
const tbody = document.getElementById('report-table-body'); const tbody = document.getElementById('report-table-body');
const summaryDiv = document.getElementById('report-summary'); const summaryDiv = document.getElementById('report-summary');
if (!data.length) { if (!data.length) {
tbody.innerHTML = '<tr><td colspan="8" class="no-data">Нет данных для отображения</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="no-data">Нет данных для отображения</td></tr>';
summaryDiv.innerHTML = ''; summaryDiv.innerHTML = '';
return; return;
} }
// Сводка по статусам
const statusCounts = {}; const statusCounts = {};
data.forEach(item => { data.forEach(item => {
statusCounts[item.status] = (statusCounts[item.status] || 0) + 1; statusCounts[item.displayStatus] = (statusCounts[item.displayStatus] || 0) + 1;
}); });
const statusLabels = { const statusLabels = {
'assigned': 'Назначена', 'assigned': 'Назначена',
'assigned_overdue': 'Назначена (просрочена)',
'in_progress': 'В работе', 'in_progress': 'В работе',
'in_progress_overdue': 'В работе (просрочена)',
'completed': 'Выполнена', 'completed': 'Выполнена',
'overdue': 'Просрочена', 'completed_after_due': 'Выполнена после срока',
'overdue': 'Просрочена (системная)',
'rework': 'На доработке', 'rework': 'На доработке',
'deleted': 'Удалена' 'deleted': 'Удалена'
}; };
@@ -160,16 +232,14 @@ function renderReport(data) {
} }
summaryDiv.innerHTML = summaryHtml; summaryDiv.innerHTML = summaryHtml;
// Таблица детальных записей
tbody.innerHTML = data.map(item => ` tbody.innerHTML = data.map(item => `
<tr> <tr>
<td>${item.task_id}</td> <td>${item.task_id}</td>
<td>${escapeHtml(item.task_title)}</td> <td>${escapeHtml(item.task_title)}</td>
<td class="task-description" title="${escapeHtml(item.task_description)}">${escapeHtml(truncateText(item.task_description, 50))}</td>
<td>${formatDateTime(item.due_date) || '—'}</td> <td>${formatDateTime(item.due_date) || '—'}</td>
<td>${escapeHtml(item.user_name || 'Неизвестно')}</td> <td>${escapeHtml(item.user_name || 'Неизвестно')}</td>
<td>${escapeHtml(item.creator_name || 'Неизвестно')}</td> <td>${escapeHtml(item.creator_name || 'Неизвестно')}</td>
<td><span class="status-badge status-${item.status}">${statusLabels[item.status] || item.status}</span></td> <td><span class="status-badge status-${item.displayStatus}">${statusLabels[item.displayStatus] || item.displayStatus}</span></td>
<td>${formatDateTime(item.status_updated_at) || '—'}</td> <td>${formatDateTime(item.status_updated_at) || '—'}</td>
</tr> </tr>
`).join(''); `).join('');
@@ -200,13 +270,13 @@ function formatDateTime(dateStr) {
}); });
} }
// Печать отчёта
function printReport() { function printReport() {
window.print(); window.print();
} }
// Экспортируем функции // Экспорт
window.showReportsSection = showReportsSection; window.showReportsSection = showReportsSection;
window.loadReportData = loadReportData; window.loadReportData = loadReportData;
window.applyFilters = applyFilters; window.applyFilters = applyFilters;
window.resetReportFilters = resetReportFilters;
window.printReport = printReport; window.printReport = printReport;

View File

@@ -5170,4 +5170,35 @@ button.btn-primary {
.status-completed { background: #28a745; color: white; } .status-completed { background: #28a745; color: white; }
.status-overdue { background: #dc3545; color: white; } .status-overdue { background: #dc3545; color: white; }
.status-rework { background: #fd7e14; color: white; } .status-rework { background: #fd7e14; color: white; }
.status-deleted { background: #6c757d; color: white; } .status-deleted { background: #6c757d; color: white; }
.reports-filters {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
margin-bottom: 20px;
}
.filter-group {
min-width: 150px;
}
.filter-group.buttons-group {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.btn-reset {
background: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn-reset:hover {
background: #5a6268;
}