отчет
This commit is contained in:
@@ -111,7 +111,8 @@ function reloadAllScripts() {
|
||||
'chat-ui.js',
|
||||
'loadMyCreatedTasks.js',
|
||||
'main.js',
|
||||
'nav-task-actions.js'
|
||||
'nav-task-actions.js',
|
||||
'reports.js'
|
||||
];
|
||||
|
||||
// Удаляем существующие скрипты
|
||||
|
||||
@@ -335,6 +335,78 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section id="reports-section" class="section">
|
||||
<h2><i class="fas fa-chart-pie"></i> Отчёт по задачам</h2>
|
||||
|
||||
<div class="reports-filters">
|
||||
<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">
|
||||
<label for="report-status-filter">Статус:</label>
|
||||
<select id="report-status-filter" onchange="applyFilters()">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="assigned">Назначена</option>
|
||||
<option value="in_progress">В работе</option>
|
||||
<option value="completed">Выполнена</option>
|
||||
<option value="overdue">Просрочена</option>
|
||||
<option value="rework">На доработке</option>
|
||||
<option value="deleted">Удалена</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="report-type-filter">Тип задачи:</label>
|
||||
<select id="report-type-filter" onchange="applyFilters()">
|
||||
<option value="">Все типы</option>
|
||||
<option value="regular">Обычная задача</option>
|
||||
<option value="document">Согласование документа</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>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<button class="btn-primary" onclick="printReport()">
|
||||
<i class="fas fa-print"></i> Печать
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="loadReportData()">
|
||||
<i class="fas fa-sync-alt"></i> Обновить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сводка по статусам -->
|
||||
<div id="report-summary" class="report-summary"></div>
|
||||
|
||||
<!-- Таблица с задачами -->
|
||||
<div class="table-container report-table-container">
|
||||
<table id="report-table" class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>№ задачи</th>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Срок выполнения</th>
|
||||
<th>Исполнитель</th>
|
||||
<th>Автор</th>
|
||||
<th>Статус исполнителя</th>
|
||||
<th>Последнее изменение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="report-table-body">
|
||||
<tr><td colspan="8" class="loading">Загрузка данных...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -566,6 +566,11 @@ function showSection(sectionName) {
|
||||
loadUserProfile();
|
||||
loadNotificationSettings();
|
||||
}
|
||||
if (sectionName === 'reports') {
|
||||
if (typeof loadReportData === 'function') {
|
||||
loadReportData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для отображения Канбан доски
|
||||
|
||||
@@ -138,6 +138,16 @@ if (currentUser && currentUser.role === 'admin') {
|
||||
id: "admin-btn"
|
||||
});
|
||||
}
|
||||
// Отчеты
|
||||
if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'tasks')) {
|
||||
navButtons.push({
|
||||
onclick: "showReportsSection()",
|
||||
className: "nav-btn reports",
|
||||
icon: "fas fa-chart-pie",
|
||||
text: "Отчёты",
|
||||
id: "reports-btn"
|
||||
});
|
||||
}
|
||||
// Кнопка выхода
|
||||
navButtons.push({
|
||||
onclick: "logout()",
|
||||
|
||||
206
public/reports.js
Normal file
206
public/reports.js
Normal file
@@ -0,0 +1,206 @@
|
||||
// reports.js – Отчёт по задачам с фильтрацией и группировкой
|
||||
|
||||
let reportData = []; // все назначения для отчёта
|
||||
let currentReportFiltered = []; // отфильтрованные данные
|
||||
|
||||
// Конфигурация статусов и типов для фильтров
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: 'Все статусы' },
|
||||
{ value: 'assigned', label: 'Назначена' },
|
||||
{ value: 'in_progress', label: 'В работе' },
|
||||
{ value: 'completed', label: 'Выполнена' },
|
||||
{ value: 'overdue', label: 'Просрочена' },
|
||||
{ value: 'rework', label: 'На доработке' },
|
||||
{ value: 'deleted', label: 'Удалена' }
|
||||
];
|
||||
|
||||
const TASK_TYPE_OPTIONS = [
|
||||
{ value: '', label: 'Все типы' },
|
||||
{ value: 'regular', label: 'Обычная задача' },
|
||||
{ value: 'document', label: 'Согласование документа' },
|
||||
{ value: 'it', label: 'ИТ отдел' },
|
||||
{ value: 'ahch', label: 'АХЧ' },
|
||||
{ value: 'psychologist', label: 'Психолог' },
|
||||
{ value: 'speech_therapist', label: 'Логопед' },
|
||||
{ value: 'hr', label: 'Диспетчер расписания' },
|
||||
{ value: 'certificate', label: 'Справка' },
|
||||
{ value: 'e_journal', label: 'Эл. журнал' }
|
||||
];
|
||||
|
||||
// Функция показа секции отчёта
|
||||
function showReportsSection() {
|
||||
showSection('reports');
|
||||
// Если данные ещё не загружены – загружаем
|
||||
if (reportData.length === 0) {
|
||||
loadReportData();
|
||||
} else {
|
||||
applyFilters(); // применяем текущие фильтры
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка данных для отчёта (используем существующее API)
|
||||
async function loadReportData() {
|
||||
try {
|
||||
const tbody = document.getElementById('report-table-body');
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="loading">Загрузка данных...</td></tr>';
|
||||
|
||||
// Загружаем все задачи (или через специальный эндпоинт)
|
||||
const response = await fetch('/api/tasks?status=all&limit=1000');
|
||||
if (!response.ok) throw new Error('Ошибка загрузки задач');
|
||||
|
||||
const tasks = await response.json();
|
||||
|
||||
// Преобразуем задачи в плоский список назначений
|
||||
reportData = [];
|
||||
tasks.forEach(task => {
|
||||
if (task.assignments && task.assignments.length) {
|
||||
task.assignments.forEach(ass => {
|
||||
reportData.push({
|
||||
task_id: task.id,
|
||||
task_title: task.title,
|
||||
task_description: task.description || '',
|
||||
task_type: task.task_type || 'regular',
|
||||
due_date: task.due_date,
|
||||
user_id: ass.user_id,
|
||||
user_name: ass.user_name,
|
||||
status: ass.status,
|
||||
status_updated_at: ass.updated_at || task.updated_at,
|
||||
creator_name: task.creator_name
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Заполняем фильтр пользователей
|
||||
populateUserFilter(reportData);
|
||||
|
||||
// Применяем фильтры и отображаем
|
||||
applyFilters();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки отчёта:', error);
|
||||
document.getElementById('report-table-body').innerHTML =
|
||||
'<tr><td colspan="8" class="error">Ошибка загрузки данных</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Заполнение выпадающего списка пользователей
|
||||
function populateUserFilter(data) {
|
||||
const select = document.getElementById('report-user-filter');
|
||||
const usersMap = new Map();
|
||||
data.forEach(item => {
|
||||
usersMap.set(item.user_id, item.user_name);
|
||||
});
|
||||
|
||||
let options = '<option value="">Все пользователи</option>';
|
||||
// Сортируем по имени
|
||||
const sorted = Array.from(usersMap.entries()).sort((a, b) => a[1].localeCompare(b[1]));
|
||||
for (let [id, name] of sorted) {
|
||||
options += `<option value="${id}">${escapeHtml(name)}</option>`;
|
||||
}
|
||||
select.innerHTML = options;
|
||||
}
|
||||
|
||||
// Применение всех фильтров и рендеринг
|
||||
function applyFilters() {
|
||||
const userId = document.getElementById('report-user-filter').value;
|
||||
const statusFilter = document.getElementById('report-status-filter').value;
|
||||
const typeFilter = document.getElementById('report-type-filter').value;
|
||||
|
||||
currentReportFiltered = reportData.filter(item => {
|
||||
if (userId && item.user_id != userId) return false;
|
||||
if (statusFilter && item.status !== statusFilter) return false;
|
||||
if (typeFilter && item.task_type !== typeFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
renderReport(currentReportFiltered);
|
||||
}
|
||||
|
||||
// Рендеринг таблицы и сводки
|
||||
function renderReport(data) {
|
||||
const tbody = document.getElementById('report-table-body');
|
||||
const summaryDiv = document.getElementById('report-summary');
|
||||
|
||||
if (!data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="no-data">Нет данных для отображения</td></tr>';
|
||||
summaryDiv.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Сводка по статусам
|
||||
const statusCounts = {};
|
||||
data.forEach(item => {
|
||||
statusCounts[item.status] = (statusCounts[item.status] || 0) + 1;
|
||||
});
|
||||
|
||||
const statusLabels = {
|
||||
'assigned': 'Назначена',
|
||||
'in_progress': 'В работе',
|
||||
'completed': 'Выполнена',
|
||||
'overdue': 'Просрочена',
|
||||
'rework': 'На доработке',
|
||||
'deleted': 'Удалена'
|
||||
};
|
||||
|
||||
let summaryHtml = '';
|
||||
for (let [status, count] of Object.entries(statusCounts)) {
|
||||
summaryHtml += `
|
||||
<div class="summary-item">
|
||||
<span class="status-badge">${count}</span>
|
||||
<span class="status-label">${statusLabels[status] || status}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
summaryDiv.innerHTML = summaryHtml;
|
||||
|
||||
// Таблица детальных записей
|
||||
tbody.innerHTML = data.map(item => `
|
||||
<tr>
|
||||
<td>${item.task_id}</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>${escapeHtml(item.user_name)}</td>
|
||||
<td>${escapeHtml(item.creator_name)}</td>
|
||||
<td><span class="status-badge status-${item.status}">${statusLabels[item.status] || item.status}</span></td>
|
||||
<td>${formatDateTime(item.status_updated_at) || '—'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Вспомогательные функции
|
||||
function truncateText(text, maxLen) {
|
||||
if (!text) return '';
|
||||
return text.length > maxLen ? text.substr(0, maxLen) + '…' : text;
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
if (!unsafe) return '';
|
||||
return String(unsafe)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString('ru-RU', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Печать отчёта
|
||||
function printReport() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
// Экспортируем функции
|
||||
window.showReportsSection = showReportsSection;
|
||||
window.loadReportData = loadReportData;
|
||||
window.applyFilters = applyFilters;
|
||||
window.printReport = printReport;
|
||||
103
public/style.css
103
public/style.css
@@ -5069,4 +5069,105 @@ button.btn-primary {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
.nav-btn.reports {
|
||||
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
||||
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
|
||||
}
|
||||
.nav-btn.reports:hover {
|
||||
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4);
|
||||
}
|
||||
.reports-filters {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.report-summary {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.summary-item {
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.summary-item .status-badge {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.summary-item .status-label {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.report-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.report-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
.report-table th {
|
||||
background: #f1f3f5;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
.report-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.report-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.report-table .task-description {
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Печатные стили */
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
#reports-section, #reports-section * {
|
||||
visibility: visible;
|
||||
}
|
||||
#reports-section {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
}
|
||||
.reports-filters, .summary-item, .btn-primary, .btn-secondary {
|
||||
display: none !important;
|
||||
}
|
||||
.report-table th {
|
||||
background: #f0f0f0 !important;
|
||||
color: black !important;
|
||||
}
|
||||
.report-table td {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
.status-assigned { background: #ffc107; color: #212529; }
|
||||
.status-in_progress { background: #17a2b8; color: white; }
|
||||
.status-completed { background: #28a745; color: white; }
|
||||
.status-overdue { background: #dc3545; color: white; }
|
||||
.status-rework { background: #fd7e14; color: white; }
|
||||
.status-deleted { background: #6c757d; color: white; }
|
||||
Reference in New Issue
Block a user