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

This commit is contained in:
2025-12-16 10:19:21 +03:00
parent 1a953cc558
commit 0b34fbd720
7 changed files with 318 additions and 47 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-12-16 06:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0028_remove_issue_type_fields'),
]
operations = [
migrations.AddField(
model_name='dailyreport',
name='location_place',
field=models.CharField(choices=[('kr', 'КР'), ('dv', 'ДВ')], default='kr', help_text='К какому комплексу принадлежит журнал', max_length=30, null=True, verbose_name='Комплекс'),
),
]

View File

@@ -30,6 +30,10 @@ class IssueType(models.Model):
class DailyReport(models.Model):
"""Ежедневный отчёт"""
PLACES = [
("kr", "КР"),
("dv", "ДВ")
]
date = models.DateField(
unique=True,
verbose_name="Дата",
@@ -50,6 +54,14 @@ class DailyReport(models.Model):
)
explanation = models.TextField(blank=True, null=True, verbose_name='Пояснение')
comment = models.TextField(blank=True, null=True, verbose_name='Комментарий')
location_place = models.CharField(
max_length=30,
choices=PLACES,
null=True,
default="kr",
verbose_name="Комплекс",
help_text="К какому комплексу принадлежит журнал",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создано")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Обновлено")

View File

@@ -128,7 +128,7 @@ class Band(models.Model):
)
def __str__(self):
return self.name
return f"{self.name}({int(self.border_start)}-{int(self.border_end)})МГц"
class Meta:
verbose_name = "Диапазон"

View File

@@ -29,43 +29,42 @@
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'lyngsatapp:lyngsat_list' %}">Справочные данные</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
</li> -->
<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>
</li>
{% endif %}
<!-- <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li> -->
<li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">Карта</a>
<!-- Дропдаун "Прочее" -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarOtherDropdown" role="button" data-bs-toggle="dropdown">
Прочее
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'lyngsatapp:lyngsat_list' %}">Справочные данные</a></li>
<li><a class="dropdown-item" href="{% url 'mainapp:signal_marks' %}">Отметки сигналов</a></li>
<li><a class="dropdown-item" href="{% url 'mainapp:errors_report' %}">Журнал ошибок</a></li>
<li><a class="dropdown-item" href="{% url 'mapsapp:2dmap' %}">Карта</a></li>
</ul>
</li>
<!-- Дропдаун "Админ" (только для администраторов) -->
{% if user.customuser.role == 'admin' %}
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:user_permissions_list' %}">Разрешения</a>
</li>
{% endif %}
{% if user.customuser.role == 'admin' %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarAdminDropdown" role="button" data-bs-toggle="dropdown">
Админ
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'mainapp:user_permissions_list' %}">Разрешения</a></li>
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Админ панель</a></li>
</ul>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
<!-- Пользовательское меню -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarUserDropdown" role="button" data-bs-toggle="dropdown">
{% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% elif user.get_full_name %}

View File

