Новый визуальный функционал
This commit is contained in:
@@ -178,8 +178,8 @@ USE_TZ = True
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = 'login'
|
||||||
LOGIN_REDIRECT_URL = 'home'
|
LOGIN_REDIRECT_URL = 'mainapp:home'
|
||||||
LOGOUT_REDIRECT_URL = 'home'
|
LOGOUT_REDIRECT_URL = 'mainapp:home'
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# STATIC FILES CONFIGURATION
|
# STATIC FILES CONFIGURATION
|
||||||
|
|||||||
@@ -5,17 +5,18 @@
|
|||||||
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% if page_obj.has_other_pages %}
|
{% if page_obj %}
|
||||||
<nav aria-label="Page navigation" class="d-flex align-items-center">
|
<nav aria-label="Page navigation" class="d-flex align-items-center">
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
<ul class="pagination mb-0">
|
<ul class="pagination mb-0">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1" title="Первая">
|
<a class="page-link" onclick="updatePage(1)" href="javascript:void(0)" title="Первая">
|
||||||
<i class="bi bi-chevron-bar-left"></i>
|
<i class="bi bi-chevron-bar-left"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" title="Предыдущая">
|
<a class="page-link" onclick="updatePage({{ page_obj.previous_page_number }})" href="javascript:void(0)" title="Предыдущая">
|
||||||
<i class="bi bi-chevron-left"></i>
|
<i class="bi bi-chevron-left"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -28,29 +29,74 @@
|
|||||||
</li>
|
</li>
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
<a class="page-link" onclick="updatePage({{ num }})" href="javascript:void(0)">{{ num }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" title="Следующая">
|
<a class="page-link" onclick="updatePage({{ page_obj.next_page_number }})" href="javascript:void(0)" title="Следующая">
|
||||||
<i class="bi bi-chevron-right"></i>
|
<i class="bi bi-chevron-right"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" title="Последняя">
|
<a class="page-link" onclick="updatePage({{ page_obj.paginator.num_pages }})" href="javascript:void(0)" title="Последняя">
|
||||||
<i class="bi bi-chevron-bar-right"></i>
|
<i class="bi bi-chevron-bar-right"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if show_info %}
|
{% if show_info %}
|
||||||
<div class="ms-3 text-muted small">
|
<div class="ms-3 text-muted small">
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
|
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
|
||||||
|
{% else %}
|
||||||
|
{% if page_obj.paginator.count > 0 %}
|
||||||
|
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
|
||||||
|
{% else %}
|
||||||
|
0 из 0
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getAllFilterParams() {
|
||||||
|
const filterForm = document.getElementById('filter-form');
|
||||||
|
|
||||||
|
if (filterForm) {
|
||||||
|
const formData = new FormData(filterForm);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
if (key !== 'page') {
|
||||||
|
params.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toolbarSearch = document.getElementById('toolbar-search');
|
||||||
|
if (toolbarSearch && toolbarSearch.value.trim() !== '') {
|
||||||
|
params.set('search', toolbarSearch.value);
|
||||||
|
} else {
|
||||||
|
params.delete('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
} else {
|
||||||
|
const currentParams = new URLSearchParams(window.location.search);
|
||||||
|
currentParams.delete('page');
|
||||||
|
return currentParams.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePage(pageNumber) {
|
||||||
|
const filterParams = getAllFilterParams();
|
||||||
|
const urlParams = new URLSearchParams(filterParams);
|
||||||
|
urlParams.set('page', pageNumber);
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!-- Selected Items Offcanvas Component -->
|
||||||
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="selectedItemsOffcanvas" aria-labelledby="selectedItemsOffcanvasLabel" style="width: 100vw;">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="selectedItemsOffcanvasLabel">Выбранные элементы</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body p-0">
|
||||||
|
<div class="d-flex flex-column h-100">
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="p-3 border-bottom">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeSelectedItems()">
|
||||||
|
<i class="bi bi-trash"></i> Убрать из списка
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" onclick="sendSelectedItems()">
|
||||||
|
<i class="bi bi-send"></i> Отправить
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm ms-auto" data-bs-dismiss="offcanvas">
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table container -->
|
||||||
|
<div class="flex-grow-1 overflow-auto">
|
||||||
|
<div class="table-responsive" style="height: 100%;">
|
||||||
|
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||||
|
<thead class="table-dark sticky-top">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center" style="width: 3%;">
|
||||||
|
<input type="checkbox" id="select-all-selected" class="form-check-input" onclick="toggleAllSelected(this)">
|
||||||
|
</th>
|
||||||
|
<th scope="col">Имя</th>
|
||||||
|
<th scope="col">Спутник</th>
|
||||||
|
<th scope="col">Част, МГц</th>
|
||||||
|
<th scope="col">Полоса, МГц</th>
|
||||||
|
<th scope="col">Поляризация</th>
|
||||||
|
<th scope="col">Сим. V</th>
|
||||||
|
<th scope="col">Модул</th>
|
||||||
|
<th scope="col">ОСШ</th>
|
||||||
|
<th scope="col">Время ГЛ</th>
|
||||||
|
<th scope="col">Местоположение</th>
|
||||||
|
<th scope="col">Геолокация</th>
|
||||||
|
<th scope="col">Кубсат</th>
|
||||||
|
<th scope="col">Опер. отд</th>
|
||||||
|
<th scope="col">Обновлено</th>
|
||||||
|
<th scope="col">Кем(обн)</th>
|
||||||
|
<th scope="col">Создано</th>
|
||||||
|
<th scope="col">Кем(созд)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="selected-items-table-body">
|
||||||
|
<!-- Selected items will be populated here via JavaScript -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
475
dbapp/mainapp/templates/mainapp/objitem_detail.html
Normal file
475
dbapp/mainapp/templates/mainapp/objitem_detail.html
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load static leaflet_tags %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
|
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
|
||||||
|
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
||||||
|
.btn-action { margin-right: 0.5rem; }
|
||||||
|
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
|
||||||
|
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
|
||||||
|
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
|
||||||
|
.form-check-input { margin-top: 0.25rem; }
|
||||||
|
.datetime-group { display: flex; gap: 1rem; }
|
||||||
|
.datetime-group > div { flex: 1; }
|
||||||
|
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
||||||
|
.map-container { margin-bottom: 1rem; }
|
||||||
|
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
||||||
|
.map-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.map-control-btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.map-control-btn.active {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
.map-control-btn.edit {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border-color: #ffeeba;
|
||||||
|
}
|
||||||
|
.map-control-btn.save {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
border-color: #bee5eb;
|
||||||
|
}
|
||||||
|
.map-control-btn.cancel {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon {
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-3">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||||
|
<h2>Просмотр объекта: {{ object.name }}</h2>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Основная информация -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-header">
|
||||||
|
<h4>Основная информация</h4>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Название:</label>
|
||||||
|
<div class="readonly-field">{{ object.name|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Дата создания:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Создан пользователем:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Дата последнего изменения:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Изменен пользователем:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ВЧ загрузки -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-header">
|
||||||
|
<h4>ВЧ загрузка</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for param in object.parameters_obj.all %}
|
||||||
|
<div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Спутник:</label>
|
||||||
|
<div class="readonly-field">{{ param.id_satellite.name|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Частота (МГц):</label>
|
||||||
|
<div class="readonly-field">{{ param.frequency|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Полоса (МГц):</label>
|
||||||
|
<div class="readonly-field">{{ param.freq_range|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Поляризация:</label>
|
||||||
|
<div class="readonly-field">{{ param.polarization.name|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Символьная скорость:</label>
|
||||||
|
<div class="readonly-field">{{ param.bod_velocity|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Модуляция:</label>
|
||||||
|
<div class="readonly-field">{{ param.modulation.name|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">ОСШ:</label>
|
||||||
|
<div class="readonly-field">{{ param.snr|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Стандарт:</label>
|
||||||
|
<div class="readonly-field">{{ param.standard.name|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<p>Нет данных о ВЧ загрузке</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Блок с картой -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-header">
|
||||||
|
<h4>Карта</h4>
|
||||||
|
</div>
|
||||||
|
<div class="map-container">
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Геоданные -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-header">
|
||||||
|
<h4>Геоданные</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.geo_obj %}
|
||||||
|
<!-- Координаты геолокации -->
|
||||||
|
<div class="coord-sync-group">
|
||||||
|
<div class="coord-group-header">Координаты геолокации</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Широта:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Долгота:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Координаты Кубсата -->
|
||||||
|
<div class="coord-group">
|
||||||
|
<div class="coord-group-header">Координаты Кубсата</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Долгота:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Широта:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Координаты оперативников -->
|
||||||
|
<div class="coord-group">
|
||||||
|
<div class="coord-group-header">Координаты оперативников</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Долгота:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Широта:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Местоположение:</label>
|
||||||
|
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Комментарий:</label>
|
||||||
|
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Дата и время:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-check-label">Усредненное значение:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Расстояние гео-кубсат, км:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.distance_coords_kup is not None %}
|
||||||
|
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Расстояние гео-опер, км:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.distance_coords_valid is not None %}
|
||||||
|
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Расстояние кубсат-опер, км:</label>
|
||||||
|
<div class="readonly-field">
|
||||||
|
{% if object.geo_obj.distance_kup_valid is not None %}
|
||||||
|
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>Нет данных о геолокации</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-4">
|
||||||
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
|
<a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<!-- Подключаем Leaflet и его плагины -->
|
||||||
|
{% leaflet_js %}
|
||||||
|
{% leaflet_css %}
|
||||||
|
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Инициализация карты
|
||||||
|
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Определяем цвета для маркеров
|
||||||
|
const colors = {
|
||||||
|
geo: 'blue',
|
||||||
|
kupsat: 'red',
|
||||||
|
valid: 'green'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для создания иконки маркера
|
||||||
|
function createMarkerIcon(color) {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||||
|
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
shadowSize: [41, 41]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Маркеры
|
||||||
|
const markers = {};
|
||||||
|
function createMarker(position, color, name) {
|
||||||
|
const marker = L.marker(position, {
|
||||||
|
draggable: false,
|
||||||
|
icon: createMarkerIcon(color),
|
||||||
|
title: name
|
||||||
|
}).addTo(map);
|
||||||
|
marker.bindPopup(name);
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем координаты из данных объекта
|
||||||
|
{% if object.geo_obj and object.geo_obj.coords %}
|
||||||
|
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
|
||||||
|
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
|
||||||
|
{% else %}
|
||||||
|
const geoLat = 55.75;
|
||||||
|
const geoLng = 37.62;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
|
||||||
|
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
|
||||||
|
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
|
||||||
|
{% else %}
|
||||||
|
const kupsatLat = 55.75;
|
||||||
|
const kupsatLng = 37.61;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if object.geo_obj and object.geo_obj.coords_valid %}
|
||||||
|
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
|
||||||
|
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
|
||||||
|
{% else %}
|
||||||
|
const validLat = 55.75;
|
||||||
|
const validLng = 37.63;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
// Создаем маркеры
|
||||||
|
markers.geo = createMarker(
|
||||||
|
[geoLat, geoLng],
|
||||||
|
colors.geo,
|
||||||
|
'Геолокация'
|
||||||
|
);
|
||||||
|
|
||||||
|
markers.kupsat = createMarker(
|
||||||
|
[kupsatLat, kupsatLng],
|
||||||
|
colors.kupsat,
|
||||||
|
'Кубсат'
|
||||||
|
);
|
||||||
|
|
||||||
|
markers.valid = createMarker(
|
||||||
|
[validLat, validLng],
|
||||||
|
colors.valid,
|
||||||
|
'Оперативник'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Центрируем карту на первом маркере
|
||||||
|
if (map.hasLayer(markers.geo)) {
|
||||||
|
map.setView(markers.geo.getLatLng(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Легенда
|
||||||
|
const legend = L.control({ position: 'bottomright' });
|
||||||
|
|
||||||
|
legend.onAdd = function() {
|
||||||
|
const div = L.DomUtil.create('div', 'info legend');
|
||||||
|
div.style.fontSize = '14px';
|
||||||
|
div.style.backgroundColor = 'white';
|
||||||
|
div.style.padding = '10px';
|
||||||
|
div.style.borderRadius = '4px';
|
||||||
|
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||||
|
div.innerHTML = `
|
||||||
|
<h5>Легенда</h5>
|
||||||
|
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
||||||
|
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
|
||||||
|
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
|
legend.addTo(map);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'mainapp:objitem_list' %}{% if return_params %}?{{ return_params }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
<div class="d-flex justify-content-end mt-4">
|
<div class="d-flex justify-content-end mt-4">
|
||||||
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
|
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
{% if object %}
|
{% if object %}
|
||||||
<a href="{% url 'mainapp:objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a>
|
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
{% block title %}Список объектов{% endblock %}
|
{% block title %}Список объектов{% endblock %}
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
.table-responsive table {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-responsive tr.selected {
|
.table-responsive tr.selected {
|
||||||
background-color: #d4edff;
|
background-color: #d4edff;
|
||||||
}
|
}
|
||||||
@@ -72,16 +68,39 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
<!-- Add to List Button with Counter -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-success btn-sm" type="button" onclick="addSelectedToList()">
|
||||||
|
<i class="bi bi-plus-circle"></i> Добавить к
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Items Counter Button -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
||||||
|
<i class="bi bi-list-check"></i> Список
|
||||||
|
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Column visibility toggle button -->
|
<!-- Column visibility toggle button -->
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||||
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-gear"></i> Колонки
|
<i class="bi bi-gear"></i> Колонки
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050;">
|
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050; max-height: 300px; overflow-y: auto;">
|
||||||
<li>
|
<li>
|
||||||
<label class="dropdown-item">
|
<label class="dropdown-item">
|
||||||
<input type="checkbox" id="select-all-columns" checked
|
<input type="checkbox" id="select-all-columns" unchecked
|
||||||
onchange="toggleAllColumns(this)"> Выбрать всё
|
onchange="toggleAllColumns(this)"> Выбрать всё
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
@@ -214,6 +233,24 @@
|
|||||||
onchange="toggleColumn(this)"> Кем (создание)
|
onchange="toggleColumn(this)"> Кем (создание)
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<label class="dropdown-item">
|
||||||
|
<input type="checkbox" class="column-toggle" data-column="21" unchecked
|
||||||
|
onchange="toggleColumn(this)"> Комментарий
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label class="dropdown-item">
|
||||||
|
<input type="checkbox" class="column-toggle" data-column="22" unchecked
|
||||||
|
onchange="toggleColumn(this)"> Усреднённое
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label class="dropdown-item">
|
||||||
|
<input type="checkbox" class="column-toggle" data-column="23" unchecked
|
||||||
|
onchange="toggleColumn(this)"> Стандарт
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,12 +264,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<!-- Offcanvas Filter Panel -->
|
||||||
<!-- Filters Sidebar - Made narrower -->
|
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
|
||||||
<div class="col-md-auto">
|
<div class="offcanvas-header">
|
||||||
<div class="card h-100">
|
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
|
||||||
<div class="card-body">
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||||||
<h5 class="card-title">Фильтры</h5>
|
</div>
|
||||||
|
<div class="offcanvas-body">
|
||||||
<form method="get" id="filter-form">
|
<form method="get" id="filter-form">
|
||||||
<!-- Satellite Selection - Multi-select -->
|
<!-- Satellite Selection - Multi-select -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -422,6 +460,9 @@
|
|||||||
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
|
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
|
||||||
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %}
|
{% include 'mainapp/components/_table_header.html' with label="Кем(созд)" field="" sortable=False %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
|
||||||
|
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -431,7 +472,7 @@
|
|||||||
<input type="checkbox" class="form-check-input item-checkbox"
|
<input type="checkbox" class="form-check-input item-checkbox"
|
||||||
value="{{ item.id }}">
|
value="{{ item.id }}">
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{% if item.obj.id %}{% url 'mainapp:objitem_update' item.obj.id %}?return_params={{ request.GET.urlencode }}{% endif %}">{{ item.name }}</a></td>
|
<td><a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||||
<td>{{ item.satellite_name }}</td>
|
<td>{{ item.satellite_name }}</td>
|
||||||
<td>{{ item.frequency }}</td>
|
<td>{{ item.frequency }}</td>
|
||||||
<td>{{ item.freq_range }}</td>
|
<td>{{ item.freq_range }}</td>
|
||||||
@@ -451,10 +492,13 @@
|
|||||||
<td>{{ item.updated_by }}</td>
|
<td>{{ item.updated_by }}</td>
|
||||||
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td>{{ item.obj.created_by }}</td>
|
<td>{{ item.obj.created_by }}</td>
|
||||||
|
<td>{{ item.comment }}</td>
|
||||||
|
<td>{{ item.is_average }}</td>
|
||||||
|
<td>{{ item.standard }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="19" class="text-center py-4">
|
<td colspan="22" class="text-center py-4">
|
||||||
{% if selected_satellite_id %}
|
{% if selected_satellite_id %}
|
||||||
Нет данных для выбранных фильтров
|
Нет данных для выбранных фильтров
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -810,10 +854,310 @@
|
|||||||
creationUserCheckbox.checked = false;
|
creationUserCheckbox.checked = false;
|
||||||
toggleColumn(creationUserCheckbox);
|
toggleColumn(creationUserCheckbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide comment, is_average, and standard columns by default
|
||||||
|
const commentCheckbox = document.querySelector('input[data-column="21"]');
|
||||||
|
const isAverageCheckbox = document.querySelector('input[data-column="22"]');
|
||||||
|
const standardCheckbox = document.querySelector('input[data-column="23"]');
|
||||||
|
|
||||||
|
if (commentCheckbox) {
|
||||||
|
commentCheckbox.checked = false;
|
||||||
|
toggleColumn(commentCheckbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize column visibility after page loads
|
if (isAverageCheckbox) {
|
||||||
setTimeout(initColumnVisibility, 100); // Slight delay to ensure DOM is fully loaded
|
isAverageCheckbox.checked = false;
|
||||||
|
toggleColumn(isAverageCheckbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (standardCheckbox) {
|
||||||
|
standardCheckbox.checked = false;
|
||||||
|
toggleColumn(standardCheckbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter counter functionality
|
||||||
|
function updateFilterCounter() {
|
||||||
|
const form = document.getElementById('filter-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
let filterCount = 0;
|
||||||
|
|
||||||
|
// Count non-empty form fields
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
// For multi-select fields, we need to handle them separately
|
||||||
|
if (key === 'satellite_id' || key === 'modulation' || key === 'polarization') {
|
||||||
|
// Skip counting individual selections - they'll be counted as one filter
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filterCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count selected options in multi-select fields
|
||||||
|
const multiSelectFields = ['satellite_id', 'modulation', 'polarization'];
|
||||||
|
for (const field of multiSelectFields) {
|
||||||
|
const selectElement = document.querySelector(`select[name="${field}"]`);
|
||||||
|
if (selectElement) {
|
||||||
|
const selectedOptions = Array.from(selectElement.selectedOptions).filter(opt => opt.selected);
|
||||||
|
if (selectedOptions.length > 0) {
|
||||||
|
filterCount++; // Count the entire field as one filter even if multiple options are selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count checkbox filters
|
||||||
|
const hasKupsatCheckboxes = document.querySelectorAll('input[name="has_kupsat"]:checked');
|
||||||
|
const hasValidCheckboxes = document.querySelectorAll('input[name="has_valid"]:checked');
|
||||||
|
|
||||||
|
if (hasKupsatCheckboxes.length > 0) {
|
||||||
|
filterCount++;
|
||||||
|
}
|
||||||
|
if (hasValidCheckboxes.length > 0) {
|
||||||
|
filterCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the filter counter
|
||||||
|
const counterElement = document.getElementById('filterCounter');
|
||||||
|
if (counterElement) {
|
||||||
|
if (filterCount > 0) {
|
||||||
|
counterElement.textContent = filterCount;
|
||||||
|
counterElement.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
counterElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update filter counter when page loads
|
||||||
|
updateFilterCounter();
|
||||||
|
|
||||||
|
// Add event listeners to form elements to update counter when filters change
|
||||||
|
const form = document.getElementById('filter-form');
|
||||||
|
if (form) {
|
||||||
|
// For input fields
|
||||||
|
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||||||
|
inputFields.forEach(input => {
|
||||||
|
input.addEventListener('input', updateFilterCounter);
|
||||||
|
input.addEventListener('change', updateFilterCounter);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For select elements (including multi-select)
|
||||||
|
const selectFields = form.querySelectorAll('select');
|
||||||
|
selectFields.forEach(select => {
|
||||||
|
select.addEventListener('change', updateFilterCounter);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For checkbox inputs
|
||||||
|
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||||||
|
checkboxFields.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', updateFilterCounter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update when the offcanvas is shown
|
||||||
|
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||||
|
if (offcanvasElement) {
|
||||||
|
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize selected items array from localStorage
|
||||||
|
function loadSelectedItemsFromStorage() {
|
||||||
|
try {
|
||||||
|
const storedItems = localStorage.getItem('selectedItems');
|
||||||
|
if (storedItems) {
|
||||||
|
window.selectedItems = JSON.parse(storedItems);
|
||||||
|
} else {
|
||||||
|
window.selectedItems = [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading selected items from storage:', e);
|
||||||
|
window.selectedItems = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to save selected items to localStorage
|
||||||
|
function saveSelectedItemsToStorage() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('selectedItems', JSON.stringify(window.selectedItems));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error saving selected items to storage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load selected items from localStorage on page load
|
||||||
|
loadSelectedItemsFromStorage();
|
||||||
|
|
||||||
|
// Function to update the selected items counter
|
||||||
|
window.updateSelectedCounter = function() {
|
||||||
|
const counterElement = document.getElementById('selectedCounter');
|
||||||
|
if (window.selectedItems && window.selectedItems.length > 0) {
|
||||||
|
counterElement.textContent = window.selectedItems.length;
|
||||||
|
counterElement.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
counterElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update the counter in the offcanvas
|
||||||
|
const offcanvasCounter = document.querySelector('#selectedItemsOffcanvas .offcanvas-header .badge');
|
||||||
|
if (offcanvasCounter && window.selectedItems && window.selectedItems.length > 0) {
|
||||||
|
offcanvasCounter.textContent = window.selectedItems.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to add selected items to the list
|
||||||
|
window.addSelectedToList = function() {
|
||||||
|
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
|
if (checkedCheckboxes.length === 0) {
|
||||||
|
alert('Пожалуйста, выберите хотя бы один элемент для добавления в список');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the data for each selected row and add to the selectedItems array
|
||||||
|
checkedCheckboxes.forEach(checkbox => {
|
||||||
|
const row = checkbox.closest('tr');
|
||||||
|
const itemId = checkbox.value;
|
||||||
|
|
||||||
|
// Check if item is already in the list
|
||||||
|
const itemExists = window.selectedItems.some(item => item.id === itemId);
|
||||||
|
if (!itemExists) {
|
||||||
|
// Create an object containing the row data
|
||||||
|
const rowData = {
|
||||||
|
id: itemId,
|
||||||
|
name: row.cells[1].textContent,
|
||||||
|
satellite: row.cells[2].textContent,
|
||||||
|
frequency: row.cells[3].textContent,
|
||||||
|
freq_range: row.cells[4].textContent,
|
||||||
|
polarization: row.cells[5].textContent,
|
||||||
|
bod_velocity: row.cells[6].textContent,
|
||||||
|
modulation: row.cells[7].textContent,
|
||||||
|
snr: row.cells[8].textContent,
|
||||||
|
geo_timestamp: row.cells[9].textContent,
|
||||||
|
geo_location: row.cells[10].textContent,
|
||||||
|
geo_coords: row.cells[11].textContent,
|
||||||
|
kupsat_coords: row.cells[12].textContent,
|
||||||
|
valid_coords: row.cells[13].textContent,
|
||||||
|
updated_at: row.cells[14].textContent,
|
||||||
|
updated_by: row.cells[15].textContent,
|
||||||
|
created_at: row.cells[16].textContent,
|
||||||
|
created_by: row.cells[17].textContent
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectedItems.push(rowData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the counter
|
||||||
|
if (typeof updateSelectedCounter === 'function') {
|
||||||
|
updateSelectedCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selections in the main table
|
||||||
|
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
itemCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
const selectAllCheckbox = document.getElementById('select-all');
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert(`Добавлено ${checkedCheckboxes.length} элемент(ов) в список. Всего: ${window.selectedItems.length} элемент(ов).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(initColumnVisibility, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to populate the selected items table in the offcanvas
|
||||||
|
function populateSelectedItemsTable() {
|
||||||
|
const tableBody = document.getElementById('selected-items-table-body');
|
||||||
|
if (!tableBody) return;
|
||||||
|
|
||||||
|
// Clear existing rows
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
// Add rows for each selected item
|
||||||
|
window.selectedItems.forEach((item, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="text-center">
|
||||||
|
<input type="checkbox" class="form-check-input selected-item-checkbox" value="${item.id}">
|
||||||
|
</td>
|
||||||
|
<td>${item.name}</td>
|
||||||
|
<td>${item.satellite}</td>
|
||||||
|
<td>${item.frequency}</td>
|
||||||
|
<td>${item.freq_range}</td>
|
||||||
|
<td>${item.polarization}</td>
|
||||||
|
<td>${item.bod_velocity}</td>
|
||||||
|
<td>${item.modulation}</td>
|
||||||
|
<td>${item.snr}</td>
|
||||||
|
<td>${item.geo_timestamp}</td>
|
||||||
|
<td>${item.geo_location}</td>
|
||||||
|
<td>${item.geo_coords}</td>
|
||||||
|
<td>${item.kupsat_coords}</td>
|
||||||
|
<td>${item.valid_coords}</td>
|
||||||
|
<td>${item.updated_at}</td>
|
||||||
|
<td>${item.updated_by}</td>
|
||||||
|
<td>${item.created_at}</td>
|
||||||
|
<td>${item.created_by}</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to remove selected items from the list
|
||||||
|
function removeSelectedItems() {
|
||||||
|
const checkboxes = document.querySelectorAll('#selected-items-table-body .selected-item-checkbox:checked');
|
||||||
|
if (checkboxes.length === 0) {
|
||||||
|
alert('Пожалуйста, выберите хотя бы один элемент для удаления из списка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get IDs of items to remove
|
||||||
|
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
|
||||||
|
|
||||||
|
// Remove items from the selectedItems array
|
||||||
|
window.selectedItems = window.selectedItems.filter(item => !idsToRemove.includes(item.id));
|
||||||
|
|
||||||
|
// Update the counter and table
|
||||||
|
if (typeof updateSelectedCounter === 'function') {
|
||||||
|
updateSelectedCounter();
|
||||||
|
}
|
||||||
|
populateSelectedItemsTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send selected items (placeholder)
|
||||||
|
function sendSelectedItems() {
|
||||||
|
const selectedCount = document.querySelectorAll('#selected-items-table-body .selected-item-checkbox:checked').length;
|
||||||
|
if (selectedCount === 0) {
|
||||||
|
alert('Пожалуйста, выберите хотя бы один элемент для отправки');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`Отправка ${selectedCount} элементов... (функция в разработке)`);
|
||||||
|
// Placeholder for actual send functionality
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to toggle all checkboxes in the selected items table
|
||||||
|
function toggleAllSelected(checkbox) {
|
||||||
|
const checkboxes = document.querySelectorAll('#selected-items-table-body .selected-item-checkbox');
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.checked = checkbox.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the selected items table when the offcanvas is shown
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const offcanvasElement = document.getElementById('selectedItemsOffcanvas');
|
||||||
|
if (offcanvasElement) {
|
||||||
|
offcanvasElement.addEventListener('show.bs.offcanvas', function() {
|
||||||
|
populateSelectedItemsTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Include the selected items offcanvas component -->
|
||||||
|
{% include 'mainapp/components/_selected_items_offcanvas.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -23,5 +23,6 @@ urlpatterns = [
|
|||||||
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
|
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
|
||||||
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||||
]
|
]
|
||||||
@@ -724,6 +724,9 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
bod_velocity = "-"
|
bod_velocity = "-"
|
||||||
modulation_name = "-"
|
modulation_name = "-"
|
||||||
snr = "-"
|
snr = "-"
|
||||||
|
standard_name = "-"
|
||||||
|
comment = "-"
|
||||||
|
is_average = "-"
|
||||||
|
|
||||||
if param:
|
if param:
|
||||||
if hasattr(param, "id_satellite") and param.id_satellite:
|
if hasattr(param, "id_satellite") and param.id_satellite:
|
||||||
@@ -760,6 +763,17 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
else "-"
|
else "-"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if hasattr(param, "standard") and param.standard:
|
||||||
|
standard_name = (
|
||||||
|
param.standard.name
|
||||||
|
if hasattr(param.standard, "name")
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(obj, "geo_obj") and obj.geo_obj:
|
||||||
|
comment = obj.geo_obj.comment or "-"
|
||||||
|
is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
|
||||||
|
|
||||||
processed_objects.append(
|
processed_objects.append(
|
||||||
{
|
{
|
||||||
"id": obj.id,
|
"id": obj.id,
|
||||||
@@ -780,6 +794,9 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
"distance_geo_valid": distance_geo_valid,
|
"distance_geo_valid": distance_geo_valid,
|
||||||
"distance_kup_valid": distance_kup_valid,
|
"distance_kup_valid": distance_kup_valid,
|
||||||
"updated_by": obj.updated_by if obj.updated_by else "-",
|
"updated_by": obj.updated_by if obj.updated_by else "-",
|
||||||
|
"comment": comment,
|
||||||
|
"is_average": is_average,
|
||||||
|
"standard": standard_name,
|
||||||
"obj": obj,
|
"obj": obj,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -840,10 +857,11 @@ class ObjItemFormView(
|
|||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Возвращает URL с сохраненными параметрами фильтров."""
|
"""Возвращает URL с сохраненными параметрами фильтров."""
|
||||||
# Получаем сохраненные параметры из GET запроса
|
# Получаем все параметры из GET запроса и сохраняем их в URL
|
||||||
return_params = self.request.GET.get('return_params', '')
|
if self.request.GET:
|
||||||
if return_params:
|
from urllib.parse import urlencode
|
||||||
return reverse_lazy("mainapp:objitem_list") + '?' + return_params
|
query_string = urlencode(self.request.GET)
|
||||||
|
return reverse_lazy("mainapp:objitem_list") + '?' + query_string
|
||||||
return reverse_lazy("mainapp:objitem_list")
|
return reverse_lazy("mainapp:objitem_list")
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@@ -890,7 +908,11 @@ class ObjItemFormView(
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
context = self.get_context_data()
|
context = self.get_context_data()
|
||||||
parameter_forms = context["parameter_forms"]
|
parameter_forms = context["parameter_forms"]
|
||||||
geo_form = context["geo_form"]
|
|
||||||
|
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
||||||
|
geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
|
||||||
|
else:
|
||||||
|
geo_form = GeoForm(self.request.POST, prefix="geo")
|
||||||
|
|
||||||
# Сохраняем основной объект
|
# Сохраняем основной объект
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
@@ -902,7 +924,15 @@ class ObjItemFormView(
|
|||||||
self.save_parameters(parameter_forms)
|
self.save_parameters(parameter_forms)
|
||||||
|
|
||||||
# Сохраняем геоданные
|
# Сохраняем геоданные
|
||||||
|
if geo_form.is_valid():
|
||||||
self.save_geo_data(geo_form)
|
self.save_geo_data(geo_form)
|
||||||
|
else:
|
||||||
|
context.update({
|
||||||
|
'form': form,
|
||||||
|
'parameter_forms': parameter_forms,
|
||||||
|
'geo_form': geo_form,
|
||||||
|
})
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@@ -1011,6 +1041,48 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
|
|||||||
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
template_name = "mainapp/objitem_confirm_delete.html"
|
template_name = "mainapp/objitem_confirm_delete.html"
|
||||||
success_url = reverse_lazy("mainapp:home")
|
success_url = reverse_lazy("mainapp:objitem_list")
|
||||||
success_message = "Объект успешно удалён!"
|
success_message = "Объект успешно удалён!"
|
||||||
required_roles = ["admin", "moderator"]
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
"""Возвращает URL с сохраненными параметрами фильтров."""
|
||||||
|
# Получаем все параметры из GET запроса и сохраняем их в URL
|
||||||
|
if self.request.GET:
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
query_string = urlencode(self.request.GET)
|
||||||
|
return reverse_lazy("mainapp:objitem_list") + '?' + query_string
|
||||||
|
return reverse_lazy("mainapp:objitem_list")
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemDetailView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Представление для просмотра деталей ObjItem в режиме чтения.
|
||||||
|
|
||||||
|
Доступно для всех авторизованных пользователей, показывает данные в режиме чтения.
|
||||||
|
"""
|
||||||
|
def get(self, request, pk):
|
||||||
|
obj = ObjItem.objects.filter(pk=pk).select_related(
|
||||||
|
'geo_obj',
|
||||||
|
'updated_by__user',
|
||||||
|
'created_by__user',
|
||||||
|
).prefetch_related(
|
||||||
|
'parameters_obj__id_satellite',
|
||||||
|
'parameters_obj__polarization',
|
||||||
|
'parameters_obj__modulation',
|
||||||
|
'parameters_obj__standard',
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
from django.http import Http404
|
||||||
|
raise Http404("Объект не найден")
|
||||||
|
|
||||||
|
# Сохраняем параметры возврата для кнопки "Назад"
|
||||||
|
return_params = request.GET.get('return_params', '')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': obj,
|
||||||
|
'return_params': return_params
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "mainapp/objitem_detail.html", context)
|
||||||
|
|||||||
Reference in New Issue
Block a user