Files
dbstorage/dbapp/mainapp/templates/mainapp/source_list.html

2868 lines
147 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'mainapp/base.html' %}
{% load static %}
{% load static leaflet_tags %}
{% block title %}Список объектов{% endblock %}
{% block extra_css %}
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-draw/leaflet.draw.css' %}" rel="stylesheet">
<style>
.table-responsive tr.selected {
background-color: #d4edff;
}
.sticky-top {
position: sticky;
top: 0;
z-index: 10;
}
.btn-group .badge {
position: absolute;
top: -5px;
right: -5px;
font-size: 0.65rem;
padding: 0.2em 0.4em;
}
.btn-group .btn {
position: relative;
}
#polygonFilterMap {
z-index: 1;
}
</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="Поиск по ID или имени..."
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:source_create' %}" class="btn btn-success btn-sm" title="Создать новый источник">
<i class="bi bi-plus-circle"></i> Создать
</a>
{% endif %}
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
Передача точек
</a> -->
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
<i class="bi bi-file-earmark-excel"></i> Excel
</a>
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success btn-sm" title="Загрузка данных из CSV">
<i class="bi bi-file-earmark-text"></i> CSV
</a>
{% 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-primary btn-sm" title="Показать на карте"
onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта
</button>
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
<i class="bi bi-calculator"></i> Усреднение
</a>
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
</a>
<a href="{% url 'mainapp:statistics' %}" class="btn btn-secondary btn-sm" title="Статистика">
<i class="bi bi-bar-chart-line"></i> Статистика
</a>
</div>
<!-- Add to List Button -->
<div>
<button class="btn btn-outline-success btn-sm" type="button" onclick="addSelectedToList()">
<i class="bi bi-plus-circle"></i> Добавить к
</button>
</div>
<!-- Selected Sources Counter Button -->
<div>
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#selectedSourcesOffcanvas" aria-controls="selectedSourcesOffcanvas">
<i class="bi bi-list-check"></i> Список
<span id="selectedSourcesCounter" class="badge bg-info" style="display: none;">0</span>
</button>
</div>
<!-- Filter Toggle Button -->
<div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
</div>
<!-- Column visibility toggle button -->
<div>
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear"></i> Колонки
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="columnVisibilityDropdown" style="max-height: 400px; overflow-y: auto;">
<li>
<label class="dropdown-item">
<input type="checkbox" id="select-all-columns" onchange="toggleAllColumns(this)"> Выбрать всё
</label>
</li>
<li><hr class="dropdown-divider"></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="0" checked onchange="toggleColumn(this)"> Выбрать</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="1" checked onchange="toggleColumn(this)"> ID</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="2" checked onchange="toggleColumn(this)"> Имя</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="3" checked onchange="toggleColumn(this)"> Спутник</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="4" checked onchange="toggleColumn(this)"> Тип объекта</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="5" checked onchange="toggleColumn(this)"> Принадлежность объекта</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="6" checked onchange="toggleColumn(this)"> Координаты ГЛ</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="7" checked onchange="toggleColumn(this)"> Кол-во ГЛ(точек)</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="8" checked onchange="toggleColumn(this)"> Координаты Кубсата</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="9" checked onchange="toggleColumn(this)"> Координаты визуального наблюдения</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Координаты справочные</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Примечание</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="12" onchange="toggleColumn(this)"> Создано</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="13" onchange="toggleColumn(this)"> Обновлено</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Дата подтверждения</label></li>
<!-- <li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Последний сигнал</label></li> -->
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Действия</label></li>
</ul>
</div>
</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">
<!-- Hidden field to preserve polygon filter -->
{% if polygon_coords %}
<input type="hidden" name="polygon" value="{{ polygon_coords }}">
{% endif %}
<!-- Polygon Filter Section -->
<div class="mb-3">
<label class="form-label fw-bold">
<i class="bi bi-pentagon"></i> Фильтр по полигону
</label>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-success btn-sm"
onclick="openPolygonFilterMap()">
<i class="bi bi-pentagon"></i> Нарисовать полигон
{% if polygon_coords %}
<span class="badge bg-success ms-1">✓ Активен</span>
{% endif %}
</button>
{% if polygon_coords %}
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="clearPolygonFilter()" title="Очистить фильтр по полигону">
<i class="bi bi-x-circle"></i> Очистить полигон
</button>
{% endif %}
</div>
</div>
<hr class="my-3">
<!-- 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>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_1"
value="1" {% if has_coords_average == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_average_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_0"
value="0" {% if has_coords_average == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_average_0">Нет</label>
</div>
</div>
</div>
<!-- Kubsat Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты Кубсата:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_1"
value="1" {% if has_coords_kupsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_kupsat_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_0"
value="0" {% if has_coords_kupsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_kupsat_0">Нет</label>
</div>
</div>
</div>
<!-- Valid Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты визуального наблюдения:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_1"
value="1" {% if has_coords_valid == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_valid_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_0"
value="0" {% if has_coords_valid == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_valid_0">Нет</label>
</div>
</div>
</div>
<!-- Reference Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты справочные:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_1"
value="1" {% if has_coords_reference == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_reference_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_0"
value="0" {% if has_coords_reference == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_coords_reference_0">Нет</label>
</div>
</div>
</div>
<!-- ObjectInfo Filter -->
<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('info_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('info_id', false)">Снять</button>
</div>
<select name="info_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for info in object_infos %}
<option value="{{ info.id }}" {% if info.id in selected_info %}selected{% endif %}>
{{ info.name }}
</option>
{% endfor %}
</select>
</div>
<!-- ObjectOwnership Filter -->
<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('ownership_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('ownership_id', false)">Снять</button>
</div>
<select name="ownership_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for ownership in object_ownerships %}
<option value="{{ ownership.id }}" {% if ownership.id in selected_ownership %}selected{% endif %}>
{{ ownership.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Source Requests Filter -->
<div class="mb-2">
<label class="form-label">Заявки на Кубсат:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_requests" id="has_requests_1"
value="1" {% if has_requests == '1' %}checked{% endif %}
onchange="toggleRequestSubfilters()">
<label class="form-check-label" for="has_requests_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_requests" id="has_requests_0"
value="0" {% if has_requests == '0' %}checked{% endif %}
onchange="toggleRequestSubfilters()">
<label class="form-check-label" for="has_requests_0">Нет</label>
</div>
</div>
<!-- Подфильтры заявок (видны только когда выбрано "Есть") -->
<div id="requestSubfilters" class="mt-2 ps-2 border-start border-primary" style="display: {% if has_requests == '1' %}block{% else %}none{% endif %};">
<!-- Статус заявки (мультивыбор) -->
<div class="mb-2">
<label class="form-label small">Статус заявки:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary py-0"
onclick="selectAllOptions('request_status', true)">Все</button>
<button type="button" class="btn btn-sm btn-outline-secondary py-0"
onclick="selectAllOptions('request_status', false)">Снять</button>
</div>
<select name="request_status" class="form-select form-select-sm" multiple size="5">
<option value="planned" {% if 'planned' in selected_request_statuses %}selected{% endif %}>Запланировано</option>
<option value="conducted" {% if 'conducted' in selected_request_statuses %}selected{% endif %}>Проведён</option>
<option value="successful" {% if 'successful' in selected_request_statuses %}selected{% endif %}>Успешно</option>
<option value="no_correlation" {% if 'no_correlation' in selected_request_statuses %}selected{% endif %}>Нет корреляции</option>
<option value="no_signal" {% if 'no_signal' in selected_request_statuses %}selected{% endif %}>Нет сигнала в спектре</option>
<option value="unsuccessful" {% if 'unsuccessful' in selected_request_statuses %}selected{% endif %}>Неуспешно</option>
<option value="downloading" {% if 'downloading' in selected_request_statuses %}selected{% endif %}>Скачивание</option>
<option value="processing" {% if 'processing' in selected_request_statuses %}selected{% endif %}>Обработка</option>
<option value="result_received" {% if 'result_received' in selected_request_statuses %}selected{% endif %}>Результат получен</option>
</select>
</div>
<!-- Приоритет заявки -->
<div class="mb-2">
<label class="form-label small">Приоритет:</label>
<select name="request_priority" class="form-select form-select-sm" multiple size="3">
<option value="low" {% if 'low' in selected_request_priorities %}selected{% endif %}>Низкий</option>
<option value="medium" {% if 'medium' in selected_request_priorities %}selected{% endif %}>Средний</option>
<option value="high" {% if 'high' in selected_request_priorities %}selected{% endif %}>Высокий</option>
</select>
</div>
<!-- ГСО успешно -->
<div class="mb-2">
<label class="form-label small">ГСО успешно:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="request_gso_success" id="request_gso_success_1"
value="true" {% if request_gso_success == 'true' %}checked{% endif %}>
<label class="form-check-label small" for="request_gso_success_1">Да</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="request_gso_success" id="request_gso_success_0"
value="false" {% if request_gso_success == 'false' %}checked{% endif %}>
<label class="form-check-label small" for="request_gso_success_0">Нет</label>
</div>
</div>
</div>
<!-- Кубсат успешно -->
<div class="mb-2">
<label class="form-label small">Кубсат успешно:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="request_kubsat_success" id="request_kubsat_success_1"
value="true" {% if request_kubsat_success == 'true' %}checked{% endif %}>
<label class="form-check-label small" for="request_kubsat_success_1">Да</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="request_kubsat_success" id="request_kubsat_success_0"
value="false" {% if request_kubsat_success == 'false' %}checked{% endif %}>
<label class="form-check-label small" for="request_kubsat_success_0">Нет</label>
</div>
</div>
</div>
<!-- Дата планирования -->
<div class="mb-2">
<label class="form-label small">Дата планирования:</label>
<input type="date" name="request_planned_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ request_planned_from|default:'' }}">
<input type="date" name="request_planned_to" class="form-control form-control-sm"
placeholder="До" value="{{ request_planned_to|default:'' }}">
</div>
<!-- Дата заявки -->
<div class="mb-2">
<label class="form-label small">Дата заявки:</label>
<input type="date" name="request_date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ request_date_from|default:'' }}">
<input type="date" name="request_date_to" class="form-control form-control-sm"
placeholder="До" value="{{ request_date_to|default:'' }}">
</div>
</div>
</div>
<!-- Point Count Filter -->
<div class="mb-2">
<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"
placeholder="До" value="{{ objitem_count_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>
<hr class="my-3">
<h6 class="text-muted mb-2"><i class="bi bi-sliders"></i> Фильтры по параметрам точек</h6>
<!-- Geo Timestamp Filter -->
<div class="mb-2">
<label class="form-label">Дата ГЛ:</label>
<input type="date" name="geo_date_from" id="geo_date_from" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ geo_date_from|default:'' }}">
<input type="date" name="geo_date_to" id="geo_date_to" class="form-control form-control-sm"
placeholder="До" value="{{ geo_date_to|default:'' }}">
</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_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('polarization_id', false)">Снять</button>
</div>
<select name="polarization_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for polarization in polarizations %}
<option value="{{ polarization.id }}" {% if polarization.id in selected_polarizations %}selected{% endif %}>
{{ polarization.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Modulation 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('modulation_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('modulation_id', false)">Снять</button>
</div>
<select name="modulation_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for modulation in modulations %}
<option value="{{ modulation.id }}" {% if modulation.id in selected_modulations %}selected{% endif %}>
{{ modulation.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
placeholder="До" value="{{ freq_max|default:'' }}">
</div>
<!-- Frequency Range (Bandwidth) 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>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Символьная скорость, БОД:</label>
<input type="number" step="0.001" name="bod_velocity_min" class="form-control form-control-sm mb-1"
placeholder="От" value="{{ bod_velocity_min|default:'' }}">
<input type="number" step="0.001" name="bod_velocity_max" class="form-control form-control-sm"
placeholder="До" value="{{ bod_velocity_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>
<!-- Mirrors 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('mirror_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('mirror_id', false)">Снять</button>
</div>
<select name="mirror_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for mirror in mirrors %}
<option value="{{ mirror.id }}" {% if mirror.id in selected_mirrors %}selected{% endif %}>
{{ mirror.name }}
</option>
{% endfor %}
</select>
</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 table-bordered" 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;">
{% include 'mainapp/components/_sort_header.html' with field='id' label='ID' current_sort=sort %}
</th>
<th scope="col" style="min-width: 150px;">Имя</th>
<th scope="col" style="min-width: 120px;">Спутник</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" class="text-center" style="min-width: 100px;">
{% include 'mainapp/components/_sort_header.html' with field='objitem_count' label='Кол-во ГЛ(точек)' current_sort=sort %}
</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: 120px;">Примечание</th>
<th scope="col" style="min-width: 120px;">
{% include 'mainapp/components/_sort_header.html' with field='created_at' label='Создано' current_sort=sort %}
</th>
<th scope="col" style="min-width: 120px;">
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
</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: 150px;">Действия</th>
</tr>
</thead>
<tbody>
{% for source in processed_sources %}
<tr>
<td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox"
value="{{ source.id }}">
</td>
<td class="text-center">{{ source.id }}</td>
<td>{{ source.name }}</td>
<td>
{% if source.satellite_id %}
<a href="#" class="text-decoration-underline"
onclick="showSatelliteModal({{ source.satellite_id }}); return false;">
{{ source.satellite }}
</a>
{% else %}
{{ source.satellite }}
{% endif %}
</td>
<td>{{ source.info }}</td>
<td>
{% if source.ownership == "ТВ" and source.has_lyngsat %}
<a href="#" class="text-primary text-decoration-none"
onclick="showLyngsatModal({{ source.lyngsat_id }}); return false;">
<i class="bi bi-tv"></i> {{ source.ownership }}
</a>
{% else %}
{{ source.ownership }}
{% endif %}
</td>
<td>{{ source.coords_average }}</td>
<td class="text-center">{{ source.objitem_count }}</td>
<td>{{ source.coords_kupsat }}</td>
<td>{{ source.coords_valid }}</td>
<td>{{ source.coords_reference }}</td>
<td>{{ source.note }}</td>
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.confirm_at|date:"d.m.Y H:i"|default:"-" }}</td>
<!-- <td>{{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}</td> -->
<td class="text-center">
<div class="btn-group" role="group">
{% if source.objitem_count > 0 %}
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
target="_blank"
class="btn btn-sm btn-outline-success"
title="Показать объект с точками на карте">
<i class="bi bi-geo-alt"></i>
<span class="badge bg-success">{{ source.objitem_count }}</span>
</a>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Нет точек для отображения">
<i class="bi bi-geo-alt"></i>
</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="Показать детали">
<i class="bi bi-eye"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-info"
onclick="showSourceRequests({{ source.id }})"
title="Заявки на источник">
<i class="bi bi-list-task"></i>
</button>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:source_update' source.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 %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="15" class="text-center text-muted">Нет данных для отображения</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Source Details -->
<div class="modal fade" id="sourceDetailsModal" tabindex="-1" aria-labelledby="sourceDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта #<span id="modalSourceId"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div id="modalLoadingSpinner" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
<div id="modalContent" style="display: none;">
<!-- Marks Section -->
<div id="marksSection" class="mb-3" style="display: none;">
<h6 class="mb-2">Наличие сигнала объекта (<span id="marksCount">0</span>):</h6>
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th style="width: 20%;">Наличие сигнала</th>
<th style="width: 40%;">Дата и время</th>
<th style="width: 40%;">Пользователь</th>
</tr>
</thead>
<tbody id="marksTableBody">
<!-- Marks will be loaded here -->
</tbody>
</table>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
id="modalColumnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear"></i> Колонки
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="modalColumnVisibilityDropdown" style="max-height: 400px; overflow-y: auto;">
<li>
<label class="dropdown-item">
<input type="checkbox" id="modal-select-all-columns" onchange="toggleAllModalColumns(this)"> Выбрать всё
</label>
</li>
<li><hr class="dropdown-divider"></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="0" checked onchange="toggleModalColumn(this)"> Выбрать</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="1" checked onchange="toggleModalColumn(this)"> ID</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="2" checked onchange="toggleModalColumn(this)"> Имя</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="3" checked onchange="toggleModalColumn(this)"> Спутник</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="4" checked onchange="toggleModalColumn(this)"> Транспондер</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="5" checked onchange="toggleModalColumn(this)"> Частота, МГц</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="6" checked onchange="toggleModalColumn(this)"> Полоса, МГц</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="7" checked onchange="toggleModalColumn(this)"> Поляризация</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="8" checked onchange="toggleModalColumn(this)"> Сим. V</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="9" checked onchange="toggleModalColumn(this)"> Модул</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="10" checked onchange="toggleModalColumn(this)"> ОСШ</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="11" checked onchange="toggleModalColumn(this)"> Время ГЛ</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="12" checked onchange="toggleModalColumn(this)"> Местоположение</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="13" checked onchange="toggleModalColumn(this)"> Геолокация</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="14" checked onchange="toggleModalColumn(this)"> Обновлено</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="15" checked onchange="toggleModalColumn(this)"> Кем(обн)</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="16" onchange="toggleModalColumn(this)"> Создано</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="17" onchange="toggleModalColumn(this)"> Кем(созд)</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="18" onchange="toggleModalColumn(this)"> Комментарий</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="19" onchange="toggleModalColumn(this)"> Усреднённое</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="20" onchange="toggleModalColumn(this)"> Стандарт</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип объекта</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="22" onchange="toggleModalColumn(this)"> Sigma</label></li>
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
</ul>
</div>
</div>
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
<thead class="table-light sticky-top">
<tr>
<th class="text-center" style="width: 3%;">
<input type="checkbox" id="modal-select-all" class="form-check-input">
</th>
<th class="text-center" style="min-width: 60px;">ID</th>
<th style="min-width: 120px;">Имя</th>
<th style="min-width: 120px;">Спутник</th>
<th style="min-width: 100px;">Транспондер</th>
<th style="min-width: 100px;">Частота, МГц</th>
<th style="min-width: 100px;">Полоса, МГц</th>
<th style="min-width: 100px;">Поляризация</th>
<th style="min-width: 100px;">Сим. V</th>
<th style="min-width: 100px;">Модул</th>
<th style="min-width: 80px;">ОСШ</th>
<th style="min-width: 120px;">Время ГЛ</th>
<th style="min-width: 120px;">Местоположение</th>
<th style="min-width: 120px;">Геолокация</th>
<th style="min-width: 120px;">Обновлено</th>
<th style="min-width: 100px;">Кем(обн)</th>
<th style="min-width: 120px;">Создано</th>
<th style="min-width: 100px;">Кем(созд)</th>
<th style="min-width: 150px;">Комментарий</th>
<th style="min-width: 100px;">Усреднённое</th>
<th style="min-width: 100px;">Стандарт</th>
<th style="min-width: 100px;">Тип объекта</th>
<th style="min-width: 80px;">Sigma</th>
<th style="min-width: 80px;">Зеркала</th>
</tr>
</thead>
<tbody id="objitemTableBody">
<!-- Data will be loaded here via AJAX -->
</tbody>
</table>
</div>
</div>
<div id="modalNoData" class="text-center text-muted py-4" style="display: none;">
Нет связанных точек
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-draw/leaflet.draw.js' %}"></script>
<script>
// Polygon filter map variables
let polygonFilterMapInstance = null;
let drawnItems = null;
let drawControl = null;
let currentPolygon = null;
// Initialize polygon filter map
function initPolygonFilterMap() {
if (polygonFilterMapInstance) {
return; // Already initialized
}
// Create map centered on Russia
polygonFilterMapInstance = L.map('polygonFilterMap').setView([55.7558, 37.6173], 4);
// Add OpenStreetMap tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(polygonFilterMapInstance);
// Initialize FeatureGroup to store drawn items
drawnItems = new L.FeatureGroup();
polygonFilterMapInstance.addLayer(drawnItems);
// Initialize draw control
drawControl = new L.Control.Draw({
position: 'topright',
draw: {
polygon: {
allowIntersection: false,
showArea: true,
drawError: {
color: '#e1e100',
message: '<strong>Ошибка:</strong> полигон не должен пересекать сам себя!'
},
shapeOptions: {
color: '#3388ff',
fillOpacity: 0.2
}
},
polyline: false,
rectangle: {
shapeOptions: {
color: '#3388ff',
fillOpacity: 0.2
}
},
circle: false,
circlemarker: false,
marker: false
},
edit: {
featureGroup: drawnItems,
remove: true
}
});
polygonFilterMapInstance.addControl(drawControl);
// Handle polygon creation
polygonFilterMapInstance.on(L.Draw.Event.CREATED, function (event) {
const layer = event.layer;
// Remove existing polygon
drawnItems.clearLayers();
// Add new polygon
drawnItems.addLayer(layer);
currentPolygon = layer;
});
// Handle polygon edit
polygonFilterMapInstance.on(L.Draw.Event.EDITED, function (event) {
const layers = event.layers;
layers.eachLayer(function (layer) {
currentPolygon = layer;
});
});
// Handle polygon deletion
polygonFilterMapInstance.on(L.Draw.Event.DELETED, function () {
currentPolygon = null;
});
// Load existing polygon if present
{% if polygon_coords %}
try {
const coords = {{ polygon_coords|safe }};
if (coords && coords.length > 0) {
const latLngs = coords.map(coord => [coord[1], coord[0]]); // [lng, lat] -> [lat, lng]
const polygon = L.polygon(latLngs, {
color: '#3388ff',
fillOpacity: 0.2
});
drawnItems.addLayer(polygon);
currentPolygon = polygon;
// Fit map to polygon bounds
polygonFilterMapInstance.fitBounds(polygon.getBounds());
}
} catch (e) {
console.error('Error loading existing polygon:', e);
}
{% endif %}
}
// Open polygon filter map modal
function openPolygonFilterMap() {
const modal = new bootstrap.Modal(document.getElementById('polygonFilterModal'));
modal.show();
// Initialize map after modal is shown (to ensure proper rendering)
setTimeout(() => {
initPolygonFilterMap();
if (polygonFilterMapInstance) {
polygonFilterMapInstance.invalidateSize();
}
}, 300);
}
// Clear polygon on map
function clearPolygonOnMap() {
if (drawnItems) {
drawnItems.clearLayers();
currentPolygon = null;
}
}
// Apply polygon filter
function applyPolygonFilter() {
if (!currentPolygon) {
alert('Пожалуйста, нарисуйте полигон на карте');
return;
}
// Get polygon coordinates
const latLngs = currentPolygon.getLatLngs()[0]; // Get first ring for polygon
const coords = latLngs.map(latLng => [latLng.lng, latLng.lat]); // [lat, lng] -> [lng, lat]
// Close the polygon by adding first point at the end
coords.push(coords[0]);
// Add polygon coordinates to URL and reload
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('polygon', JSON.stringify(coords));
urlParams.delete('page'); // Reset to first page
window.location.search = urlParams.toString();
}
// Clear polygon filter
function clearPolygonFilter() {
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete('polygon');
urlParams.delete('page');
window.location.search = urlParams.toString();
}
</script>
<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 show selected sources on map
function showSelectedOnMap() {
// 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);
});
// Build URL with IDs and preserve polygon filter if present
const urlParams = new URLSearchParams(window.location.search);
const polygonParam = urlParams.get('polygon');
let url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
if (polygonParam) {
url += '&polygon=' + encodeURIComponent(polygonParam);
}
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();
const urlParams = new URLSearchParams(window.location.search);
if (searchValue) {
urlParams.set('search', searchValue);
} else {
urlParams.delete('search');
}
urlParams.delete('page');
// Preserve polygon filter
// (already in urlParams from window.location.search)
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');
// Preserve polygon filter
// (already in urlParams from window.location.search)
window.location.search = urlParams.toString();
}
// Handle Enter key in search input
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');
// Preserve polygon filter
// (already in urlParams from window.location.search)
window.location.search = urlParams.toString();
}
// Sorting functionality is now handled by sorting.js (loaded via base.html)
// Setup radio-like behavior for filter checkboxes
function setupRadioLikeCheckboxes(name) {
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
if (this.checked) {
checkboxes.forEach(other => {
if (other !== this) {
other.checked = false;
}
});
}
});
});
}
// 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;
}
}
}
// Function to toggle request subfilters visibility
function toggleRequestSubfilters() {
const hasRequestsYes = document.getElementById('has_requests_1');
const subfilters = document.getElementById('requestSubfilters');
if (hasRequestsYes && subfilters) {
if (hasRequestsYes.checked) {
subfilters.style.display = 'block';
} else {
subfilters.style.display = 'none';
}
}
}
// 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, 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++;
}
}
// Check if polygon filter is active
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('polygon')) {
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';
}
}
}
// Column visibility functions with localStorage support
function getColumnVisibilityKey() {
return 'sourceListColumnVisibility';
}
function saveColumnVisibility() {
const columnCheckboxes = document.querySelectorAll('.column-toggle');
const visibility = {};
columnCheckboxes.forEach(checkbox => {
const columnIndex = checkbox.getAttribute('data-column');
visibility[columnIndex] = checkbox.checked;
});
localStorage.setItem(getColumnVisibilityKey(), JSON.stringify(visibility));
}
function loadColumnVisibility() {
const saved = localStorage.getItem(getColumnVisibilityKey());
if (saved) {
return JSON.parse(saved);
}
return null;
}
function toggleColumn(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
const table = document.querySelector('.table');
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
if (checkbox.checked) {
cells.forEach(cell => {
cell.style.display = '';
});
} else {
cells.forEach(cell => {
cell.style.display = 'none';
});
}
// Save state after toggle
saveColumnVisibility();
}
function toggleAllColumns(selectAllCheckbox) {
const columnCheckboxes = document.querySelectorAll('.column-toggle');
columnCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
toggleColumn(checkbox);
});
}
// Initialize column visibility - hide Создано and Обновлено columns by default
function initColumnVisibility() {
const savedVisibility = loadColumnVisibility();
if (savedVisibility) {
// Restore saved state
const columnCheckboxes = document.querySelectorAll('.column-toggle');
columnCheckboxes.forEach(checkbox => {
const columnIndex = checkbox.getAttribute('data-column');
if (savedVisibility.hasOwnProperty(columnIndex)) {
checkbox.checked = savedVisibility[columnIndex];
toggleColumnWithoutSave(checkbox);
}
});
} else {
// Default state: hide Создано and Обновлено columns
const createdAtCheckbox = document.querySelector('input[data-column="11"]');
const updatedAtCheckbox = document.querySelector('input[data-column="12"]');
const noteCheckbox = document.querySelector('input[data-column="13"]');
if (createdAtCheckbox) {
createdAtCheckbox.checked = false;
toggleColumnWithoutSave(createdAtCheckbox);
}
if (updatedAtCheckbox) {
updatedAtCheckbox.checked = false;
toggleColumnWithoutSave(updatedAtCheckbox);
}
if (noteCheckbox) {
noteCheckbox.checked = false;
toggleColumnWithoutSave(noteCheckbox);
}
// Save initial state
saveColumnVisibility();
}
}
// Helper function to toggle without saving (used during initialization)
function toggleColumnWithoutSave(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
const table = document.querySelector('.table');
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
if (checkbox.checked) {
cells.forEach(cell => {
cell.style.display = '';
});
} else {
cells.forEach(cell => {
cell.style.display = 'none';
});
}
}
// Initialize selected sources array from localStorage
function loadSelectedSourcesFromStorage() {
try {
const storedSources = localStorage.getItem('selectedSources');
if (storedSources) {
window.selectedSources = JSON.parse(storedSources);
} else {
window.selectedSources = [];
}
} catch (e) {
console.error('Error loading selected sources from storage:', e);
window.selectedSources = [];
}
}
// Function to save selected sources to localStorage
window.saveSelectedSourcesToStorage = function () {
try {
localStorage.setItem('selectedSources', JSON.stringify(window.selectedSources));
} catch (e) {
console.error('Error saving selected sources to storage:', e);
}
}
// Function to update the selected sources counter
window.updateSelectedSourcesCounter = function () {
const counterElement = document.getElementById('selectedSourcesCounter');
if (window.selectedSources && window.selectedSources.length > 0) {
counterElement.textContent = window.selectedSources.length;
counterElement.style.display = 'inline';
} else {
counterElement.style.display = 'none';
}
}
// Function to add selected sources 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 selectedSources array
checkedCheckboxes.forEach(checkbox => {
const row = checkbox.closest('tr');
const sourceId = checkbox.value;
const sourceExists = window.selectedSources.some(source => source.id === sourceId);
if (!sourceExists) {
const rowData = {
id: sourceId,
name: row.cells[2].textContent.trim(),
satellite: row.cells[3].textContent.trim(),
info: row.cells[4].textContent.trim(),
ownership: row.cells[5].textContent.trim(),
coords_average: row.cells[6].textContent.trim(),
objitem_count: row.cells[7].textContent.trim()
};
window.selectedSources.push(rowData);
}
});
// Update the counter
updateSelectedSourcesCounter();
// Save selected sources to localStorage
saveSelectedSourcesToStorage();
// Clear selections in the main table
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
itemCheckboxes.forEach(checkbox => {
checkbox.checked = false;
updateRowHighlight(checkbox);
});
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Load selected sources from localStorage
loadSelectedSourcesFromStorage();
updateSelectedSourcesCounter();
// Setup select-all checkbox
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;
});
// Add shift-click handler
checkbox.addEventListener('click', handleCheckboxClick);
});
}
// Setup radio-like checkboxes for filters
setupRadioLikeCheckboxes('has_coords_average');
setupRadioLikeCheckboxes('has_coords_kupsat');
setupRadioLikeCheckboxes('has_coords_valid');
setupRadioLikeCheckboxes('has_coords_reference');
setupRadioLikeCheckboxes('has_requests');
setupRadioLikeCheckboxes('request_gso_success');
setupRadioLikeCheckboxes('request_kubsat_success');
// Initialize request subfilters visibility
toggleRequestSubfilters();
// Update filter counter on page load
updateFilterCounter();
// Add event listeners to form elements to update counter when filters change
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 checkboxFields = form.querySelectorAll('input[type="checkbox"]');
checkboxFields.forEach(checkbox => {
checkbox.addEventListener('change', updateFilterCounter);
});
}
// Update counter when offcanvas is shown
const offcanvasElement = document.getElementById('offcanvasFilters');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
// Initialize column visibility
setTimeout(initColumnVisibility, 100);
// Update the selected sources table when the offcanvas is shown
const selectedSourcesOffcanvas = document.getElementById('selectedSourcesOffcanvas');
if (selectedSourcesOffcanvas) {
selectedSourcesOffcanvas.addEventListener('show.bs.offcanvas', function () {
populateSelectedSourcesTable();
});
}
});
// Function to populate the selected sources table in the offcanvas
function populateSelectedSourcesTable() {
const tableBody = document.getElementById('selected-sources-table-body');
const noDataDiv = document.getElementById('selectedSourcesNoData');
const table = tableBody.closest('.table-responsive');
const offcanvasCounter = document.getElementById('selectedSourcesOffcanvasCounter');
if (!tableBody) return;
// Clear existing rows
tableBody.innerHTML = '';
if (!window.selectedSources || window.selectedSources.length === 0) {
// Show no data message
if (table) table.style.display = 'none';
if (noDataDiv) noDataDiv.style.display = 'block';
if (offcanvasCounter) offcanvasCounter.textContent = '0';
return;
}
// Hide no data message and show table
if (table) table.style.display = 'block';
if (noDataDiv) noDataDiv.style.display = 'none';
if (offcanvasCounter) offcanvasCounter.textContent = window.selectedSources.length;
// Add rows for each selected source
window.selectedSources.forEach((source, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="text-center">
<input type="checkbox" class="form-check-input selected-source-checkbox" value="${source.id}">
</td>
<td>${source.id}</td>
<td>${source.name}</td>
<td>${source.satellite}</td>
<td>${source.info}</td>
<td>${source.ownership}</td>
<td>${source.coords_average}</td>
<td class="text-center">${source.objitem_count}</td>
`;
tableBody.appendChild(row);
});
}
// Function to remove selected sources from the list
function removeSelectedSources() {
const checkboxes = document.querySelectorAll('#selected-sources-table-body .selected-source-checkbox:checked');
if (checkboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один источник для удаления из списка');
return;
}
// Get IDs of sources to remove
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
// Remove sources from the selectedSources array
window.selectedSources = window.selectedSources.filter(source => !idsToRemove.includes(source.id));
// Save selected sources to localStorage
saveSelectedSourcesToStorage();
// Update the counter and table
updateSelectedSourcesCounter();
populateSelectedSourcesTable();
}
// Function to show selected sources on map
function showSelectedSourcesOnMap() {
if (!window.selectedSources || window.selectedSources.length === 0) {
alert('Список источников пуст');
return;
}
const selectedIds = window.selectedSources.map(source => source.id);
const urlParams = new URLSearchParams(window.location.search);
const polygonParam = urlParams.get('polygon');
let url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
if (polygonParam) {
url += '&polygon=' + encodeURIComponent(polygonParam);
}
window.open(url, '_blank');
}
// Function to show playback animation for selected sources
function showPlaybackAnimation() {
if (!window.selectedSources || window.selectedSources.length === 0) {
alert('Список источников пуст');
return;
}
// Check if any source has points
const sourcesWithPoints = window.selectedSources.filter(source => parseInt(source.objitem_count) > 0);
if (sourcesWithPoints.length === 0) {
alert('Выбранные источники не содержат точек ГЛ');
return;
}
const selectedIds = window.selectedSources.map(source => source.id);
const url = '{% url "mainapp:multi_sources_playback_map" %}' + '?ids=' + selectedIds.join(',');
window.open(url, '_blank');
}
// Function to merge selected sources
function mergeSelectedSources() {
if (!window.selectedSources || window.selectedSources.length < 2) {
alert('Для объединения необходимо выбрать минимум 2 источника');
return;
}
// Show merge modal
const modal = new bootstrap.Modal(document.getElementById('mergeSourcesModal'));
// Populate target source info
const targetSource = window.selectedSources[0];
document.getElementById('targetSourceInfo').innerHTML = `
<strong>ID:</strong> ${targetSource.id}<br>
<strong>Имя:</strong> ${targetSource.name}<br>
<strong>Спутник:</strong> ${targetSource.satellite}<br>
<strong>Количество точек:</strong> ${targetSource.objitem_count}
`;
// Populate sources to merge list
const sourcesToMergeList = document.getElementById('sourcesToMergeList');
sourcesToMergeList.innerHTML = '';
for (let i = 1; i < window.selectedSources.length; i++) {
const source = window.selectedSources[i];
const li = document.createElement('li');
li.className = 'list-group-item';
li.innerHTML = `
<strong>ID ${source.id}:</strong> ${source.name}
<span class="badge bg-secondary">${source.objitem_count} точек</span>
`;
sourcesToMergeList.appendChild(li);
}
modal.show();
}
// Function to confirm merge
function confirmMerge() {
const infoId = document.getElementById('mergeInfoSelect').value;
const ownershipId = document.getElementById('mergeOwnershipSelect').value;
const note = document.getElementById('mergeNoteTextarea').value;
if (!infoId) {
alert('Пожалуйста, выберите тип объекта');
return;
}
if (!ownershipId) {
alert('Пожалуйста, выберите принадлежность объекта');
return;
}
// Prepare data
const sourceIds = window.selectedSources.map(source => source.id);
// Get CSRF token
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');
// Send AJAX request
fetch('{% url "mainapp:merge_sources" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken
},
body: JSON.stringify({
source_ids: sourceIds,
info_id: infoId,
ownership_id: ownershipId,
note: note
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
// Clear selected sources
window.selectedSources = [];
saveSelectedSourcesToStorage();
updateSelectedSourcesCounter();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('mergeSourcesModal'));
modal.hide();
// Reload page
location.reload();
} else {
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Произошла ошибка при объединении источников');
});
}
// Function to toggle all checkboxes in the selected sources table
function toggleAllSelectedSources(checkbox) {
const checkboxes = document.querySelectorAll('#selected-sources-table-body .selected-source-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
}
// Show source details in modal
function showSourceDetails(sourceId) {
// Update modal title
document.getElementById('modalSourceId').textContent = sourceId;
// Show loading spinner, hide content and error
document.getElementById('modalLoadingSpinner').style.display = 'block';
document.getElementById('modalContent').style.display = 'none';
document.getElementById('modalNoData').style.display = 'none';
document.getElementById('modalErrorMessage').style.display = 'none';
// Open modal
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
modal.show();
// Build URL with filter parameters
const urlParams = new URLSearchParams(window.location.search);
const geoDateFrom = urlParams.get('geo_date_from');
const geoDateTo = urlParams.get('geo_date_to');
let apiUrl = '/api/source/' + sourceId + '/objitems/';
const params = new URLSearchParams();
if (geoDateFrom) {
params.append('geo_date_from', geoDateFrom);
}
if (geoDateTo) {
params.append('geo_date_to', geoDateTo);
}
if (params.toString()) {
apiUrl += '?' + params.toString();
}
// Fetch data from API
fetch(apiUrl)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('Объект не найден');
} else {
throw new Error('Ошибка при загрузке данных');
}
}
return response.json();
})
.then(data => {
// Hide loading spinner
document.getElementById('modalLoadingSpinner').style.display = 'none';
// Show marks if available
if (data.marks && data.marks.length > 0) {
document.getElementById('marksSection').style.display = 'block';
document.getElementById('marksCount').textContent = data.marks.length;
const marksTableBody = document.getElementById('marksTableBody');
marksTableBody.innerHTML = '';
data.marks.forEach(mark => {
const row = document.createElement('tr');
let markBadge = '<span class="badge bg-secondary">-</span>';
if (mark.mark === true) {
markBadge = '<span class="badge bg-success">Есть</span>';
} else if (mark.mark === false) {
markBadge = '<span class="badge bg-danger">Нет</span>';
}
row.innerHTML = '<td class="text-center">' + markBadge + '</td>' +
'<td>' + mark.timestamp + '</td>' +
'<td>' + mark.created_by + '</td>';
marksTableBody.appendChild(row);
});
} else {
document.getElementById('marksSection').style.display = 'none';
}
if (data.objitems && data.objitems.length > 0) {
// Show content
document.getElementById('modalContent').style.display = 'block';
document.getElementById('objitemCount').textContent = data.objitems.length;
// Populate table
const tbody = document.getElementById('objitemTableBody');
tbody.innerHTML = '';
data.objitems.forEach(objitem => {
const row = document.createElement('tr');
// Build transponder cell
let transponderCell = '-';
if (objitem.has_transponder) {
transponderCell = '<a href="#" class="text-decoration-underline" ' +
'onclick="showTransponderModal(' + objitem.transponder_id + '); return false;" ' +
'title="Показать данные транспондера">' +
objitem.transponder_info +
'</a>';
}
// Build LyngSat cell
let lyngsatCell = '-';
if (objitem.has_lyngsat) {
lyngsatCell = '<a href="#" class="text-primary text-decoration-none" ' +
'onclick="showLyngsatModal(' + objitem.lyngsat_id + '); return false;">' +
'<i class="bi bi-tv"></i> ТВ' +
'</a>';
}
// Build Sigma cell
let sigmaCell = '-';
if (objitem.has_sigma) {
sigmaCell = '<a href="#" class="text-info text-decoration-none" ' +
'onclick="showSigmaParameterModal(' + objitem.parameter_id + '); return false;" ' +
'title="' + objitem.sigma_info + '">' +
'<i class="bi bi-graph-up"></i> ' + objitem.sigma_info +
'</a>';
}
// Build satellite cell with link
let satelliteCell = objitem.satellite_name;
if (objitem.satellite_id) {
satelliteCell = '<a href="#" class="text-decoration-underline" ' +
'onclick="showSatelliteModal(' + objitem.satellite_id + '); return false;">' +
objitem.satellite_name +
'</a>';
}
row.innerHTML = '<td class="text-center">' +
'<input type="checkbox" class="form-check-input modal-item-checkbox" value="' + objitem.id + '">' +
'</td>' +
'<td class="text-center">' + objitem.id + '</td>' +
'<td>' + objitem.name + '</td>' +
'<td>' + satelliteCell + '</td>' +
'<td>' + transponderCell + '</td>' +
'<td>' + objitem.frequency + '</td>' +
'<td>' + objitem.freq_range + '</td>' +
'<td>' + objitem.polarization + '</td>' +
'<td>' + objitem.bod_velocity + '</td>' +
'<td>' + objitem.modulation + '</td>' +
'<td>' + objitem.snr + '</td>' +
'<td>' + objitem.geo_timestamp + '</td>' +
'<td>' + objitem.geo_location + '</td>' +
'<td>' + objitem.geo_coords + '</td>' +
'<td>' + objitem.updated_at + '</td>' +
'<td>' + objitem.updated_by + '</td>' +
'<td>' + objitem.created_at + '</td>' +
'<td>' + objitem.created_by + '</td>' +
'<td>' + objitem.comment + '</td>' +
'<td>' + objitem.is_average + '</td>' +
'<td>' + objitem.standard + '</td>' +
'<td>' + lyngsatCell + '</td>' +
'<td>' + sigmaCell + '</td>' +
'<td>' + objitem.mirrors + '</td>';
tbody.appendChild(row);
});
// Setup modal select-all checkbox
setupModalSelectAll();
// Initialize column visibility after DOM update
// Use requestAnimationFrame to ensure DOM is rendered
requestAnimationFrame(() => {
setTimeout(() => {
initModalColumnVisibility();
}, 50);
});
} else {
// Show no data message
document.getElementById('modalNoData').style.display = 'block';
}
})
.catch(error => {
// Hide loading spinner
document.getElementById('modalLoadingSpinner').style.display = 'none';
// Show error message
const errorDiv = document.getElementById('modalErrorMessage');
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
});
}
// Setup select-all functionality for modal
function setupModalSelectAll() {
const modalSelectAll = document.getElementById('modal-select-all');
const modalItemCheckboxes = document.querySelectorAll('.modal-item-checkbox');
if (modalSelectAll && modalItemCheckboxes.length > 0) {
// Remove old event listeners by cloning
const newModalSelectAll = modalSelectAll.cloneNode(true);
modalSelectAll.parentNode.replaceChild(newModalSelectAll, modalSelectAll);
newModalSelectAll.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.modal-item-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = newModalSelectAll.checked;
});
});
modalItemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allCheckboxes = document.querySelectorAll('.modal-item-checkbox');
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
document.getElementById('modal-select-all').checked = allChecked;
});
});
}
}
// Modal column visibility functions with localStorage support
function getModalColumnVisibilityKey() {
return 'sourceListModalColumnVisibility';
}
function saveModalColumnVisibility() {
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
const visibility = {};
columnCheckboxes.forEach(checkbox => {
const columnIndex = checkbox.getAttribute('data-column');
visibility[columnIndex] = checkbox.checked;
});
localStorage.setItem(getModalColumnVisibilityKey(), JSON.stringify(visibility));
}
function loadModalColumnVisibility() {
const saved = localStorage.getItem(getModalColumnVisibilityKey());
if (saved) {
return JSON.parse(saved);
}
return null;
}
// Function to toggle modal column visibility
function toggleModalColumn(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
// Get the specific tbody for objitems
const tbody = document.getElementById('objitemTableBody');
if (!tbody) return;
// Get the parent table
const table = tbody.closest('table');
if (!table) return;
// Get all rows and toggle specific cell in each
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.children;
if (cells[columnIndex]) {
if (checkbox.checked) {
cells[columnIndex].style.removeProperty('display');
} else {
cells[columnIndex].style.setProperty('display', 'none', 'important');
}
}
});
// Save state after toggle
saveModalColumnVisibility();
}
// Function to toggle all modal columns
function toggleAllModalColumns(selectAllCheckbox) {
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
columnCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
toggleModalColumn(checkbox);
});
}
// Helper function to toggle modal column without saving
function toggleModalColumnWithoutSave(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
const tbody = document.getElementById('objitemTableBody');
if (!tbody) return;
const table = tbody.closest('table');
if (!table) return;
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.children;
if (cells[columnIndex]) {
if (checkbox.checked) {
cells[columnIndex].style.removeProperty('display');
} else {
cells[columnIndex].style.setProperty('display', 'none', 'important');
}
}
});
}
// Initialize modal column visibility
function initModalColumnVisibility() {
const savedVisibility = loadModalColumnVisibility();
if (savedVisibility) {
// Restore saved state
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
columnCheckboxes.forEach(checkbox => {
const columnIndex = checkbox.getAttribute('data-column');
if (savedVisibility.hasOwnProperty(columnIndex)) {
checkbox.checked = savedVisibility[columnIndex];
toggleModalColumnWithoutSave(checkbox);
}
});
} else {
// Default state: hide columns by default: Создано (16), Кем(созд) (17), Комментарий (18), Усреднённое (19), Стандарт (20), Sigma (22)
const columnsToHide = [16, 17, 18, 19, 20, 22];
const tbody = document.getElementById('objitemTableBody');
if (!tbody) {
console.log('objitemTableBody not found');
return;
}
const table = tbody.closest('table');
if (!table) {
console.log('Table not found');
return;
}
// Update checkboxes and hide columns
columnsToHide.forEach(columnIndex => {
const checkbox = document.querySelector(`.modal-column-toggle[data-column="${columnIndex}"]`);
if (checkbox) {
checkbox.checked = false;
}
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.children;
if (cells[columnIndex]) {
cells[columnIndex].style.setProperty('display', 'none', 'important');
}
});
});
// Save initial state
saveModalColumnVisibility();
}
}
// Function to show LyngSat modal
function showLyngsatModal(lyngsatId) {
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch('/api/lyngsat/' + lyngsatId + '/')
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
let html = '<div class="container-fluid"><div class="row g-3">' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Спутник:</td><td><strong>' + data.satellite + '</strong></td></tr>' +
'<tr><td class="text-muted">Частота:</td><td><strong>' + data.frequency + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></td></tr>' +
'<tr><td class="text-muted">Канал:</td><td>' + data.channel_info + '</td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-gear"></i> Технические параметры</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Модуляция:</td><td><span class="badge bg-secondary">' + data.modulation + '</span></td></tr>' +
'<tr><td class="text-muted">Стандарт:</td><td><span class="badge bg-secondary">' + data.standard + '</span></td></tr>' +
'<tr><td class="text-muted">Сим. скорость:</td><td><strong>' + data.sym_velocity + ' БОД</strong></td></tr>' +
'<tr><td class="text-muted">FEC:</td><td>' + data.fec + '</td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' +
'<div class="card-body"><div class="row">' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на объект:</span><br>' +
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
'</div></div></div></div></div></div></div>';
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' +
'<i class="bi bi-exclamation-triangle"></i> ' + error.message + '</div>';
});
}
// Function to show transponder modal
function showTransponderModal(transponderId) {
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
modal.show();
const modalBody = document.getElementById('transponderModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-success" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch('/api/transponder/' + transponderId + '/')
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных транспондера');
}
return response.json();
})
.then(data => {
let html = '<div class="container-fluid"><div class="row g-3">' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + (data.name || '-') + '</strong></td></tr>' +
'<tr><td class="text-muted">Спутник:</td><td><strong>' + data.satellite + '</strong></td></tr>' +
'<tr><td class="text-muted">Зона покрытия:</td><td>' + (data.zone_name || '-') + '</td></tr>' +
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-broadcast"></i> Частотные параметры</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Downlink:</td><td><strong>' + data.downlink + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Uplink:</td><td><strong>' + (data.uplink || '-') + (data.uplink ? ' МГц' : '') + '</strong></td></tr>' +
'<tr><td class="text-muted">Полоса:</td><td><strong>' + data.frequency_range + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Перенос:</td><td>' + (data.transfer || '-') + (data.transfer ? ' МГц' : '') + '</td></tr>' +
'<tr><td class="text-muted">ОСШ:</td><td><strong>' + (data.snr || '-') + (data.snr ? ' дБ' : '') + '</strong></td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Метаданные</strong></div>' +
'<div class="card-body"><div class="row">' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Дата создания:</span><br><strong>' + (data.created_at || '-') + '</strong></p></div>' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Создан пользователем:</span><br><strong>' + (data.created_by || '-') + '</strong></p></div>' +
'</div></div></div></div></div></div>';
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' +
'<i class="bi bi-exclamation-triangle"></i> ' + error.message + '</div>';
});
}
</script>
<!-- LyngSat Data Modal -->
<div class="modal fade" id="lyngsatModal" tabindex="-1" aria-labelledby="lyngsatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные объекта LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<!-- Transponder Data Modal -->
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="transponderModalLabel">
<i class="bi bi-broadcast"></i> Данные транспондера
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="transponderModalBody">
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<!-- Include the sigma parameter modal component -->
{% include 'mainapp/components/_sigma_parameter_modal.html' %}
<!-- Include the satellite modal component -->
{% include 'mainapp/components/_satellite_modal.html' %}
<!-- Selected Sources Offcanvas -->
<div class="offcanvas offcanvas-end" tabindex="-1" id="selectedSourcesOffcanvas" aria-labelledby="selectedSourcesOffcanvasLabel" style="width: 80%;">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="selectedSourcesOffcanvasLabel">
Выбранные источники
<span class="badge bg-info" id="selectedSourcesOffcanvasCounter">0</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
</div>
<div class="offcanvas-body">
<div class="mb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="showSelectedSourcesOnMap()">
<i class="bi bi-map"></i> Карта
</button>
<button type="button" class="btn btn-outline-info btn-sm" onclick="showPlaybackAnimation()" title="Анимация движения объектов">
<i class="bi bi-play-circle"></i> Анимация
</button>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="button" class="btn btn-outline-success btn-sm" onclick="mergeSelectedSources()">
<i class="bi bi-union"></i> Объединить
</button>
{% endif %}
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeSelectedSources()">
<i class="bi bi-trash"></i> Удалить из списка
</button>
</div>
</div>
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered">
<thead class="table-light sticky-top">
<tr>
<th class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all-sources" class="form-check-input" onchange="toggleAllSelectedSources(this)">
</th>
<th class="text-center" style="min-width: 60px;">ID</th>
<th style="min-width: 150px;">Имя</th>
<th style="min-width: 120px;">Спутник</th>
<th style="min-width: 120px;">Тип объекта</th>
<th style="min-width: 150px;">Принадлежность</th>
<th style="min-width: 150px;">Координаты ГЛ</th>
<th class="text-center" style="min-width: 100px;">Кол-во точек</th>
</tr>
</thead>
<tbody id="selected-sources-table-body">
<!-- Data will be loaded here via JavaScript -->
</tbody>
</table>
</div>
<div id="selectedSourcesNoData" class="text-center text-muted py-4" style="display: none;">
Список пуст. Добавьте источники из основной таблицы.
</div>
</div>
</div>
<!-- Merge Sources Modal -->
<div class="modal fade" id="mergeSourcesModal" tabindex="-1" aria-labelledby="mergeSourcesModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="mergeSourcesModalLabel">
<i class="bi bi-union"></i> Объединение источников
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Внимание:</strong> Все точки из выбранных источников будут присвоены первому источнику в списке.
Остальные источники будут удалены.
</div>
<div class="card mb-3">
<div class="card-header bg-light">
<strong>Целевой источник (все точки будут присвоены ему):</strong>
</div>
<div class="card-body" id="targetSourceInfo">
<!-- Target source info will be populated here -->
</div>
</div>
<div class="card mb-3">
<div class="card-header bg-light">
<strong>Источники для объединения (будут удалены):</strong>
</div>
<div class="card-body">
<ul class="list-group" id="sourcesToMergeList">
<!-- Sources to merge will be populated here -->
</ul>
</div>
</div>
<div class="mb-3">
<label for="mergeInfoSelect" class="form-label">Тип объекта <span class="text-danger">*</span></label>
<select class="form-select" id="mergeInfoSelect" required>
<option value="">Выберите тип объекта</option>
{% for info in object_infos %}
<option value="{{ info.id }}">{{ info.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="mergeOwnershipSelect" class="form-label">Принадлежность объекта <span class="text-danger">*</span></label>
<select class="form-select" id="mergeOwnershipSelect" required>
<option value="">Выберите принадлежность</option>
{% for ownership in object_ownerships %}
<option value="{{ ownership.id }}">{{ ownership.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="mergeNoteTextarea" class="form-label">Примечание</label>
<textarea class="form-control" id="mergeNoteTextarea" rows="3" placeholder="Введите примечание (необязательно)"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-success" onclick="confirmMerge()">
<i class="bi bi-check-circle"></i> Объединить
</button>
</div>
</div>
</div>
</div>
<!-- Polygon Filter Map Modal -->
<div class="modal fade" id="polygonFilterModal" tabindex="-1" aria-labelledby="polygonFilterModalLabel" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="polygonFilterModalLabel">
<i class="bi bi-pentagon"></i> Нарисуйте полигон для фильтрации
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body p-0" style="position: relative;">
<div id="polygonHelpAlert" class="alert alert-info m-2" style="position: absolute; top: 10px; left: 10px; z-index: 1000; max-width: 400px; opacity: 0.95;">
<button type="button" class="btn-close btn-sm float-end" onclick="document.getElementById('polygonHelpAlert').style.display='none'"></button>
<small>
<strong>Инструкция:</strong>
<ul class="mb-0 ps-3">
<li>Используйте инструменты справа для рисования полигона или прямоугольника</li>
<li>Кликайте по карте для создания вершин полигона</li>
<li>Замкните полигон, кликнув на первую точку</li>
<li>Нажмите "Применить фильтр" для фильтрации источников</li>
</ul>
</small>
</div>
<div id="polygonFilterMap" style="height: calc(100vh - 120px); width: 100%;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-danger" onclick="clearPolygonOnMap()">
<i class="bi bi-trash"></i> Очистить полигон
</button>
<button type="button" class="btn btn-primary" onclick="applyPolygonFilter()">
<i class="bi bi-check-circle"></i> Применить фильтр
</button>
</div>
</div>
</div>
</div>
<!-- Source Requests Modal -->
<div class="modal fade" id="sourceRequestsModal" tabindex="-1" aria-labelledby="sourceRequestsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title" id="sourceRequestsModalLabel">
<i class="bi bi-list-task"></i> Заявки на источник #<span id="requestsSourceId"></span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModalForSource()">
<i class="bi bi-plus-circle"></i> Создать заявку
</button>
</div>
<div id="requestsLoadingSpinner" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
<div id="requestsContent" style="display: none;">
<div class="table-responsive" style="max-height: 50vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm table-bordered">
<thead class="table-light sticky-top">
<tr>
<th>ID</th>
<th>Статус</th>
<th>Приоритет</th>
<th>Дата планирования</th>
<th>Дата заявки</th>
<th>ГСО</th>
<th>Кубсат</th>
<th>Комментарий</th>
<th>Обновлено</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="requestsTableBody">
</tbody>
</table>
</div>
</div>
<div id="requestsNoData" class="text-center text-muted py-4" style="display: none;">
Нет заявок для этого источника
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<!-- Create/Edit Request Modal -->
<div class="modal fade" id="createRequestModal" tabindex="-1" aria-labelledby="createRequestModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="createRequestModalLabel">
<i class="bi bi-plus-circle"></i> <span id="createRequestModalTitle">Создать заявку</span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<form id="createRequestForm">
{% csrf_token %}
<input type="hidden" id="editRequestId" name="request_id" value="">
<input type="hidden" id="editRequestSourceId" name="source" value="">
<!-- Данные источника (только для чтения) -->
<div class="card bg-light mb-3" id="editSourceDataCard" style="display: none;">
<div class="card-header py-2">
<small class="text-muted"><i class="bi bi-info-circle"></i> Данные источника</small>
</div>
<div class="card-body py-2">
<div class="row">
<div class="col-md-4 mb-2">
<label class="form-label small text-muted mb-0">Имя точки</label>
<input type="text" class="form-control form-control-sm" id="editRequestObjitemName" readonly>
</div>
<div class="col-md-4 mb-2">
<label class="form-label small text-muted mb-0">Модуляция</label>
<input type="text" class="form-control form-control-sm" id="editRequestModulation" readonly>
</div>
<div class="col-md-4 mb-2">
<label class="form-label small text-muted mb-0">Символьная скорость</label>
<input type="text" class="form-control form-control-sm" id="editRequestSymbolRate" readonly>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editRequestStatus" class="form-label">Статус</label>
<select class="form-select" id="editRequestStatus" name="status">
<option value="planned">Запланировано</option>
<option value="conducted">Проведён</option>
<option value="successful">Успешно</option>
<option value="no_correlation">Нет корреляции</option>
<option value="no_signal">Нет сигнала в спектре</option>
<option value="unsuccessful">Неуспешно</option>
<option value="downloading">Скачивание</option>
<option value="processing">Обработка</option>
<option value="result_received">Результат получен</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="editRequestPriority" class="form-label">Приоритет</label>
<select class="form-select" id="editRequestPriority" name="priority">
<option value="low">Низкий</option>
<option value="medium" selected>Средний</option>
<option value="high">Высокий</option>
</select>
</div>
</div>
<!-- Координаты -->
<div class="row">
<div class="col-md-4 mb-3">
<label for="editRequestCoordsLat" class="form-label">Широта</label>
<input type="number" step="0.000001" class="form-control" id="editRequestCoordsLat" name="coords_lat"
placeholder="Например: 55.751244">
</div>
<div class="col-md-4 mb-3">
<label for="editRequestCoordsLon" class="form-label">Долгота</label>
<input type="number" step="0.000001" class="form-control" id="editRequestCoordsLon" name="coords_lon"
placeholder="Например: 37.618423">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Кол-во точек</label>
<input type="text" class="form-control" id="editRequestPointsCount" readonly value="-">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editRequestPlannedAt" class="form-label">Дата и время планирования</label>
<input type="datetime-local" class="form-control" id="editRequestPlannedAt" name="planned_at">
</div>
<div class="col-md-6 mb-3">
<label for="editRequestDate" class="form-label">Дата заявки</label>
<input type="date" class="form-control" id="editRequestDate" name="request_date">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editRequestGsoSuccess" class="form-label">ГСО успешно?</label>
<select class="form-select" id="editRequestGsoSuccess" name="gso_success">
<option value="">-</option>
<option value="true">Да</option>
<option value="false">Нет</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="editRequestKubsatSuccess" class="form-label">Кубсат успешно?</label>
<select class="form-select" id="editRequestKubsatSuccess" name="kubsat_success">
<option value="">-</option>
<option value="true">Да</option>
<option value="false">Нет</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="editRequestComment" class="form-label">Комментарий</label>
<textarea class="form-control" id="editRequestComment" name="comment" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" onclick="saveSourceRequest()">
<i class="bi bi-check-lg"></i> Сохранить
</button>
</div>
</div>
</div>
</div>
<!-- Request History Modal -->
<div class="modal fade" id="requestHistoryModal" tabindex="-1" aria-labelledby="requestHistoryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-secondary text-white">
<h5 class="modal-title" id="requestHistoryModalLabel">
<i class="bi bi-clock-history"></i> История изменений статуса
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="requestHistoryModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
// Source Requests functionality
let currentRequestsSourceId = null;
function showSourceRequests(sourceId) {
currentRequestsSourceId = sourceId;
document.getElementById('requestsSourceId').textContent = sourceId;
const modal = new bootstrap.Modal(document.getElementById('sourceRequestsModal'));
modal.show();
document.getElementById('requestsLoadingSpinner').style.display = 'block';
document.getElementById('requestsContent').style.display = 'none';
document.getElementById('requestsNoData').style.display = 'none';
fetch(`/api/source/${sourceId}/requests/`)
.then(response => response.json())
.then(data => {
document.getElementById('requestsLoadingSpinner').style.display = 'none';
if (data.requests && data.requests.length > 0) {
document.getElementById('requestsContent').style.display = 'block';
const tbody = document.getElementById('requestsTableBody');
tbody.innerHTML = '';
data.requests.forEach(req => {
const statusClass = getStatusBadgeClass(req.status);
const priorityClass = getPriorityBadgeClass(req.priority);
const row = document.createElement('tr');
row.innerHTML = `
<td>${req.id}</td>
<td><span class="badge ${statusClass}">${req.status_display}</span></td>
<td><span class="badge ${priorityClass}">${req.priority_display}</span></td>
<td>${req.planned_at}</td>
<td>${req.request_date}</td>
<td class="text-center">${req.gso_success === true ? '<span class="badge bg-success">Да</span>' : req.gso_success === false ? '<span class="badge bg-danger">Нет</span>' : '-'}</td>
<td class="text-center">${req.kubsat_success === true ? '<span class="badge bg-success">Да</span>' : req.kubsat_success === false ? '<span class="badge bg-danger">Нет</span>' : '-'}</td>
<td title="${req.comment}">${req.comment.length > 30 ? req.comment.substring(0, 30) + '...' : req.comment}</td>
<td>${req.status_updated_at}</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-info" onclick="showRequestHistory(${req.id})" title="История">
<i class="bi bi-clock-history"></i>
</button>
<button type="button" class="btn btn-outline-warning" onclick="editSourceRequest(${req.id})" title="Редактировать">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deleteSourceRequest(${req.id})" title="Удалить">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
tbody.appendChild(row);
});
} else {
document.getElementById('requestsNoData').style.display = 'block';
}
})
.catch(error => {
console.error('Error loading requests:', error);
document.getElementById('requestsLoadingSpinner').style.display = 'none';
document.getElementById('requestsNoData').style.display = 'block';
document.getElementById('requestsNoData').textContent = 'Ошибка загрузки данных';
});
}
function getStatusBadgeClass(status) {
switch(status) {
case 'successful':
case 'result_received':
return 'bg-success';
case 'unsuccessful':
case 'no_correlation':
case 'no_signal':
return 'bg-danger';
case 'planned':
return 'bg-primary';
case 'downloading':
case 'processing':
return 'bg-warning text-dark';
default:
return 'bg-secondary';
}
}
function getPriorityBadgeClass(priority) {
switch(priority) {
case 'high':
return 'bg-danger';
case 'medium':
return 'bg-warning text-dark';
default:
return 'bg-secondary';
}
}
function openCreateRequestModalForSource() {
document.getElementById('createRequestModalTitle').textContent = 'Создать заявку';
document.getElementById('createRequestForm').reset();
document.getElementById('editRequestId').value = '';
document.getElementById('editRequestSourceId').value = currentRequestsSourceId;
document.getElementById('editSourceDataCard').style.display = 'none';
document.getElementById('editRequestCoordsLat').value = '';
document.getElementById('editRequestCoordsLon').value = '';
document.getElementById('editRequestPointsCount').value = '-';
// Загружаем данные источника
loadSourceDataForRequest(currentRequestsSourceId);
const modal = new bootstrap.Modal(document.getElementById('createRequestModal'));
modal.show();
}
function loadSourceDataForRequest(sourceId) {
fetch(`{% url 'mainapp:source_data_api' source_id=0 %}`.replace('0', sourceId))
.then(response => response.json())
.then(data => {
if (data.found) {
document.getElementById('editRequestObjitemName').value = data.objitem_name || '-';
document.getElementById('editRequestModulation').value = data.modulation || '-';
document.getElementById('editRequestSymbolRate').value = data.symbol_rate || '-';
document.getElementById('editRequestPointsCount').value = data.points_count || '0';
if (data.coords_lat !== null && !document.getElementById('editRequestCoordsLat').value) {
document.getElementById('editRequestCoordsLat').value = data.coords_lat.toFixed(6);
}
if (data.coords_lon !== null && !document.getElementById('editRequestCoordsLon').value) {
document.getElementById('editRequestCoordsLon').value = data.coords_lon.toFixed(6);
}
document.getElementById('editSourceDataCard').style.display = 'block';
}
})
.catch(error => {
console.error('Error loading source data:', error);
});
}
function editSourceRequest(requestId) {
document.getElementById('createRequestModalTitle').textContent = 'Редактировать заявку';
fetch(`/api/source-request/${requestId}/`)
.then(response => response.json())
.then(data => {
document.getElementById('editRequestId').value = data.id;
document.getElementById('editRequestSourceId').value = data.source_id;
document.getElementById('editRequestStatus').value = data.status;
document.getElementById('editRequestPriority').value = data.priority;
document.getElementById('editRequestPlannedAt').value = data.planned_at || '';
document.getElementById('editRequestDate').value = data.request_date || '';
document.getElementById('editRequestGsoSuccess').value = data.gso_success === null ? '' : data.gso_success.toString();
document.getElementById('editRequestKubsatSuccess').value = data.kubsat_success === null ? '' : data.kubsat_success.toString();
document.getElementById('editRequestComment').value = data.comment || '';
// Заполняем данные источника
document.getElementById('editRequestObjitemName').value = data.objitem_name || '-';
document.getElementById('editRequestModulation').value = data.modulation || '-';
document.getElementById('editRequestSymbolRate').value = data.symbol_rate || '-';
document.getElementById('editRequestPointsCount').value = data.points_count || '0';
// Заполняем координаты
if (data.coords_lat !== null) {
document.getElementById('editRequestCoordsLat').value = data.coords_lat.toFixed(6);
} else {
document.getElementById('editRequestCoordsLat').value = '';
}
if (data.coords_lon !== null) {
document.getElementById('editRequestCoordsLon').value = data.coords_lon.toFixed(6);
} else {
document.getElementById('editRequestCoordsLon').value = '';
}
document.getElementById('editSourceDataCard').style.display = 'block';
const modal = new bootstrap.Modal(document.getElementById('createRequestModal'));
modal.show();
})
.catch(error => {
console.error('Error loading request:', error);
alert('Ошибка загрузки данных заявки');
});
}
function saveSourceRequest() {
const form = document.getElementById('createRequestForm');
const formData = new FormData(form);
const requestId = document.getElementById('editRequestId').value;
const url = requestId
? `/source-requests/${requestId}/edit/`
: '{% url "mainapp:source_request_create" %}';
fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': formData.get('csrfmiddlewaretoken'),
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Properly close modal and remove backdrop
const modalEl = document.getElementById('createRequestModal');
const modalInstance = bootstrap.Modal.getInstance(modalEl);
if (modalInstance) {
modalInstance.hide();
}
// Remove any remaining backdrops
document.querySelectorAll('.modal-backdrop').forEach(el => el.remove());
document.body.classList.remove('modal-open');
document.body.style.removeProperty('overflow');
document.body.style.removeProperty('padding-right');
showSourceRequests(currentRequestsSourceId);
} else {
alert('Ошибка: ' + JSON.stringify(result.errors));
}
})
.catch(error => {
console.error('Error saving request:', error);
alert('Ошибка сохранения заявки');
});
}
function deleteSourceRequest(requestId) {
if (!confirm('Вы уверены, что хотите удалить эту заявку?')) {
return;
}
fetch(`/source-requests/${requestId}/delete/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
showSourceRequests(currentRequestsSourceId);
} else {
alert('Ошибка: ' + result.error);
}
})
.catch(error => {
console.error('Error deleting request:', error);
alert('Ошибка удаления заявки');
});
}
function showRequestHistory(requestId) {
const modal = new bootstrap.Modal(document.getElementById('requestHistoryModal'));
modal.show();
const modalBody = document.getElementById('requestHistoryModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch(`/api/source-request/${requestId}/`)
.then(response => response.json())
.then(data => {
if (data.history && data.history.length > 0) {
let html = '<table class="table table-sm table-striped"><thead><tr><th>Старый статус</th><th>Новый статус</th><th>Дата изменения</th><th>Пользователь</th></tr></thead><tbody>';
data.history.forEach(h => {
html += `<tr><td>${h.old_status}</td><td>${h.new_status}</td><td>${h.changed_at}</td><td>${h.changed_by}</td></tr>`;
});
html += '</tbody></table>';
modalBody.innerHTML = html;
} else {
modalBody.innerHTML = '<div class="alert alert-info">История изменений пуста</div>';
}
})
.catch(error => {
modalBody.innerHTML = '<div class="alert alert-danger">Ошибка загрузки истории</div>';
});
}
</script>
{% endblock %}