Добавил журнал ошибок
This commit is contained in:
@@ -38,6 +38,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:signal_marks' %}">Отметки сигналов</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:errors_report' %}">Журнал ошибок</a>
|
||||
</li>
|
||||
{% if user|has_perm:'kubsat_view' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
||||
|
||||
586
dbapp/mainapp/templates/mainapp/errors_report.html
Normal file
586
dbapp/mainapp/templates/mainapp/errors_report.html
Normal file
@@ -0,0 +1,586 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Журнал ошибок{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.mark-cell {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
width: 60px;
|
||||
}
|
||||
.mark-positive {
|
||||
background-color: #d4edda !important;
|
||||
color: #155724;
|
||||
}
|
||||
.mark-negative {
|
||||
background-color: #f8d7da !important;
|
||||
color: #721c24;
|
||||
}
|
||||
.mark-empty {
|
||||
color: #6c757d;
|
||||
}
|
||||
.downtime-cell {
|
||||
font-size: 0.85em;
|
||||
max-width: 250px;
|
||||
}
|
||||
.table-responsive {
|
||||
max-height: calc(100vh - 250px);
|
||||
overflow: auto;
|
||||
}
|
||||
.table thead th {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
font-size: 0.85em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table thead th.issue-header {
|
||||
padding: 5px;
|
||||
font-size: 0.75em;
|
||||
min-width: 60px;
|
||||
max-width: 100px;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
.filter-panel {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.btn-edit {
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.weekly-hours-cell {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.downtime-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.downtime-item input[type="time"] {
|
||||
width: 110px;
|
||||
}
|
||||
.downtime-item input[type="text"] {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-3">
|
||||
<h4>Журнал ошибок и неисправностей</h4>
|
||||
|
||||
<div class="filter-panel d-flex gap-3 align-items-end flex-wrap">
|
||||
<div>
|
||||
<label class="form-label">Дата с:</label>
|
||||
<input type="date" id="dateFrom" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Дата по:</label>
|
||||
<input type="date" id="dateTo" class="form-control form-control-sm">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="loadData()">
|
||||
Применить
|
||||
</button>
|
||||
{% if user|has_perm:'errors_report_create' %}
|
||||
<button class="btn btn-success btn-sm" onclick="openCreateModal()">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm" id="reportTable">
|
||||
<thead>
|
||||
<tr id="headerRowTop">
|
||||
</tr>
|
||||
<tr id="headerRowBottom">
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reportBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для создания/редактирования -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Новая запись</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editForm">
|
||||
<input type="hidden" id="editId">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Дата</label>
|
||||
<input type="date" id="editDate" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Время работы за день (ч)</label>
|
||||
<input type="number" step="0.01" min="0" id="editDailyHours" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Периоды простоя</label>
|
||||
<div id="editDowntimeList"></div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addDowntimeRow()">
|
||||
<i class="bi bi-plus"></i> Добавить период
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h6>Ошибки</h6>
|
||||
<div id="editErrors"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Неисправности</h6>
|
||||
<div id="editMalfunctions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Пояснение</label>
|
||||
<textarea id="editExplanation" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Комментарий</label>
|
||||
<textarea id="editComment" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% if user|has_perm:'errors_report_delete' %}
|
||||
<button type="button" class="btn btn-danger me-auto" id="btnDelete" onclick="deleteRecord()" style="display: none;">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveRecord()">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
const issueTypes = {
|
||||
errors: [
|
||||
{% for e in errors %}
|
||||
{id: {{ e.id }}, name: "{{ e.name|escapejs }}"},
|
||||
{% endfor %}
|
||||
],
|
||||
malfunctions: [
|
||||
{% for m in malfunctions %}
|
||||
{id: {{ m.id }}, name: "{{ m.name|escapejs }}"},
|
||||
{% endfor %}
|
||||
]
|
||||
};
|
||||
|
||||
let currentData = [];
|
||||
let currentColumns = [];
|
||||
let editModal = null;
|
||||
|
||||
// Инициализация
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
editModal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
loadData();
|
||||
});
|
||||
|
||||
// Загрузка данных
|
||||
async function loadData() {
|
||||
const dateFrom = document.getElementById('dateFrom').value;
|
||||
const dateTo = document.getElementById('dateTo').value;
|
||||
|
||||
let url = '{% url "mainapp:errors_report_api" %}?';
|
||||
if (dateFrom) url += `date_from=${dateFrom}&`;
|
||||
if (dateTo) url += `date_to=${dateTo}&`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
|
||||
currentData = result.data;
|
||||
currentColumns = result.columns;
|
||||
|
||||
renderTable();
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
alert('Ошибка загрузки данных');
|
||||
}
|
||||
}
|
||||
|
||||
// Получить номер ISO недели для даты
|
||||
function getWeekKey(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
// Получаем понедельник этой недели
|
||||
const day = date.getDay();
|
||||
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
|
||||
const monday = new Date(date.setDate(diff));
|
||||
// Формируем ключ: год-номер_недели
|
||||
const year = monday.getFullYear();
|
||||
const startOfYear = new Date(year, 0, 1);
|
||||
const weekNum = Math.ceil(((monday - startOfYear) / 86400000 + startOfYear.getDay() + 1) / 7);
|
||||
return `${year}-W${weekNum}`;
|
||||
}
|
||||
|
||||
// Группировка данных по неделям и расчёт rowspan
|
||||
function calculateWeekGroups(data) {
|
||||
const weekGroups = {};
|
||||
|
||||
// Группируем записи по неделям
|
||||
data.forEach((row, idx) => {
|
||||
const weekKey = getWeekKey(row.date);
|
||||
if (!weekGroups[weekKey]) {
|
||||
weekGroups[weekKey] = {
|
||||
rows: [],
|
||||
totalHours: 0
|
||||
};
|
||||
}
|
||||
weekGroups[weekKey].rows.push(idx);
|
||||
weekGroups[weekKey].totalHours += parseFloat(row.daily_work_hours) || 0;
|
||||
});
|
||||
|
||||
// Определяем для каждой строки: нужно ли рендерить ячейку и с каким rowspan
|
||||
const rowInfo = {};
|
||||
Object.values(weekGroups).forEach(group => {
|
||||
const firstRowIdx = group.rows[0];
|
||||
group.rows.forEach((idx, i) => {
|
||||
rowInfo[idx] = {
|
||||
isFirst: i === 0,
|
||||
rowspan: group.rows.length,
|
||||
weeklyTotal: group.totalHours.toFixed(2)
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return rowInfo;
|
||||
}
|
||||
|
||||
// Рендер таблицы
|
||||
function renderTable() {
|
||||
const errors = currentColumns.filter(c => c.category === 'error');
|
||||
const malfunctions = currentColumns.filter(c => c.category === 'malfunction');
|
||||
|
||||
// Верхний ряд заголовков
|
||||
const headerRowTop = document.getElementById('headerRowTop');
|
||||
let topHtml = `
|
||||
<th rowspan="2" style="width: 50px;"></th>
|
||||
<th rowspan="2" style="width: 120px;">Дата</th>
|
||||
<th rowspan="2" style="min-width: 200px;">Простои</th>
|
||||
`;
|
||||
|
||||
if (errors.length > 0) {
|
||||
topHtml += `<th colspan="${errors.length}" class="text-center">Ошибки</th>`;
|
||||
}
|
||||
if (malfunctions.length > 0) {
|
||||
topHtml += `<th colspan="${malfunctions.length}" class="text-center">Неисправности</th>`;
|
||||
}
|
||||
|
||||
topHtml += `
|
||||
<th rowspan="2" style="width: 80px;">Раб. ч/день</th>
|
||||
<th rowspan="2" style="width: 100px;">Раб. ч/нед.</th>
|
||||
<th rowspan="2" style="min-width: 150px;">Пояснение</th>
|
||||
<th rowspan="2" style="min-width: 150px;">Комментарий</th>
|
||||
`;
|
||||
headerRowTop.innerHTML = topHtml;
|
||||
|
||||
// Нижний ряд заголовков (названия ошибок/неисправностей)
|
||||
const headerRowBottom = document.getElementById('headerRowBottom');
|
||||
let bottomHtml = '';
|
||||
|
||||
errors.forEach(col => {
|
||||
bottomHtml += `<th class="issue-header" title="${col.name}">${col.name}</th>`;
|
||||
});
|
||||
|
||||
malfunctions.forEach(col => {
|
||||
bottomHtml += `<th class="issue-header" title="${col.name}">${col.name}</th>`;
|
||||
});
|
||||
|
||||
headerRowBottom.innerHTML = bottomHtml;
|
||||
|
||||
// Рендерим строки
|
||||
const tbody = document.getElementById('reportBody');
|
||||
if (currentData.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="100" class="text-center text-muted">Нет данных</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Рассчитываем группы по неделям
|
||||
const weekInfo = calculateWeekGroups(currentData);
|
||||
|
||||
let bodyHtml = '';
|
||||
currentData.forEach((row, idx) => {
|
||||
const date = new Date(row.date);
|
||||
const dateStr = date.toLocaleDateString('ru-RU');
|
||||
const dayStr = date.toLocaleDateString('ru-RU', { weekday: 'short' });
|
||||
const fullDateStr = dateStr + ' (' + dayStr + ')';
|
||||
const info = weekInfo[idx];
|
||||
|
||||
bodyHtml += `<tr>`;
|
||||
{% if user|has_perm:'errors_report_edit' %}
|
||||
bodyHtml += `<td><button class="btn btn-outline-primary btn-edit" onclick="openEditModal(${row.id})"><i class="bi bi-pencil"></i></button></td>`;
|
||||
{% else %}
|
||||
bodyHtml += `<td>${row.id}</td>`;
|
||||
{% endif %}
|
||||
bodyHtml += `<td>${fullDateStr}</td>`;
|
||||
bodyHtml += `<td class="downtime-cell">${row.downtimes_display || '—'}</td>`;
|
||||
|
||||
// Ошибки
|
||||
errors.forEach(col => {
|
||||
const value = row[col.field];
|
||||
bodyHtml += renderMarkCell(value);
|
||||
});
|
||||
|
||||
// Неисправности
|
||||
malfunctions.forEach(col => {
|
||||
const value = row[col.field];
|
||||
bodyHtml += renderMarkCell(value);
|
||||
});
|
||||
|
||||
bodyHtml += `<td class="text-center">${row.daily_work_hours || 0}</td>`;
|
||||
|
||||
// Ячейка "Раб. ч/нед." с rowspan
|
||||
if (info.isFirst) {
|
||||
bodyHtml += `<td class="text-center align-middle weekly-hours-cell" rowspan="${info.rowspan}">${info.weeklyTotal}</td>`;
|
||||
}
|
||||
|
||||
bodyHtml += `<td>${row.explanation || ''}</td>`;
|
||||
bodyHtml += `<td>${row.comment || ''}</td>`;
|
||||
bodyHtml += `</tr>`;
|
||||
});
|
||||
|
||||
tbody.innerHTML = bodyHtml;
|
||||
}
|
||||
|
||||
// Рендер ячейки с отметкой
|
||||
function renderMarkCell(value) {
|
||||
if (value === true) {
|
||||
return '<td class="mark-cell mark-positive"><i class="bi bi-check-lg"></i></td>';
|
||||
} else if (value === false) {
|
||||
return '<td class="mark-cell mark-negative"><i class="bi bi-x-lg"></i></td>';
|
||||
}
|
||||
return '<td class="mark-cell mark-empty">—</td>';
|
||||
}
|
||||
|
||||
// Открыть модальное окно для создания
|
||||
function openCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Новая запись';
|
||||
document.getElementById('editId').value = '';
|
||||
document.getElementById('editDate').value = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('editDailyHours').value = 0;
|
||||
document.getElementById('editExplanation').value = '';
|
||||
document.getElementById('editComment').value = '';
|
||||
|
||||
const btnDelete = document.getElementById('btnDelete');
|
||||
if (btnDelete) btnDelete.style.display = 'none';
|
||||
|
||||
// Очищаем простои
|
||||
document.getElementById('editDowntimeList').innerHTML = '';
|
||||
|
||||
// Заполняем чекбоксы
|
||||
renderIssueCheckboxes({});
|
||||
|
||||
editModal.show();
|
||||
}
|
||||
|
||||
// Открыть модальное окно для редактирования
|
||||
async function openEditModal(id) {
|
||||
const row = currentData.find(r => r.id === id);
|
||||
if (!row) return;
|
||||
|
||||
document.getElementById('modalTitle').textContent = 'Редактирование записи';
|
||||
document.getElementById('editId').value = row.id;
|
||||
document.getElementById('editDate').value = row.date;
|
||||
document.getElementById('editDailyHours').value = row.daily_work_hours || 0;
|
||||
document.getElementById('editExplanation').value = row.explanation || '';
|
||||
document.getElementById('editComment').value = row.comment || '';
|
||||
|
||||
const btnDelete = document.getElementById('btnDelete');
|
||||
if (btnDelete) btnDelete.style.display = 'block';
|
||||
|
||||
// Заполняем простои
|
||||
renderDowntimes(row.downtimes || []);
|
||||
|
||||
// Заполняем чекбоксы
|
||||
renderIssueCheckboxes(row);
|
||||
|
||||
editModal.show();
|
||||
}
|
||||
|
||||
// Рендер периодов простоя
|
||||
function renderDowntimes(downtimes) {
|
||||
const container = document.getElementById('editDowntimeList');
|
||||
let html = '';
|
||||
downtimes.forEach((dt, idx) => {
|
||||
html += createDowntimeRowHtml(idx, dt.start || '', dt.end || '', dt.reason || '');
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Создать HTML для строки простоя
|
||||
function createDowntimeRowHtml(idx, start, end, reason) {
|
||||
return `
|
||||
<div class="downtime-item" data-idx="${idx}">
|
||||
<input type="time" class="form-control form-control-sm dt-start" value="${start}">
|
||||
<span>—</span>
|
||||
<input type="time" class="form-control form-control-sm dt-end" value="${end}">
|
||||
<input type="text" class="form-control form-control-sm dt-reason" placeholder="Причина" value="${reason}">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeDowntimeRow(this)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Добавить период простоя
|
||||
function addDowntimeRow() {
|
||||
const container = document.getElementById('editDowntimeList');
|
||||
const items = container.querySelectorAll('.downtime-item');
|
||||
const newIdx = items.length;
|
||||
|
||||
// Создаём новый элемент без затрагивания существующих
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = createDowntimeRowHtml(newIdx, '', '', '').trim();
|
||||
container.appendChild(template.content.firstChild);
|
||||
}
|
||||
|
||||
// Удалить период простоя
|
||||
function removeDowntimeRow(btn) {
|
||||
btn.closest('.downtime-item').remove();
|
||||
}
|
||||
|
||||
// Рендер чекбоксов ошибок/неисправностей
|
||||
function renderIssueCheckboxes(row) {
|
||||
let errorsHtml = '';
|
||||
issueTypes.errors.forEach(e => {
|
||||
const checked = row[`issue_${e.id}`] ? 'checked' : '';
|
||||
errorsHtml += `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input issue-check" type="checkbox" id="issue_${e.id}" data-issue-id="${e.id}" ${checked}>
|
||||
<label class="form-check-label" for="issue_${e.id}">${e.name}</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
document.getElementById('editErrors').innerHTML = errorsHtml;
|
||||
|
||||
let malfHtml = '';
|
||||
issueTypes.malfunctions.forEach(m => {
|
||||
const checked = row[`issue_${m.id}`] ? 'checked' : '';
|
||||
malfHtml += `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input issue-check" type="checkbox" id="issue_${m.id}" data-issue-id="${m.id}" ${checked}>
|
||||
<label class="form-check-label" for="issue_${m.id}">${m.name}</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
document.getElementById('editMalfunctions').innerHTML = malfHtml;
|
||||
}
|
||||
|
||||
// Собрать данные простоев из формы
|
||||
function collectDowntimes() {
|
||||
const items = document.querySelectorAll('#editDowntimeList .downtime-item');
|
||||
const result = [];
|
||||
items.forEach(item => {
|
||||
const start = item.querySelector('.dt-start').value;
|
||||
const end = item.querySelector('.dt-end').value;
|
||||
const reason = item.querySelector('.dt-reason').value;
|
||||
if (start && end) {
|
||||
result.push({start, end, reason});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Сохранение записи
|
||||
async function saveRecord() {
|
||||
const date = document.getElementById('editDate').value;
|
||||
if (!date) {
|
||||
alert('Укажите дату');
|
||||
return;
|
||||
}
|
||||
|
||||
// Собираем отметки
|
||||
const issueMarks = {};
|
||||
document.querySelectorAll('.issue-check').forEach(cb => {
|
||||
issueMarks[cb.dataset.issueId] = cb.checked;
|
||||
});
|
||||
|
||||
const data = {
|
||||
id: document.getElementById('editId').value || null,
|
||||
date: date,
|
||||
daily_work_hours: parseFloat(document.getElementById('editDailyHours').value) || 0,
|
||||
explanation: document.getElementById('editExplanation').value,
|
||||
comment: document.getElementById('editComment').value,
|
||||
downtimes: collectDowntimes(),
|
||||
issue_marks: issueMarks
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('{% url "mainapp:errors_report_save" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
editModal.hide();
|
||||
loadData();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения:', error);
|
||||
alert('Ошибка сохранения');
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление записи
|
||||
async function deleteRecord() {
|
||||
const id = document.getElementById('editId').value;
|
||||
if (!id) return;
|
||||
|
||||
if (!confirm('Удалить эту запись?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('{% url "mainapp:errors_report_delete" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
editModal.hide();
|
||||
loadData();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user