Поменял теханализ, улучшения по простбам

This commit is contained in:
2025-12-02 14:56:29 +03:00
parent a18071b7ec
commit 889899080a
11 changed files with 1785 additions and 205 deletions

View File

@@ -0,0 +1,71 @@
<!-- Frequency Plan Modal -->
<div class="modal fade" id="frequencyPlanModal" tabindex="-1" aria-labelledby="frequencyPlanModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="frequencyPlanModalLabel">Частотный план</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-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
<div id="modalFrequencyContent" style="display: none;">
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;"></span> Downlink (синий), <span style="color: #fd7e14;"></span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации и связи с парным каналом.</p>
<div class="frequency-plan">
<div class="chart-controls">
<button type="button" class="btn btn-sm btn-outline-primary" id="modalResetZoom">
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
</button>
</div>
<div class="frequency-chart-container">
<canvas id="modalFrequencyChart"></canvas>
</div>
<div class="mt-3">
<p><strong>Всего транспондеров:</strong> <span id="modalTransponderCount">0</span></p>
</div>
</div>
</div>
<div id="modalNoData" style="display: none;" class="text-center text-muted py-5">
<p>Нет данных о транспондерах для этого спутника</p>
</div>
</div>
</div>
</div>
</div>
<style>
.frequency-plan {
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.frequency-chart-container {
position: relative;
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 20px;
height: 400px;
}
.chart-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.chart-controls button {
padding: 5px 15px;
font-size: 0.9rem;
}
</style>

View File

@@ -53,9 +53,9 @@
onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта
</button>
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
<!-- <a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
<i class="bi bi-clipboard-data"></i> Тех. анализ
</a>
</a> -->
</div>
<!-- Items per page select moved here -->

View File

@@ -275,11 +275,14 @@ document.addEventListener('DOMContentLoaded', function() {
layout: "fitDataStretch",
height: "500px",
placeholder: "Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'.",
initialSort: [
{column: "frequency", dir: "asc"}
],
columns: [
{title: "Источник", field: "source_name", minWidth: 180, widthGrow: 2},
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
{title: "Частота", field: "frequency", minWidth: 100},
{title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
{title: "Модуляция", field: "modulation", minWidth: 90},
{title: "Зеркала", field: "mirrors", minWidth: 130},
{
@@ -624,14 +627,20 @@ document.addEventListener('DOMContentLoaded', function() {
'ОСШ': group.snr,
'Зеркала': group.mirrors,
'Усреднённые координаты': group.avg_coordinates,
'Тип усреднения': group.avg_type || 'ГК',
'Медианное время': group.avg_time || '-',
'Кол-во точек': group.valid_points_count,
'Интервал': group.interval_label
// 'Тип усреднения': group.avg_type || 'ГК',
'Время': group.avg_time || '-',
'Кол-во точек': group.valid_points_count
});
});
});
// Sort by frequency
summaryData.sort((a, b) => {
const freqA = parseFloat(a['Частота, МГц']) || 0;
const freqB = parseFloat(b['Частота, МГц']) || 0;
return freqA - freqB;
});
const allPointsData = [];
allSourcesData.forEach(source => {
source.groups.forEach(group => {
@@ -651,13 +660,19 @@ document.addEventListener('DOMContentLoaded', function() {
'Координаты точки': point.coordinates,
'Усреднённые координаты': group.avg_coordinates,
'Расстояние от среднего, км': point.distance_from_avg,
'Статус': point.is_outlier ? 'Выброс' : 'OK',
'Интервал': group.interval_label
'Статус': point.is_outlier ? 'Выброс' : 'OK'
});
});
});
});
// Sort by frequency
allPointsData.sort((a, b) => {
const freqA = parseFloat(a['Частота, МГц']) || 0;
const freqB = parseFloat(b['Частота, МГц']) || 0;
return freqA - freqB;
});
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(summaryData), "Усреднение");
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(allPointsData), "Все точки");
@@ -737,41 +752,41 @@ document.addEventListener('DOMContentLoaded', function() {
"tags": {"layers": [], "creator": CREATOR_ID}
});
group.points.forEach(point => {
if (point.is_outlier) return;
// group.points.forEach(point => {
// if (point.is_outlier) return;
const pointCoord = point.coord_tuple;
const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
const pointSourceId = generateUUID();
// const pointCoord = point.coord_tuple;
// const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
// const pointSourceId = generateUUID();
result.push({
"tacticObjectType": "source",
"captionPosition": "right",
"id": pointSourceId,
"icon": {"type": "circle", "color": sourceColor},
"caption": pointCaption,
"name": pointCaption,
"customActions": [],
"trackBehavior": {},
"bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
"bearingBehavior": {},
"tags": {"creator": CREATOR_ID}
});
// result.push({
// "tacticObjectType": "source",
// "captionPosition": "right",
// "id": pointSourceId,
// "icon": {"type": "circle", "color": sourceColor},
// "caption": pointCaption,
// "name": pointCaption,
// "customActions": [],
// "trackBehavior": {},
// "bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
// "bearingBehavior": {},
// "tags": {"creator": CREATOR_ID}
// });
result.push({
"tacticObjectType": "position",
"id": generateUUID(),
"parentId": pointSourceId,
"timeStamp": point.timestamp_unix || (Date.now() / 1000),
"latitude": pointCoord[1],
"altitude": 0,
"longitude": pointCoord[0],
"caption": "",
"tooltip": "",
"customActions": [],
"tags": {"layers": [], "creator": CREATOR_ID}
});
});
// result.push({
// "tacticObjectType": "position",
// "id": generateUUID(),
// "parentId": pointSourceId,
// "timeStamp": point.timestamp_unix || (Date.now() / 1000),
// "latitude": pointCoord[1],
// "altitude": 0,
// "longitude": pointCoord[0],
// "caption": "",
// "tooltip": "",
// "customActions": [],
// "tags": {"layers": [], "creator": CREATOR_ID}
// });
// });
});
});