@@ -44,8 +44,15 @@
word-wrap: break-word;
white-space: normal;
}
.filter-panel {
margin-bottom: 15px;
.btn-group .badge {
position: absolute;
top: -5px;
right: -5px;
font-size: 0.65rem;
padding: 0.2em 0.4em;
}
.btn-group .btn {
position: relative;
}
.btn-edit {
padding: 2px 6px;
@@ -78,23 +85,123 @@
<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">
<!-- Toolbar -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center gap-3">
<!-- Action buttons -->
<div class="d-flex gap-2">
{% if user|has_perm:'errors_report_create' %}
<button class="btn btn-success btn-sm" onclick="openCreateModal()">
<i class="bi bi-plus-lg"></i> Добавить запись
</button>
{% endif %}
<!-- <button class="btn btn-primary btn-sm" onclick="loadData()">
<i class="bi bi-arrow-clockwise"></i> Обновить
</button> -->
</div>
<!-- Filter Toggle Button -->
<div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div>
<label class="form-label">Дата по:</label>
<input type="date" id="dateTo" class="form-control form-control-sm">
</div>
<!-- Offcanvas Filter Panel -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
</div>
<div class="offcanvas-body">
<form id="filter-form">
<!-- Date Filter -->
<div class="mb-3">
<label class="form-label">
Период
</label>
<div class="mb-2">
<label class="form-label small">Дата с:</label>
<input type="date" id="dateFrom" name="date_from" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label class="form-label small">Дата по:</label>
<input type="date" id="dateTo" name="date_to" class="form-control form-control-sm">
</div>
</div>
<!-- Location Filter -->
<div class="mb-3">
<label class="form-label">
Комплекс
</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('locationFilter', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('locationFilter', false)">Снять</button>
</div>
<select id="locationFilter" name="location_place" class="form-select form-select-sm" multiple size="2">
<option value="kr">КР</option>
<option value="dv">ДВ</option>
</select>
</div>
<!-- Error Filter -->
<div class="mb-3">
<label class="form-label">
Ошибки
</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('errorFilter', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('errorFilter', false)">Снять</button>
</div>
<select id="errorFilter" name="error_filter" class="form-select form-select-sm" multiple size="6">
{% for e in errors %}
<option value="{{ e.id }}">{{ e.name }}</option>
{% endfor %}
</select>
</div>
<!-- Malfunction Filter -->
<div class="mb-3">
<label class="form-label">
Неисправности
</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('malfunctionFilter', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('malfunctionFilter', false)">Снять</button>
</div>
<select id="malfunctionFilter" name="malfunction_filter" class="form-select form-select-sm" multiple size="6">
{% for m in malfunctions %}
<option value="{{ m.id }}">{{ m.name }}</option>
{% endfor %}
</select>
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-4">
<button type="button" class="btn btn-primary btn-sm" onclick="applyFilters()">
Применить
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="resetFilters()">
Сбросить
</button>
</div>
</form>
</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">
@@ -123,14 +230,21 @@
<form id="editForm">
<input type="hidden" id="editId">
<div class="row mb-3">
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">Дата</label>
<input type="date" id="editDate" class="form-control" required>
</div>
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">Время работы за день (ч)</label>
<input type="number" step="0.01" min="0" id="editDailyHours" class="form-control" value="0">
</div>
<div class="col-md-4">
<label class="form-label">Комплекс</label>
<select id="editLocationPlace" class="form-select" required>
<option value="kr">КР</option>
<option value="dv">ДВ</option>
</select>
</div>
</div>
<div class="mb-3">
@@ -200,6 +314,21 @@ let editModal = null;
document.addEventListener('DOMContentLoaded', function() {
editModal = new bootstrap.Modal(document.getElementById('editModal'));
loadData();
// Добавляем обработчики для обновления счетчика фильтров
const form = document.getElementById('filter-form');
if (form) {
const inputFields = form.querySelectorAll('input[type="date"], select');
inputFields.forEach(input => {
input.addEventListener('change', updateFilterCounter);
});
}
// Обновляем счетчик при открытии offcanvas
const offcanvasElement = document.getElementById('offcanvasFilters');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
});
// Загрузка данных
@@ -207,10 +336,33 @@ async function loadData() {
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
// Получаем выбранные значения из мультиселектов
const locationSelect = document.getElementById('locationFilter');
const locationPlaces = Array.from(locationSelect.selectedOptions).map(opt => opt.value);
const errorSelect = document.getElementById('errorFilter');
const errorFilters = Array.from(errorSelect.selectedOptions).map(opt => opt.value);
const malfunctionSelect = document.getElementById('malfunctionFilter');
const malfunctionFilters = Array.from(malfunctionSelect.selectedOptions).map(opt => opt.value);
let url = '{% url "mainapp:errors_report_api" %}?';
if (dateFrom) url += `date_from=${dateFrom}&`;
if (dateTo) url += `date_to=${dateTo}&`;
// Добавляем множественные значения
locationPlaces.forEach(loc => {
url += `location_place=${loc}&`;
});
errorFilters.forEach(err => {
url += `error_filter=${err}&`;
});
malfunctionFilters.forEach(mal => {
url += `malfunction_filter=${mal}&`;
});
try {
const response = await fetch(url);
const result = await response.json();
@@ -219,12 +371,82 @@ async function loadData() {
currentColumns = result.columns;
renderTable();
updateFilterCounter();
} catch (error) {
console.error('Ошибка загрузки:', error);
alert('Ошибка загрузки данных');
}
}
// Функция для выбора/снятия всех опций в select
function selectAllOptions(selectId, selectAll) {
const selectElement = document.getElementById(selectId);
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
}
}
// Применить фильтры
function applyFilters() {
loadData();
// Закрыть offcanvas
const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('offcanvasFilters'));
if (offcanvas) {
offcanvas.hide();
}
}
// Сбросить фильтры
function resetFilters() {
document.getElementById('dateFrom').value = '';
document.getElementById('dateTo').value = '';
// Снимаем выбор со всех мультиселектов
selectAllOptions('locationFilter', false);
selectAllOptions('errorFilter', false);
selectAllOptions('malfunctionFilter', false);
loadData();
// Закрыть offcanvas
const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('offcanvasFilters'));
if (offcanvas) {
offcanvas.hide();
}
}
// Обновить счетчик фильтров
function updateFilterCounter() {
let filterCount = 0;
// Проверяем даты
if (document.getElementById('dateFrom').value) filterCount++;
if (document.getElementById('dateTo').value) filterCount++;
// Проверяем мультиселекты
const locationSelect = document.getElementById('locationFilter');
if (locationSelect.selectedOptions.length > 0) filterCount++;
const errorSelect = document.getElementById('errorFilter');
if (errorSelect.selectedOptions.length > 0) filterCount++;
const malfunctionSelect = document.getElementById('malfunctionFilter');
if (malfunctionSelect.selectedOptions.length > 0) filterCount++;
// Отображаем счетчик
const counterElement = document.getElementById('filterCounter');
if (counterElement) {
if (filterCount > 0) {
counterElement.textContent = filterCount;
counterElement.style.display = 'inline';
} else {
counterElement.style.display = 'none';
}
}
}
// Получить номер ISO недели для даты
function getWeekKey(dateStr) {
const date = new Date(dateStr);
@@ -295,6 +517,7 @@ function renderTable() {
topHtml += `
<th rowspan="2" style="width: 80px;">Раб. ч/день</th>
<th rowspan="2" style="width: 100px;">Раб. ч/нед.</th>
<th rowspan="2" style="width: 80px;">Комплекс</th>
<th rowspan="2" style="min-width: 150px;">Пояснение</th>
<th rowspan="2" style="min-width: 150px;">Комментарий</th>
`;
@@ -360,6 +583,7 @@ function renderTable() {
bodyHtml += `<td class="text-center align-middle weekly-hours-cell" rowspan="${info.rowspan}">${info.weeklyTotal}</td>`;
}
bodyHtml += `<td class="text-center">${row.location_place_display || ''}</td>`;
bodyHtml += `<td>${row.explanation || ''}</td>`;
bodyHtml += `<td>${row.comment || ''}</td>`;
bodyHtml += `</tr>`;
@@ -384,6 +608,7 @@ function openCreateModal() {
document.getElementById('editId').value = '';
document.getElementById('editDate').value = new Date().toISOString().split('T')[0];
document.getElementById('editDailyHours').value = 0;
document.getElementById('editLocationPlace').value = 'kr';
document.getElementById('editExplanation').value = '';
document.getElementById('editComment').value = '';
@@ -408,6 +633,7 @@ async function openEditModal(id) {
document.getElementById('editId').value = row.id;
document.getElementById('editDate').value = row.date;
document.getElementById('editDailyHours').value = row.daily_work_hours || 0;
document.getElementById('editLocationPlace').value = row.location_place || 'kr';
document.getElementById('editExplanation').value = row.explanation || '';
document.getElementById('editComment').value = row.comment || '';
@@ -525,6 +751,7 @@ async function saveRecord() {
id: document.getElementById('editId').value || null,
date: date,
daily_work_hours: parseFloat(document.getElementById('editDailyHours').value) || 0,
location_place: document.getElementById('editLocationPlace').value,
explanation: document.getElementById('editExplanation').value,
comment: document.getElementById('editComment').value,
downtimes: collectDowntimes(),

View File

@@ -31,6 +31,9 @@ class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
# Получаем параметры фильтрации
date_from = request.GET.get('date_from')
date_to = request.GET.get('date_to')
location_places = request.GET.getlist('location_place')
error_filters = request.GET.getlist('error_filter')
malfunction_filters = request.GET.getlist('malfunction_filter')
reports = DailyReport.objects.all()
@@ -38,8 +41,16 @@ class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
reports = reports.filter(date__gte=date_from)
if date_to:
reports = reports.filter(date__lte=date_to)
if location_places:
reports = reports.filter(location_place__in=location_places)
reports = reports.prefetch_related('downtime_periods', 'issue_marks__issue_type')
# Фильтрация по ошибкам/неисправностям
if error_filters:
reports = reports.filter(issue_marks__issue_type_id__in=error_filters, issue_marks__is_present=True)
if malfunction_filters:
reports = reports.filter(issue_marks__issue_type_id__in=malfunction_filters, issue_marks__is_present=True)
reports = reports.prefetch_related('downtime_periods', 'issue_marks__issue_type').distinct()
# Получаем все типы ошибок/неисправностей
issue_types = IssueType.objects.all().order_by('category', 'name')
@@ -68,6 +79,8 @@ class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
'weekly_work_hours': float(report.weekly_work_hours),
'explanation': report.explanation or '',
'comment': report.comment or '',
'location_place': report.location_place or '',
'location_place_display': report.get_location_place_display() if report.location_place else '',
}
# Добавляем отметки по каждому типу
@@ -129,6 +142,7 @@ class ErrorsReportSaveAPIView(View):
report.daily_work_hours = Decimal(str(data.get('daily_work_hours', 0)))
report.explanation = data.get('explanation', '')
report.comment = data.get('comment', '')
report.location_place = data.get('location_place', 'kr')
report.save()
except DailyReport.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Запись не найдена'}, status=404)
@@ -139,6 +153,7 @@ class ErrorsReportSaveAPIView(View):
daily_work_hours=Decimal(str(data.get('daily_work_hours', 0))),
explanation=data.get('explanation', ''),
comment=data.get('comment', ''),
location_place=data.get('location_place', 'kr'),
created_by=request.user if request.user.is_authenticated else None,
)

View File

@@ -198,7 +198,7 @@ class SatelliteListView(LoginRequiredMixin, View):
processed_satellites = []
for satellite in page_obj:
# Get band names
band_names = [band.name for band in satellite.band.all()]
band_names = [f"{band.name}({int(band.border_start)}-{int(band.border_end)})" for band in satellite.band.all()]
# Get location_place display value
location_place_display = dict(Satellite.PLACES).get(satellite.location_place, "-") if satellite.location_place else "-"