Виджет с усреднёнными точками на карте
This commit is contained in:
@@ -16,6 +16,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Объекты</a>
|
||||
</li>
|
||||
<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 'mainapp:actions' %}">Действия</a>
|
||||
</li>
|
||||
|
||||
237
dbapp/mainapp/templates/mainapp/source_averaging_map.html
Normal file
237
dbapp/mainapp/templates/mainapp/source_averaging_map.html
Normal file
@@ -0,0 +1,237 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Визуализация усреднения источника #{{ source_id }}{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.legend {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
font-size: 11px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.legend h6 {
|
||||
font-size: 13px;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.legend-item {
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.legend-marker {
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
margin-right: 6px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.legend-info {
|
||||
background: #f8f9fa;
|
||||
padding: 6px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Инициализация карты
|
||||
let map = L.map('map').setView([55.75, 37.62], 10);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
// Цвета для маркеров
|
||||
var markerColors = {
|
||||
'red': 'red',
|
||||
'orange': 'orange',
|
||||
'blue': 'blue',
|
||||
'green': 'green',
|
||||
'purple': 'violet',
|
||||
'cyan': 'yellow',
|
||||
'violet': 'violet'
|
||||
};
|
||||
|
||||
var getColorIcon = function(color) {
|
||||
var iconColor = markerColors[color] || color;
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + iconColor + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
};
|
||||
|
||||
var overlays = [];
|
||||
|
||||
// Создаём слои для каждой группы
|
||||
{% for group in groups %}
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = '{{ group.color }}';
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var popupContent = '';
|
||||
{% if point_data.name %}
|
||||
popupContent += '<strong>{{ point_data.name|escapejs }}</strong><br>';
|
||||
{% endif %}
|
||||
{% if point_data.frequency %}
|
||||
popupContent += 'Частота: {{ point_data.frequency|escapejs }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.objitem_id %}
|
||||
popupContent += 'ObjItem ID: {{ point_data.objitem_id }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.step %}
|
||||
popupContent += '{{ point_data.step|escapejs }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.distance %}
|
||||
popupContent += 'Расстояние: {{ point_data.distance|escapejs }}<br>';
|
||||
{% endif %}
|
||||
{% if point_data.source_id %}
|
||||
popupContent += '{{ point_data.source_id|escapejs }}<br>';
|
||||
{% endif %}
|
||||
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(popupContent || 'Точка {{ forloop.counter }}');
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
var label = "{{ forloop.counter }}";
|
||||
{% if point_data.name %}
|
||||
label += " - {{ point_data.name|escapejs|truncatechars:30 }}";
|
||||
{% elif point_data.step %}
|
||||
label += " - {{ point_data.step|escapejs }}";
|
||||
{% elif point_data.source_id %}
|
||||
label += " - {{ point_data.source_id|escapejs }}";
|
||||
{% endif %}
|
||||
|
||||
subgroup.push({
|
||||
label: label,
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: groupName + ' ({{ group.points|length }})',
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Корневая группа
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
{% for point_data in group.points %}
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Добавляем легенду в левый нижний угол
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML = '<h6>Визуализация усреднения</h6>';
|
||||
div.innerHTML += '<div class="legend-info">';
|
||||
div.innerHTML += 'Источник: #{{ source_id }}<br>';
|
||||
div.innerHTML += 'Всего точек: {{ total_points }}<br>';
|
||||
div.innerHTML += 'Шагов усреднения: {{ total_steps }}';
|
||||
div.innerHTML += '</div>';
|
||||
|
||||
{% for group in groups %}
|
||||
div.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}${markerColors['{{ group.color }}'] || '{{ group.color }}'}.png');"></div>
|
||||
<span>{{ group.name|escapejs }} ({{ group.points|length }})</span>
|
||||
</div>
|
||||
`;
|
||||
{% endfor %}
|
||||
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
137
dbapp/mainapp/templates/mainapp/source_bulk_delete_confirm.html
Normal file
137
dbapp/mainapp/templates/mainapp/source_bulk_delete_confirm.html
Normal file
@@ -0,0 +1,137 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Подтверждение удаления источников{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i> Подтверждение удаления источников
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="bi bi-exclamation-circle"></i> Внимание!
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Вы собираетесь удалить <strong>{{ total_sources }}</strong> источник(ов).
|
||||
Это действие также удалит <strong>{{ total_objitems }}</strong> связанных точек.
|
||||
</p>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<strong>Это действие необратимо!</strong> Все данные будут безвозвратно удалены.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
||||
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 15%;">ID источника</th>
|
||||
<th class="text-center" style="width: 20%;">Кол-во точек</th>
|
||||
<th style="width: 65%;">Спутники</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for source in sources_info %}
|
||||
<tr>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-primary">{{ source.objitem_count }}</span>
|
||||
</td>
|
||||
<td>{{ source.satellites }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4" role="alert">
|
||||
<h6 class="alert-heading">Что будет удалено:</h6>
|
||||
<ul class="mb-0">
|
||||
<li><strong>{{ total_sources }}</strong> источник(ов)</li>
|
||||
<li><strong>{{ total_objitems }}</strong> точек ГЛ</li>
|
||||
<li>Все связанные геолокационные данные</li>
|
||||
<li>Все связанные параметры</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="ids" value="{{ ids }}">
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('deleteForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('confirmDeleteBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Удаление...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
fetch('{% url "mainapp:delete_selected_sources" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken,
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
window.location.href = '{% url "mainapp:home" %}';
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении источников');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -67,6 +67,12 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedSources()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
@@ -100,6 +106,24 @@
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<form method="get" id="filter-form">
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<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('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Coordinates Average Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Усредненные координаты:</label>
|
||||
@@ -168,9 +192,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ObjItem Count Filter -->
|
||||
<!-- Point Count Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество ObjItem:</label>
|
||||
<label class="form-label">Количество точек:</label>
|
||||
<input type="number" name="objitem_count_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ objitem_count_min|default:'' }}">
|
||||
<input type="number" name="objitem_count_max" class="form-control form-control-sm"
|
||||
@@ -217,13 +241,14 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">Спутник</th>
|
||||
<th scope="col" style="min-width: 150px;">Усредненные координаты</th>
|
||||
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
|
||||
<th scope="col" style="min-width: 150px;">Координаты оперативников</th>
|
||||
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
|
||||
<th scope="col" class="text-center" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||
Кол-во ObjItem
|
||||
Кол-во точек
|
||||
{% if sort == 'objitem_count' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-objitem_count' %}
|
||||
@@ -262,6 +287,7 @@
|
||||
value="{{ source.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td>{{ source.satellite }}</td>
|
||||
<td>{{ source.coords_average }}</td>
|
||||
<td>{{ source.coords_kupsat }}</td>
|
||||
<td>{{ source.coords_valid }}</td>
|
||||
@@ -285,6 +311,19 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if source.objitem_count > 1 %}
|
||||
<a href="{% url 'mainapp:show_source_averaging_map' source.id %}"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-info"
|
||||
title="Визуализация усреднения">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно точек для усреднения">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})"
|
||||
title="Показать детали">
|
||||
@@ -307,7 +346,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="11" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -335,7 +374,7 @@
|
||||
</div>
|
||||
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||||
<div id="modalContent" style="display: none;">
|
||||
<h6>Связанные объекты (<span id="objitemCount">0</span>):</h6>
|
||||
<h6>Связанные точки (<span id="objitemCount">0</span>):</h6>
|
||||
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-light sticky-top">
|
||||
@@ -364,7 +403,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="modalNoData" class="text-center text-muted py-4" style="display: none;">
|
||||
Нет связанных объектов
|
||||
Нет связанных точек
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -427,6 +466,27 @@ function showSelectedOnMap() {
|
||||
window.open(url, '_blank'); // Open in a new tab
|
||||
}
|
||||
|
||||
// Function to delete selected sources
|
||||
function deleteSelectedSources() {
|
||||
// Get all checked checkboxes
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один источник для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IDs from checked checkboxes
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
// Redirect to confirmation page
|
||||
const url = '{% url "mainapp:delete_selected_sources" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function performSearch() {
|
||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||
@@ -501,6 +561,16 @@ function setupRadioLikeCheckboxes(name) {
|
||||
});
|
||||
}
|
||||
|
||||
// Function to select/deselect all options in a select element
|
||||
function selectAllOptions(selectName, selectAll) {
|
||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||||
if (selectElement) {
|
||||
for (let i = 0; i < selectElement.options.length; i++) {
|
||||
selectElement.options[i].selected = selectAll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter counter functionality
|
||||
function updateFilterCounter() {
|
||||
const form = document.getElementById('filter-form');
|
||||
@@ -510,6 +580,19 @@ function updateFilterCounter() {
|
||||
// Count non-empty form fields
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
// For multi-select fields, skip counting individual selections
|
||||
if (key === 'satellite_id') {
|
||||
continue;
|
||||
}
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count selected options in satellite multi-select field
|
||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
if (satelliteSelect) {
|
||||
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(opt => opt.selected);
|
||||
if (selectedOptions.length > 0) {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +652,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
input.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const selectFields = form.querySelectorAll('select');
|
||||
selectFields.forEach(select => {
|
||||
select.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxFields.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateFilterCounter);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Подтверждение удаления транспондеров{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i> Подтверждение удаления транспондеров
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h5 class="alert-heading">
|
||||
<i class="bi bi-exclamation-circle"></i> Внимание!
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Вы собираетесь удалить <strong>{{ total_transponders }}</strong> транспондер(ов).
|
||||
Это действие также удалит <strong>{{ total_objitems }}</strong> связанных точек.
|
||||
</p>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<strong>Это действие необратимо!</strong> Все данные будут безвозвратно удалены.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4 mb-3">Детали удаления:</h5>
|
||||
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 10%;">ID</th>
|
||||
<th style="width: 20%;">Название</th>
|
||||
<th style="width: 20%;">Спутник</th>
|
||||
<th class="text-center" style="width: 15%;">Downlink, МГц</th>
|
||||
<th class="text-center" style="width: 15%;">Полоса, МГц</th>
|
||||
<th class="text-center" style="width: 20%;">Кол-во точек</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transponder in transponders_info %}
|
||||
<tr>
|
||||
<td class="text-center">{{ transponder.id }}</td>
|
||||
<td>{{ transponder.name }}</td>
|
||||
<td>{{ transponder.satellite }}</td>
|
||||
<td class="text-center">{{ transponder.downlink }}</td>
|
||||
<td class="text-center">{{ transponder.frequency_range }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-primary">{{ transponder.objitem_count }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4" role="alert">
|
||||
<h6 class="alert-heading">Что будет удалено:</h6>
|
||||
<ul class="mb-0">
|
||||
<li><strong>{{ total_transponders }}</strong> транспондер(ов)</li>
|
||||
<li><strong>{{ total_objitems }}</strong> точек ГЛ</li>
|
||||
<li>Все связанные геолокационные данные</li>
|
||||
<li>Все связанные параметры</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="ids" value="{{ ids }}">
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'mainapp:transponder_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('deleteForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('confirmDeleteBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Удаление...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
fetch('{% url "mainapp:delete_selected_transponders" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken,
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
window.location.href = '{% url "mainapp:transponder_list" %}';
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении транспондеров');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Подтвердить удаление';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
280
dbapp/mainapp/templates/mainapp/transponder_form.html
Normal file
280
dbapp/mainapp/templates/mainapp/transponder_form.html
Normal file
@@ -0,0 +1,280 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if action == 'update' and objitem_count > 0 %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
С этим транспондером связано <strong>{{ objitem_count }}</strong> точек ГЛ.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="transponderForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Основная информация -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Основная информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ form.name.label }}
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.sat_id.id_for_label }}" class="form-label">
|
||||
{{ form.sat_id.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.sat_id }}
|
||||
{% if form.sat_id.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.sat_id.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.zone_name.id_for_label }}" class="form-label">
|
||||
{{ form.zone_name.label }}
|
||||
</label>
|
||||
{{ form.zone_name }}
|
||||
{% if form.zone_name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.zone_name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.zone_name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.zone_name.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.polarization.id_for_label }}" class="form-label">
|
||||
{{ form.polarization.label }}
|
||||
</label>
|
||||
{{ form.polarization }}
|
||||
{% if form.polarization.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.polarization.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Частотные параметры -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Частотные параметры</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.downlink.id_for_label }}" class="form-label">
|
||||
{{ form.downlink.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.downlink }}
|
||||
{% if form.downlink.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.downlink.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.downlink.help_text %}
|
||||
<small class="form-text text-muted">{{ form.downlink.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.uplink.id_for_label }}" class="form-label">
|
||||
{{ form.uplink.label }}
|
||||
</label>
|
||||
{{ form.uplink }}
|
||||
{% if form.uplink.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.uplink.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.uplink.help_text %}
|
||||
<small class="form-text text-muted">{{ form.uplink.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.frequency_range.id_for_label }}" class="form-label">
|
||||
{{ form.frequency_range.label }}
|
||||
</label>
|
||||
{{ form.frequency_range }}
|
||||
{% if form.frequency_range.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.frequency_range.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.frequency_range.help_text %}
|
||||
<small class="form-text text-muted">{{ form.frequency_range.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if action == 'update' %}
|
||||
<small class="form-text text-muted">
|
||||
<i class="bi bi-lock"></i> Поле только для чтения при редактировании
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if action == 'update' and object.transfer %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Перенос (вычисляемое поле)</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.transfer|floatformat:3 }} МГц
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
Автоматически вычисляется как |Downlink - Uplink|
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные параметры -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Дополнительные параметры</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.snr.id_for_label }}" class="form-label">
|
||||
{{ form.snr.label }}
|
||||
</label>
|
||||
{{ form.snr }}
|
||||
{% if form.snr.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.snr.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.snr.help_text %}
|
||||
<small class="form-text text-muted">{{ form.snr.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Метаданные (только при редактировании) -->
|
||||
{% if action == 'update' %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Метаданные</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Создано</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.created_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Создал</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.created_by|default:"-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Обновлено</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.updated_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Обновил</label>
|
||||
<div class="form-control" readonly style="background-color: #e9ecef;">
|
||||
{{ object.updated_by|default:"-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'mainapp:transponder_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Назад к списку
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('transponderForm');
|
||||
|
||||
// Disable readonly fields on submit to prevent them from being sent
|
||||
form.addEventListener('submit', function(e) {
|
||||
const readonlyFields = form.querySelectorAll('[readonly], [disabled]');
|
||||
readonlyFields.forEach(field => {
|
||||
// Don't disable if it's just a display field (div)
|
||||
if (field.tagName === 'SELECT' || field.tagName === 'INPUT') {
|
||||
field.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
577
dbapp/mainapp/templates/mainapp/transponder_list.html
Normal file
577
dbapp/mainapp/templates/mainapp/transponder_list.html
Normal file
@@ -0,0 +1,577 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Список транспондеров{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.table-responsive tr.selected {
|
||||
background-color: #d4edff;
|
||||
}
|
||||
.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>Список транспондеров</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<!-- Search bar -->
|
||||
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||||
<div class="input-group">
|
||||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск..."
|
||||
value="{{ search_query|default:'' }}">
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
onclick="performSearch()">Найти</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
onclick="clearSearch()">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items per page select -->
|
||||
<div>
|
||||
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
||||
<select name="items_per_page" id="items-per-page"
|
||||
class="form-select form-select-sm d-inline-block" style="width: auto;"
|
||||
onchange="updateItemsPerPage()">
|
||||
{% for option in available_items_per_page %}
|
||||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||||
{{ option }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedTransponders()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
</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>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="ms-auto">
|
||||
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 method="get" id="filter-form">
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<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('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Polarization Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<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('polarization', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="selectAllOptions('polarization', false)">Снять</button>
|
||||
</div>
|
||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for pol in polarizations %}
|
||||
<option value="{{ pol.id }}" {% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||
{{ pol.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Downlink Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Downlink, МГц:</label>
|
||||
<input type="number" step="0.001" name="downlink_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ downlink_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="downlink_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ downlink_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Uplink Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Uplink, МГц:</label>
|
||||
<input type="number" step="0.001" name="uplink_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ uplink_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="uplink_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ uplink_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Frequency Range Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Полоса, МГц:</label>
|
||||
<input type="number" step="0.001" name="freq_range_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ freq_range_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="freq_range_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ freq_range_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- SNR Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">ОСШ, дБ:</label>
|
||||
<input type="number" step="0.1" name="snr_min" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ snr_min|default:'' }}">
|
||||
<input type="number" step="0.1" name="snr_max" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ snr_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ date_from|default:'' }}">
|
||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||
<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" class="form-check-input">
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 60px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
||||
ID
|
||||
{% if sort == 'id' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-id' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('name')" class="text-white text-decoration-none">
|
||||
Название
|
||||
{% if sort == 'name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('sat_id__name')" class="text-white text-decoration-none">
|
||||
Спутник
|
||||
{% if sort == 'sat_id__name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-sat_id__name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('downlink')" class="text-white text-decoration-none">
|
||||
Downlink, МГц
|
||||
{% if sort == 'downlink' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-downlink' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('uplink')" class="text-white text-decoration-none">
|
||||
Uplink, МГц
|
||||
{% if sort == 'uplink' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-uplink' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('frequency_range')" class="text-white text-decoration-none">
|
||||
Полоса, МГц
|
||||
{% if sort == 'frequency_range' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-frequency_range' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 100px;">Перенос, МГц</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('zone_name')" class="text-white text-decoration-none">
|
||||
Зона покрытия
|
||||
{% if sort == 'zone_name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-zone_name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('polarization__name')" class="text-white text-decoration-none">
|
||||
Поляризация
|
||||
{% if sort == 'polarization__name' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-polarization__name' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('snr')" class="text-white text-decoration-none">
|
||||
ОСШ, дБ
|
||||
{% if sort == 'snr' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-snr' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 80px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||
Кол-во точек
|
||||
{% if sort == 'objitem_count' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-objitem_count' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('created_at')" class="text-white text-decoration-none">
|
||||
Создано
|
||||
{% if sort == 'created_at' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-created_at' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" style="min-width: 120px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('updated_at')" class="text-white text-decoration-none">
|
||||
Обновлено
|
||||
{% if sort == 'updated_at' %}
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
{% elif sort == '-updated_at' %}
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 100px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transponder in processed_transponders %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ transponder.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ transponder.id }}</td>
|
||||
<td>{{ transponder.name }}</td>
|
||||
<td>{{ transponder.satellite }}</td>
|
||||
<td>{{ transponder.downlink }}</td>
|
||||
<td>{{ transponder.uplink }}</td>
|
||||
<td>{{ transponder.frequency_range }}</td>
|
||||
<td>{{ transponder.transfer }}</td>
|
||||
<td>{{ transponder.zone_name }}</td>
|
||||
<td>{{ transponder.polarization }}</td>
|
||||
<td>{{ transponder.snr }}</td>
|
||||
<td class="text-center">{{ transponder.objitem_count }}</td>
|
||||
<td>{{ transponder.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ transponder.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-center">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:transponder_update' transponder.id %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Редактировать транспондер">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="15" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let lastCheckedIndex = null;
|
||||
|
||||
function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
row.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxClick(e) {
|
||||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||||
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||||
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
updateRowHighlight(checkboxes[i]);
|
||||
}
|
||||
} else {
|
||||
updateRowHighlight(e.target);
|
||||
}
|
||||
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||||
}
|
||||
|
||||
// Function to delete selected transponders
|
||||
function deleteSelectedTransponders() {
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один транспондер для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
const url = '{% url "mainapp:delete_selected_transponders" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function performSearch() {
|
||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (searchValue) {
|
||||
urlParams.set('search', searchValue);
|
||||
} else {
|
||||
urlParams.delete('search');
|
||||
}
|
||||
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
document.getElementById('toolbar-search').value = '';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.delete('search');
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Items per page functionality
|
||||
function updateItemsPerPage() {
|
||||
const itemsPerPage = document.getElementById('items-per-page').value;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set('items_per_page', itemsPerPage);
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Sorting functionality
|
||||
function updateSort(field) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentSort = urlParams.get('sort');
|
||||
|
||||
let newSort;
|
||||
if (currentSort === field) {
|
||||
newSort = '-' + field;
|
||||
} else if (currentSort === '-' + field) {
|
||||
newSort = field;
|
||||
} else {
|
||||
newSort = field;
|
||||
}
|
||||
|
||||
urlParams.set('sort', newSort);
|
||||
urlParams.delete('page');
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Function to select/deselect all options in a select element
|
||||
function selectAllOptions(selectName, selectAll) {
|
||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||||
if (selectElement) {
|
||||
for (let i = 0; i < selectElement.options.length; i++) {
|
||||
selectElement.options[i].selected = selectAll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter counter functionality
|
||||
function updateFilterCounter() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const formData = new FormData(form);
|
||||
let filterCount = 0;
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
if (key === 'satellite_id' || key === 'polarization') {
|
||||
continue;
|
||||
}
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count selected options in multi-select fields
|
||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
if (satelliteSelect) {
|
||||
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(opt => opt.selected);
|
||||
if (selectedOptions.length > 0) {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const polarizationSelect = document.querySelector('select[name="polarization"]');
|
||||
if (polarizationSelect) {
|
||||
const selectedOptions = Array.from(polarizationSelect.selectedOptions).filter(opt => opt.selected);
|
||||
if (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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
});
|
||||
});
|
||||
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
});
|
||||
|
||||
checkbox.addEventListener('click', handleCheckboxClick);
|
||||
});
|
||||
}
|
||||
|
||||
updateFilterCounter();
|
||||
|
||||
const form = document.getElementById('filter-form');
|
||||
if (form) {
|
||||
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||||
inputFields.forEach(input => {
|
||||
input.addEventListener('input', updateFilterCounter);
|
||||
input.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const selectFields = form.querySelectorAll('select');
|
||||
selectFields.forEach(select => {
|
||||
select.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
}
|
||||
|
||||
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user