View File

@@ -303,17 +303,26 @@
<td>{{ satellite.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ satellite.updated_at|date:"d.m.Y H:i" }}</td>
<td class="text-center">
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:satellite_update' satellite.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 class="d-flex gap-1 justify-content-center">
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:satellite_update' satellite.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 %}
{% if satellite.transponder_count > 0 %}
<button type="button" class="btn btn-sm btn-outline-info"
onclick="showFrequencyPlan({{ satellite.id }})"
title="Частотный план">
<i class="bi bi-bar-chart"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
@@ -330,6 +339,8 @@
</div>
</div>
{% include 'mainapp/components/_frequency_plan_modal.html' %}
{% endblock %}
{% block extra_js %}
@@ -526,6 +537,607 @@ document.addEventListener('DOMContentLoaded', function() {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
});
// Frequency Plan Modal functionality
let modalCanvas, modalCtx, modalContainer;
let modalZoomLevelUL = 1;
let modalZoomLevelDL = 1;
let modalPanOffsetUL = 0;
let modalPanOffsetDL = 0;
let modalIsDragging = false;
let modalDragStartX = 0;
let modalDragStartOffsetUL = 0;
let modalDragStartOffsetDL = 0;
let modalDragArea = null;
let modalHoveredTransponder = null;
let modalTransponderRects = [];
let modalTranspondersData = [];
let modalMinFreqUL, modalMaxFreqUL, modalFreqRangeUL;
let modalMinFreqDL, modalMaxFreqDL, modalFreqRangeDL;
let modalOriginalMinFreqUL, modalOriginalMaxFreqUL, modalOriginalFreqRangeUL;
let modalOriginalMinFreqDL, modalOriginalMaxFreqDL, modalOriginalFreqRangeDL;
let modalUplinkStartY, modalUplinkHeight, modalDownlinkStartY, modalDownlinkHeight;
function showFrequencyPlan(satelliteId) {
const modal = new bootstrap.Modal(document.getElementById('frequencyPlanModal'));
// Reset modal state
document.getElementById('modalLoadingSpinner').style.display = 'block';
document.getElementById('modalFrequencyContent').style.display = 'none';
document.getElementById('modalNoData').style.display = 'none';
modal.show();
// Fetch transponder data
fetch(`/api/satellite/${satelliteId}/transponders/`)
.then(response => response.json())
.then(data => {
document.getElementById('modalLoadingSpinner').style.display = 'none';
if (data.transponders && data.transponders.length > 0) {
modalTranspondersData = data.transponders;
document.getElementById('modalTransponderCount').textContent = data.count;
document.getElementById('modalFrequencyContent').style.display = 'block';
// Initialize chart after modal is shown
setTimeout(() => {
initializeModalFrequencyChart();
}, 100);
} else {
document.getElementById('modalNoData').style.display = 'block';
}
})
.catch(error => {
console.error('Error fetching transponder data:', error);
document.getElementById('modalLoadingSpinner').style.display = 'none';
document.getElementById('modalNoData').style.display = 'block';
});
}
function initializeModalFrequencyChart() {
if (!modalTranspondersData || modalTranspondersData.length === 0) {
return;
}
modalCanvas = document.getElementById('modalFrequencyChart');
if (!modalCanvas) return;
modalContainer = modalCanvas.parentElement;
modalCtx = modalCanvas.getContext('2d');
// Calculate frequency ranges
modalMinFreqUL = Infinity;
modalMaxFreqUL = -Infinity;
modalMinFreqDL = Infinity;
modalMaxFreqDL = -Infinity;
modalTranspondersData.forEach(t => {
const dlStartFreq = t.downlink - (t.frequency_range / 2);
const dlEndFreq = t.downlink + (t.frequency_range / 2);
modalMinFreqDL = Math.min(modalMinFreqDL, dlStartFreq);
modalMaxFreqDL = Math.max(modalMaxFreqDL, dlEndFreq);
if (t.uplink) {
const ulStartFreq = t.uplink - (t.frequency_range / 2);
const ulEndFreq = t.uplink + (t.frequency_range / 2);
modalMinFreqUL = Math.min(modalMinFreqUL, ulStartFreq);
modalMaxFreqUL = Math.max(modalMaxFreqUL, ulEndFreq);
}
});
// Add padding
const paddingDL = (modalMaxFreqDL - modalMinFreqDL) * 0.04;
modalMinFreqDL -= paddingDL;
modalMaxFreqDL += paddingDL;
if (modalMaxFreqUL !== -Infinity) {
const paddingUL = (modalMaxFreqUL - modalMinFreqUL) * 0.04;
modalMinFreqUL -= paddingUL;
modalMaxFreqUL += paddingUL;
}
// Store original values
modalOriginalMinFreqDL = modalMinFreqDL;
modalOriginalMaxFreqDL = modalMaxFreqDL;
modalOriginalFreqRangeDL = modalMaxFreqDL - modalMinFreqDL;
modalFreqRangeDL = modalOriginalFreqRangeDL;
modalOriginalMinFreqUL = modalMinFreqUL;
modalOriginalMaxFreqUL = modalMaxFreqUL;
modalOriginalFreqRangeUL = modalMaxFreqUL - modalMinFreqUL;
modalFreqRangeUL = modalOriginalFreqRangeUL;
// Reset zoom and pan
modalZoomLevelUL = 1;
modalZoomLevelDL = 1;
modalPanOffsetUL = 0;
modalPanOffsetDL = 0;
// Setup event listeners
modalCanvas.addEventListener('wheel', handleModalWheel, { passive: false });
modalCanvas.addEventListener('mousedown', handleModalMouseDown);
modalCanvas.addEventListener('mousemove', handleModalMouseMove);
modalCanvas.addEventListener('mouseup', handleModalMouseUp);
modalCanvas.addEventListener('mouseleave', handleModalMouseLeave);
renderModalChart();
}
function renderModalChart() {
if (!modalCanvas || !modalCtx) return;
const dpr = window.devicePixelRatio || 1;
const rect = modalContainer.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
modalCanvas.width = width * dpr;
modalCanvas.height = height * dpr;
modalCanvas.style.width = width + 'px';
modalCanvas.style.height = height + 'px';
modalCtx.scale(dpr, dpr);
modalCtx.clearRect(0, 0, width, height);
const leftMargin = 60;
const rightMargin = 20;
const topMargin = 60;
const middleMargin = 60;
const bottomMargin = 40;
const chartWidth = width - leftMargin - rightMargin;
const availableHeight = height - topMargin - middleMargin - bottomMargin;
modalUplinkHeight = availableHeight * 0.48;
modalDownlinkHeight = availableHeight * 0.48;
// Group by polarization
const polarizationGroups = {};
modalTranspondersData.forEach(t => {
let pol = t.polarization || '-';
pol = pol.charAt(0).toUpperCase();
if (!polarizationGroups[pol]) {
polarizationGroups[pol] = [];
}
polarizationGroups[pol].push(t);
});
const polarizations = Object.keys(polarizationGroups);
const rowHeightUL = modalUplinkHeight / polarizations.length;
const rowHeightDL = modalDownlinkHeight / polarizations.length;
// Calculate visible ranges
const visibleFreqRangeUL = modalFreqRangeUL / modalZoomLevelUL;
const centerFreqUL = (modalMinFreqUL + modalMaxFreqUL) / 2;
const visibleMinFreqUL = centerFreqUL - visibleFreqRangeUL / 2 + modalPanOffsetUL;
const visibleMaxFreqUL = centerFreqUL + visibleFreqRangeUL / 2 + modalPanOffsetUL;
const visibleFreqRangeDL = modalFreqRangeDL / modalZoomLevelDL;
const centerFreqDL = (modalMinFreqDL + modalMaxFreqDL) / 2;
const visibleMinFreqDL = centerFreqDL - visibleFreqRangeDL / 2 + modalPanOffsetDL;
const visibleMaxFreqDL = centerFreqDL + visibleFreqRangeDL / 2 + modalPanOffsetDL;
modalUplinkStartY = topMargin;
modalDownlinkStartY = topMargin + modalUplinkHeight + middleMargin;
// Draw UPLINK axis
modalCtx.strokeStyle = '#dee2e6';
modalCtx.lineWidth = 1;
modalCtx.beginPath();
modalCtx.moveTo(leftMargin, modalUplinkStartY);
modalCtx.lineTo(width - rightMargin, modalUplinkStartY);
modalCtx.stroke();
modalCtx.fillStyle = '#6c757d';
modalCtx.font = '11px sans-serif';
modalCtx.textAlign = 'center';
const numTicks = 10;
for (let i = 0; i <= numTicks; i++) {
const freq = visibleMinFreqUL + (visibleMaxFreqUL - visibleMinFreqUL) * i / numTicks;
const x = leftMargin + chartWidth * i / numTicks;
modalCtx.beginPath();
modalCtx.moveTo(x, modalUplinkStartY);
modalCtx.lineTo(x, modalUplinkStartY - 5);
modalCtx.stroke();
modalCtx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
modalCtx.beginPath();
modalCtx.moveTo(x, modalUplinkStartY);
modalCtx.lineTo(x, modalUplinkStartY + modalUplinkHeight);
modalCtx.stroke();
modalCtx.strokeStyle = '#dee2e6';
modalCtx.fillText(freq.toFixed(1), x, modalUplinkStartY - 10);
}
modalCtx.fillStyle = '#000';
modalCtx.font = 'bold 12px sans-serif';
modalCtx.textAlign = 'center';
modalCtx.fillText('Uplink Частота (МГц)', width / 2, modalUplinkStartY - 25);
// Draw DOWNLINK axis
modalCtx.strokeStyle = '#dee2e6';
modalCtx.lineWidth = 1;
modalCtx.beginPath();
modalCtx.moveTo(leftMargin, modalDownlinkStartY);
modalCtx.lineTo(width - rightMargin, modalDownlinkStartY);
modalCtx.stroke();
modalCtx.fillStyle = '#6c757d';
modalCtx.font = '11px sans-serif';
modalCtx.textAlign = 'center';
for (let i = 0; i <= numTicks; i++) {
const freq = visibleMinFreqDL + (visibleMaxFreqDL - visibleMinFreqDL) * i / numTicks;
const x = leftMargin + chartWidth * i / numTicks;
modalCtx.beginPath();
modalCtx.moveTo(x, modalDownlinkStartY);
modalCtx.lineTo(x, modalDownlinkStartY - 5);
modalCtx.stroke();
modalCtx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
modalCtx.beginPath();
modalCtx.moveTo(x, modalDownlinkStartY);
modalCtx.lineTo(x, modalDownlinkStartY + modalDownlinkHeight);
modalCtx.stroke();
modalCtx.strokeStyle = '#dee2e6';
modalCtx.fillText(freq.toFixed(1), x, modalDownlinkStartY - 10);
}
modalCtx.fillStyle = '#000';
modalCtx.font = 'bold 12px sans-serif';
modalCtx.textAlign = 'center';
modalCtx.fillText('Downlink Частота (МГц)', width / 2, modalDownlinkStartY - 25);
modalCtx.save();
modalCtx.translate(15, height / 2);
modalCtx.rotate(-Math.PI / 2);
modalCtx.textAlign = 'center';
modalCtx.fillText('Поляризация', 0, 0);
modalCtx.restore();
modalTransponderRects = [];
// Draw transponders
polarizations.forEach((pol, index) => {
const group = polarizationGroups[pol];
const downlinkColor = '#0d6efd';
const uplinkColor = '#fd7e14';
const uplinkY = modalUplinkStartY + index * rowHeightUL;
const uplinkBarHeight = rowHeightUL * 0.8;
const uplinkBarY = uplinkY + (rowHeightUL - uplinkBarHeight) / 2;
const downlinkY = modalDownlinkStartY + index * rowHeightDL;
const downlinkBarHeight = rowHeightDL * 0.8;
const downlinkBarY = downlinkY + (rowHeightDL - downlinkBarHeight) / 2;
modalCtx.fillStyle = '#000';
modalCtx.font = 'bold 14px sans-serif';
modalCtx.textAlign = 'center';
modalCtx.fillText(pol, leftMargin - 25, uplinkBarY + uplinkBarHeight / 2 + 4);
modalCtx.fillText(pol, leftMargin - 25, downlinkBarY + downlinkBarHeight / 2 + 4);
if (index < polarizations.length - 1) {
modalCtx.strokeStyle = '#adb5bd';
modalCtx.lineWidth = 1;
modalCtx.beginPath();
modalCtx.moveTo(leftMargin, uplinkY + rowHeightUL);
modalCtx.lineTo(width - rightMargin, uplinkY + rowHeightUL);
modalCtx.stroke();
modalCtx.beginPath();
modalCtx.moveTo(leftMargin, downlinkY + rowHeightDL);
modalCtx.lineTo(width - rightMargin, downlinkY + rowHeightDL);
modalCtx.stroke();
}
// Draw uplink transponders
group.forEach(t => {
if (!t.uplink) return;
const startFreq = t.uplink - (t.frequency_range / 2);
const endFreq = t.uplink + (t.frequency_range / 2);
if (endFreq < visibleMinFreqUL || startFreq > visibleMaxFreqUL) {
return;
}
const x1 = leftMargin + ((startFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
const x2 = leftMargin + ((endFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
const barWidth = x2 - x1;
if (barWidth < 1) return;
const isHovered = modalHoveredTransponder && modalHoveredTransponder.transponder.name === t.name;
modalCtx.fillStyle = uplinkColor;
modalCtx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
modalCtx.strokeStyle = isHovered ? '#000' : '#fff';
modalCtx.lineWidth = isHovered ? 3 : 1;
modalCtx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
if (barWidth > 40) {
modalCtx.fillStyle = '#fff';
modalCtx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
modalCtx.textAlign = 'center';
modalCtx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
}
modalTransponderRects.push({
x: x1,
y: uplinkBarY,
width: barWidth,
height: uplinkBarHeight,
transponder: t,
type: 'uplink',
centerX: x1 + barWidth / 2
});
});
// Draw downlink transponders
group.forEach(t => {
const startFreq = t.downlink - (t.frequency_range / 2);
const endFreq = t.downlink + (t.frequency_range / 2);
if (endFreq < visibleMinFreqDL || startFreq > visibleMaxFreqDL) {
return;
}
const x1 = leftMargin + ((startFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
const x2 = leftMargin + ((endFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
const barWidth = x2 - x1;
if (barWidth < 1) return;
const isHovered = modalHoveredTransponder && modalHoveredTransponder.transponder.name === t.name;
modalCtx.fillStyle = downlinkColor;
modalCtx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
modalCtx.strokeStyle = isHovered ? '#000' : '#fff';
modalCtx.lineWidth = isHovered ? 3 : 1;
modalCtx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
if (barWidth > 40) {
modalCtx.fillStyle = '#fff';
modalCtx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
modalCtx.textAlign = 'center';
modalCtx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
}
modalTransponderRects.push({
x: x1,
y: downlinkBarY,
width: barWidth,
height: downlinkBarHeight,
transponder: t,
type: 'downlink',
centerX: x1 + barWidth / 2
});
});
});
if (modalHoveredTransponder) {
drawModalConnectionLine(modalHoveredTransponder);
drawModalTooltip(modalHoveredTransponder);
}
}
function drawModalConnectionLine(rectInfo) {
const t = rectInfo.transponder;
if (!t.uplink) return;
const downlinkRect = modalTransponderRects.find(r => r.transponder.name === t.name && r.type === 'downlink');
const uplinkRect = modalTransponderRects.find(r => r.transponder.name === t.name && r.type === 'uplink');
if (!downlinkRect || !uplinkRect) return;
const x1 = downlinkRect.centerX;
const y1 = downlinkRect.y + downlinkRect.height;
const x2 = uplinkRect.centerX;
const y2 = uplinkRect.y;
modalCtx.save();
modalCtx.strokeStyle = '#ffc107';
modalCtx.lineWidth = 2;
modalCtx.setLineDash([5, 3]);
modalCtx.globalAlpha = 0.8;
modalCtx.beginPath();
modalCtx.moveTo(x1, y1);
modalCtx.lineTo(x2, y2);
modalCtx.stroke();
modalCtx.restore();
}
function drawModalTooltip(rectInfo) {
const t = rectInfo.transponder;
const isUplink = rectInfo.type === 'uplink';
const freq = isUplink ? t.uplink : t.downlink;
const startFreq = freq - (t.frequency_range / 2);
const endFreq = freq + (t.frequency_range / 2);
const lines = [
t.name,
'Тип: ' + (isUplink ? 'Uplink' : 'Downlink'),
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
'Центр: ' + freq.toFixed(3) + ' МГц',
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
'Поляризация: ' + t.polarization,
'Зона: ' + t.zone_name
];
if (isUplink && t.downlink && t.uplink) {
const conversion = t.downlink - t.uplink;
lines.push('Перенос: ' + conversion.toFixed(3) + ' МГц');
}
modalCtx.font = '12px sans-serif';
const padding = 10;
const lineHeight = 16;
let maxWidth = 0;
lines.forEach(line => {
const width = modalCtx.measureText(line).width;
maxWidth = Math.max(maxWidth, width);
});
const tooltipWidth = maxWidth + padding * 2;
const tooltipHeight = lines.length * lineHeight + padding * 2;
const mouseX = rectInfo._mouseX || modalCanvas.width / 2;
const mouseY = rectInfo._mouseY || modalCanvas.height / 2;
let tooltipX = mouseX + 15;
let tooltipY = mouseY - tooltipHeight - 15;
if (tooltipX + tooltipWidth > modalCanvas.width) {
tooltipX = mouseX - tooltipWidth - 15;
}
if (tooltipY < 0) {
tooltipY = mouseY + 15;
}
modalCtx.fillStyle = 'rgba(0, 0, 0, 0.9)';
modalCtx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
modalCtx.fillStyle = '#fff';
modalCtx.font = 'bold 12px sans-serif';
modalCtx.textAlign = 'left';
modalCtx.fillText(lines[0], tooltipX + padding, tooltipY + padding + 12);
modalCtx.font = '11px sans-serif';
for (let i = 1; i < lines.length; i++) {
modalCtx.fillText(lines[i], tooltipX + padding, tooltipY + padding + 12 + i * lineHeight);
}
}
function handleModalWheel(e) {
e.preventDefault();
const rect = modalCanvas.getBoundingClientRect();
const mouseY = e.clientY - rect.top;
const isUplinkArea = mouseY < (modalUplinkStartY + modalUplinkHeight);
const delta = e.deltaY > 0 ? 0.9 : 1.1;
if (isUplinkArea) {
const newZoom = Math.max(1, Math.min(20, modalZoomLevelUL * delta));
if (newZoom !== modalZoomLevelUL) {
modalZoomLevelUL = newZoom;
const maxPan = (modalOriginalFreqRangeUL * (modalZoomLevelUL - 1)) / (2 * modalZoomLevelUL);
modalPanOffsetUL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetUL));
renderModalChart();
}
} else {
const newZoom = Math.max(1, Math.min(20, modalZoomLevelDL * delta));
if (newZoom !== modalZoomLevelDL) {
modalZoomLevelDL = newZoom;
const maxPan = (modalOriginalFreqRangeDL * (modalZoomLevelDL - 1)) / (2 * modalZoomLevelDL);
modalPanOffsetDL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetDL));
renderModalChart();
}
}
}
function handleModalMouseDown(e) {
const rect = modalCanvas.getBoundingClientRect();
const mouseY = e.clientY - rect.top;
modalDragArea = mouseY < (modalUplinkStartY + modalUplinkHeight) ? 'uplink' : 'downlink';
modalIsDragging = true;
modalDragStartX = e.clientX;
modalDragStartOffsetUL = modalPanOffsetUL;
modalDragStartOffsetDL = modalPanOffsetDL;
modalCanvas.style.cursor = 'grabbing';
}
function handleModalMouseMove(e) {
const rect = modalCanvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (modalIsDragging) {
const dx = e.clientX - modalDragStartX;
if (modalDragArea === 'uplink') {
const freqPerPixel = (modalFreqRangeUL / modalZoomLevelUL) / (rect.width - 80);
modalPanOffsetUL = modalDragStartOffsetUL - dx * freqPerPixel;
const maxPan = (modalOriginalFreqRangeUL * (modalZoomLevelUL - 1)) / (2 * modalZoomLevelUL);
modalPanOffsetUL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetUL));
} else {
const freqPerPixel = (modalFreqRangeDL / modalZoomLevelDL) / (rect.width - 80);
modalPanOffsetDL = modalDragStartOffsetDL - dx * freqPerPixel;
const maxPan = (modalOriginalFreqRangeDL * (modalZoomLevelDL - 1)) / (2 * modalZoomLevelDL);
modalPanOffsetDL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetDL));
}
renderModalChart();
} else {
let found = null;
for (const tr of modalTransponderRects) {
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
found = tr;
found._mouseX = mouseX;
found._mouseY = mouseY;
break;
}
}
if (found !== modalHoveredTransponder) {
modalHoveredTransponder = found;
modalCanvas.style.cursor = found ? 'pointer' : 'default';
renderModalChart();
} else if (found) {
found._mouseX = mouseX;
found._mouseY = mouseY;
}
}
}
function handleModalMouseUp() {
modalIsDragging = false;
modalCanvas.style.cursor = modalHoveredTransponder ? 'pointer' : 'default';
}
function handleModalMouseLeave() {
modalIsDragging = false;
modalHoveredTransponder = null;
modalCanvas.style.cursor = 'default';
renderModalChart();
}
function resetModalZoom() {
modalZoomLevelUL = 1;
modalZoomLevelDL = 1;
modalPanOffsetUL = 0;
modalPanOffsetDL = 0;
renderModalChart();
}
// Setup reset button
document.addEventListener('DOMContentLoaded', function() {
const resetBtn = document.getElementById('modalResetZoom');
if (resetBtn) {
resetBtn.addEventListener('click', resetModalZoom);
}
});
</script>
{% endblock %}

