Добавил журнал ошибок

This commit is contained in:
2025-12-15 17:54:26 +03:00
parent 480bb60855
commit 1a953cc558
12 changed files with 1162 additions and 7 deletions

View File

@@ -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>

View 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 %}