View File

@@ -79,9 +79,9 @@
<i class="bi bi-plus-circle"></i> Создать
</a>
{% endif %}
<a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
Передача точек
</a>
</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>
@@ -101,6 +101,9 @@
<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>
</div>
<!-- Add to List Button -->
@@ -157,7 +160,7 @@
<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="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>
@@ -496,7 +499,7 @@
{% 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" style="min-width: 150px;">Последний сигнал</th> -->
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
</tr>
</thead>
@@ -539,7 +542,7 @@
<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>{{ 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 %}

View File

@@ -53,6 +53,18 @@
{% endblock %}
{% block content %}
<!-- Toast Container -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
<div id="saveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto" id="toastTitle">Уведомление</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Закрыть"></button>
</div>
<div class="toast-body" id="toastBody">
</div>
</div>
</div>
<div class="data-entry-container">
<h2>Тех. анализ - Ввод данных</h2>
@@ -67,6 +79,11 @@
{% endfor %}
</select>
</div>
<div class="col-md-8 mb-3 d-flex align-items-end gap-2">
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-primary">
<i class="bi bi-list"></i> Список данных
</a>
</div>
<!-- <div class="col-md-8 mb-3">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i>
@@ -228,16 +245,43 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// Helper function to show toast
function showToast(title, message, type = 'info') {
const toastEl = document.getElementById('saveToast');
const toastTitle = document.getElementById('toastTitle');
const toastBody = document.getElementById('toastBody');
const toastHeader = toastEl.querySelector('.toast-header');
// Remove previous background classes
toastHeader.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'text-white');
// Add appropriate background class
if (type === 'success') {
toastHeader.classList.add('bg-success', 'text-white');
} else if (type === 'error') {
toastHeader.classList.add('bg-danger', 'text-white');
} else if (type === 'warning') {
toastHeader.classList.add('bg-warning');
}
toastTitle.textContent = title;
toastBody.innerHTML = message;
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
toast.show();
}
// Delete selected rows
document.getElementById('delete-selected').addEventListener('click', function() {
const selectedRows = table.getSelectedRows();
if (selectedRows.length === 0) {
alert('Выберите строки для удаления');
showToast('Внимание', 'Выберите строки для удаления', 'warning');
return;
}
if (confirm(`Удалить ${selectedRows.length} строк(и)?`)) {
selectedRows.forEach(row => row.delete());
showToast('Успешно', `Удалено строк: ${selectedRows.length}`, 'success');
}
});
@@ -246,21 +290,21 @@ document.addEventListener('DOMContentLoaded', function() {
const satelliteId = document.getElementById('satellite-select').value;
if (!satelliteId) {
alert('Пожалуйста, выберите спутник');
showToast('Внимание', 'Пожалуйста, выберите спутник', 'warning');
return;
}
const data = table.getData();
if (data.length === 0) {
alert('Нет данных для сохранения');
showToast('Внимание', 'Нет данных для сохранения', 'warning');
return;
}
// Validate that all rows have names
const emptyNames = data.filter(row => !row.name || row.name.trim() === '');
if (emptyNames.length > 0) {
alert('Все строки должны иметь имя');
showToast('Внимание', 'Все строки должны иметь имя', 'warning');
return;
}
@@ -284,27 +328,27 @@ document.addEventListener('DOMContentLoaded', function() {
const result = await response.json();
if (result.success) {
let message = `Успешно сохранено!\n`;
message += `Создано: ${result.created}\n`;
message += `Обновлено: ${result.updated}\n`;
let message = `<strong>Успешно сохранено!</strong><br>`;
message += `Создано: ${result.created}<br>`;
message += `Обновлено: ${result.updated}<br>`;
message += `Всего: ${result.total}`;
if (result.errors && result.errors.length > 0) {
message += `\n\nОшибки:\n${result.errors.join('\n')}`;
message += `<br><br><strong>Ошибки:</strong><br>${result.errors.join('<br>')}`;
}
alert(message);
showToast('Сохранение завершено', message, 'success');
// Clear table after successful save
if (!result.errors || result.errors.length === 0) {
table.clearData();
}
} else {
alert('Ошибка: ' + (result.error || 'Неизвестная ошибка'));
showToast('Ошибка', result.error || 'Неизвестная ошибка', 'error');
}
} catch (error) {
console.error('Error:', error);
alert('Произошла ошибка при сохранении данных');
showToast('Ошибка', 'Произошла ошибка при сохранении данных', 'error');
} finally {
// Re-enable button
this.disabled = false;

View File

@@ -0,0 +1,518 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% block title %}Тех. анализ - Список{% endblock %}
{% block extra_css %}
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
<style>
.sticky-top {
position: sticky;
top: 0;
z-index: 10;
}
#tech-analyze-table {
font-size: 12px;
}
#tech-analyze-table .tabulator-header {
font-size: 12px;
}
#tech-analyze-table .tabulator-cell {
font-size: 12px;
white-space: normal;
word-wrap: break-word;
}
#tech-analyze-table .tabulator-row {
min-height: 40px;
}
</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 или имени...">
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
</div>
</div>
<!-- Action buttons -->
<div class="d-flex gap-2">
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-success btn-sm" title="Ввод данных">
<i class="bi bi-plus-circle"></i> Ввод данных
</a>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="button" class="btn btn-danger btn-sm" title="Удалить выбранные" onclick="deleteSelected()">
<i class="bi bi-trash"></i> Удалить
</button>
{% endif %}
<button type="button" class="btn btn-info btn-sm" title="Привязать к существующим точкам" onclick="showLinkModal()">
<i class="bi bi-link-45deg"></i> Привязать к точкам
</button>
</div>
<!-- Filter Toggle Button -->
<div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
<i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Offcanvas Filter Panel -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
</div>
<div class="offcanvas-body">
<form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Спутник:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %}
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</select>
</div>
<!-- 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">
<div id="tech-analyze-table"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Link to Points Modal -->
<div class="modal fade" id="linkToPointsModal" tabindex="-1" aria-labelledby="linkToPointsModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="linkToPointsModalLabel">
<i class="bi bi-link-45deg"></i> Привязать к существующим точкам
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Будут обновлены только точки с отсутствующими данными (модуляция "-", символьная скорость -1 или 0, стандарт "-").
</div>
<div class="mb-3">
<label for="linkSatelliteSelect" class="form-label">Выберите спутник <span class="text-danger">*</span></label>
<select class="form-select" id="linkSatelliteSelect" required>
<option value="">Выберите спутник</option>
{% for satellite in satellites %}
<option value="{{ satellite.id }}">{{ satellite.name }}</option>
{% endfor %}
</select>
</div>
<div id="linkResultMessage" class="alert" style="display: none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
<button type="button" class="btn btn-primary" id="confirmLinkBtn" onclick="confirmLink(event)">
<i class="bi bi-check-circle"></i> Привязать
</button>
</div>
</div>
</div>
</div>
<!-- Include the satellite modal component -->
{% include 'mainapp/components/_satellite_modal.html' %}
{% endblock %}
{% block extra_js %}
<script src="{% static 'tabulator/js/tabulator.min.js' %}"></script>
<script>
// Helper function to 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;
}
// Initialize Tabulator
const urlParams = new URLSearchParams(window.location.search);
const ajaxParams = {};
for (const [key, value] of urlParams.entries()) {
if (ajaxParams[key]) {
if (!Array.isArray(ajaxParams[key])) {
ajaxParams[key] = [ajaxParams[key]];
}
ajaxParams[key].push(value);
} else {
ajaxParams[key] = value;
}
}
const table = new Tabulator("#tech-analyze-table", {
ajaxURL: "{% url 'mainapp:tech_analyze_api' %}",
ajaxParams: ajaxParams,
pagination: true,
paginationMode: "remote",
paginationSize: {{ items_per_page }},
paginationSizeSelector: [25, 50, 100, 200, 500],
layout: "fitDataStretch",
height: "70vh",
placeholder: "Нет данных для отображения",
rowHeight: null, // Автоматическая высота строк
columns: [
{
formatter: "rowSelection",
titleFormatter: "rowSelection",
hozAlign: "center",
headerSort: false,
width: 40,
cellClick: function(e, cell) {
cell.getRow().toggleSelect();
}
},
{title: "ID", field: "id", width: 80, hozAlign: "center"},
{
title: "Имя",
field: "name",
minWidth: 250,
widthGrow: 3,
formatter: function(cell) {
return '<div style="white-space: normal; word-wrap: break-word; padding: 4px 0;">' +
(cell.getValue() || '-') + '</div>';
}
},
{
title: "Спутник",
field: "satellite_name",
minWidth: 120,
widthGrow: 1,
formatter: function(cell) {
const data = cell.getData();
if (data.satellite_id) {
return '<a href="#" class="text-decoration-underline" onclick="showSatelliteModal(' + data.satellite_id + '); return false;">' +
(data.satellite_name || '-') + '</a>';
}
return data.satellite_name || '-';
}
},
{title: "Частота, МГц", field: "frequency", width: 120, hozAlign: "right", formatter: function(cell) {
const val = cell.getValue();
return val && val !== 0 ? val.toFixed(3) : '-';
}},
{title: "Полоса, МГц", field: "freq_range", width: 120, hozAlign: "right", formatter: function(cell) {
const val = cell.getValue();
return val && val !== 0 ? val.toFixed(3) : '-';
}},
{title: "Сим. скорость, БОД", field: "bod_velocity", width: 150, hozAlign: "right", formatter: function(cell) {
const val = cell.getValue();
return val && val !== 0 ? val.toFixed(0) : '-';
}},
{title: "Поляризация", field: "polarization_name", width: 120},
{title: "Модуляция", field: "modulation_name", width: 120},
{title: "Стандарт", field: "standard_name", width: 120},
{
title: "Примечание",
field: "note",
minWidth: 150,
widthGrow: 2,
formatter: function(cell) {
return '<div style="white-space: normal; word-wrap: break-word; padding: 4px 0;">' +
(cell.getValue() || '-') + '</div>';
}
},
{
title: "Создано",
field: "created_at",
width: 140,
formatter: function(cell) {
const val = cell.getValue();
if (!val) return '-';
try {
const date = new Date(val);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
} catch (e) {
return '-';
}
}
},
{
title: "Обновлено",
field: "updated_at",
width: 140,
formatter: function(cell) {
const val = cell.getValue();
if (!val) return '-';
try {
const date = new Date(val);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
} catch (e) {
return '-';
}
}
},
],
});
// Search functionality
function performSearch() {
const searchValue = document.getElementById('toolbar-search').value.trim();
const urlParams = new URLSearchParams(window.location.search);
if (searchValue) {
urlParams.set('search', searchValue);
} else {
urlParams.delete('search');
}
urlParams.delete('page');
window.location.search = urlParams.toString();
}
function clearSearch() {
document.getElementById('toolbar-search').value = '';
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete('search');
urlParams.delete('page');
window.location.search = urlParams.toString();
}
// Handle Enter key in search input
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
// Function to select/deselect all options in a select element
function selectAllOptions(selectName, selectAll) {
const selectElement = document.querySelector('select[name="' + selectName + '"]');
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
}
}
// Filter counter functionality
function updateFilterCounter() {
const form = document.getElementById('filter-form');
let filterCount = 0;
// Count selected satellites
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
if (satelliteSelect) {
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(function(opt) { return opt.selected; });
if (selectedOptions.length > 0) {
filterCount++;
}
}
// Display the filter counter
const counterElement = document.getElementById('filterCounter');
if (counterElement) {
if (filterCount > 0) {
counterElement.textContent = filterCount;
counterElement.style.display = 'inline';
} else {
counterElement.style.display = 'none';
}
}
}
// Delete selected items
function deleteSelected() {
const selectedRows = table.getSelectedRows();
if (selectedRows.length === 0) {
alert('Пожалуйста, выберите хотя бы одну запись для удаления');
return;
}
if (!confirm('Удалить ' + selectedRows.length + ' записей?')) {
return;
}
const selectedIds = selectedRows.map(function(row) { return row.getData().id; });
fetch('{% url "mainapp:tech_analyze_delete" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
ids: selectedIds
})
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
table.replaceData();
} else {
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(function(error) {
console.error('Error:', error);
alert('Произошла ошибка при удалении записей');
});
}
// Show link modal
function showLinkModal() {
const modal = new bootstrap.Modal(document.getElementById('linkToPointsModal'));
document.getElementById('linkResultMessage').style.display = 'none';
modal.show();
}
// Confirm link
function confirmLink(event) {
const satelliteId = document.getElementById('linkSatelliteSelect').value;
const resultDiv = document.getElementById('linkResultMessage');
if (!satelliteId) {
resultDiv.className = 'alert alert-warning';
resultDiv.textContent = 'Пожалуйста, выберите спутник';
resultDiv.style.display = 'block';
return;
}
// Show loading state
const btn = document.getElementById('confirmLinkBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Обработка...';
resultDiv.style.display = 'none';
fetch('{% url "mainapp:tech_analyze_link_existing" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
satellite_id: satelliteId
})
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
resultDiv.className = 'alert alert-success';
resultDiv.innerHTML = '<strong>Привязка завершена!</strong><br>' +
'Обновлено точек: ' + data.updated + '<br>' +
'Пропущено: ' + data.skipped + '<br>' +
'Всего обработано: ' + data.total;
if (data.errors && data.errors.length > 0) {
resultDiv.innerHTML += '<br><br><strong>Ошибки:</strong><br>' + data.errors.join('<br>');
}
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.textContent = 'Ошибка: ' + (data.error || 'Неизвестная ошибка');
}
resultDiv.style.display = 'block';
})
.catch(function(error) {
console.error('Error:', error);
resultDiv.className = 'alert alert-danger';
resultDiv.textContent = 'Произошла ошибка при привязке точек';
resultDiv.style.display = 'block';
})
.finally(function() {
// Re-enable button
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-circle"></i> Привязать';
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// 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 selectFields = form.querySelectorAll('select');
selectFields.forEach(function(select) {
select.addEventListener('change', updateFilterCounter);
});
}
// Update counter when offcanvas is shown
const offcanvasElement = document.getElementById('offcanvasFilters');
if (offcanvasElement) {
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
}
// Set search value from URL
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get('search');
if (searchQuery) {
document.getElementById('toolbar-search').value = searchQuery;
}
});
</script>
{% endblock %}