Files
wiki/tasks/flightradar24/frontend/static/schedule.js
2026-04-25 01:40:01 +03:00

364 lines
14 KiB
JavaScript
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.
/* schedule.js — filters, pagination, table render, CSV export */
const PAGE_SIZE = 100;
let currentOffset = 0;
let currentTotal = 0;
// ── init ──────────────────────────────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", () => {
// Default date range: last 7 days
const today = new Date();
const week = new Date(today);
week.setDate(today.getDate() - 7);
document.getElementById("f-date-from").value = fmtDate(week);
document.getElementById("f-date-to").value = fmtDate(today);
loadData();
});
// ── filter helpers ────────────────────────────────────────────────────────────
function getFilters() {
return {
date_from: document.getElementById("f-date-from").value || "",
date_to: document.getElementById("f-date-to").value || "",
airport: document.getElementById("f-airport").value,
direction: document.getElementById("f-direction").value,
flight_number: document.getElementById("f-flight").value.trim(),
time_from: document.getElementById("f-time-from").value || "",
time_to: document.getElementById("f-time-to").value || "",
};
}
function buildQuery(extra = {}) {
const f = { ...getFilters(), limit: PAGE_SIZE, offset: currentOffset, ...extra };
return Object.entries(f)
.filter(([, v]) => v !== "" && v !== "all")
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join("&");
}
function applyFilters() {
currentOffset = 0;
loadData();
}
function resetFilters() {
document.getElementById("f-date-from").value = "";
document.getElementById("f-date-to").value = "";
document.getElementById("f-airport").value = "all";
document.getElementById("f-direction").value = "all";
document.getElementById("f-flight").value = "";
document.getElementById("f-time-from").value = "";
document.getElementById("f-time-to").value = "";
currentOffset = 0;
loadData();
}
// ── pagination ────────────────────────────────────────────────────────────────
function prevPage() {
if (currentOffset <= 0) return;
currentOffset = Math.max(0, currentOffset - PAGE_SIZE);
loadData();
}
function nextPage() {
if (currentOffset + PAGE_SIZE >= currentTotal) return;
currentOffset += PAGE_SIZE;
loadData();
}
function updatePagination() {
const page = Math.floor(currentOffset / PAGE_SIZE) + 1;
const pages = Math.max(1, Math.ceil(currentTotal / PAGE_SIZE));
document.getElementById("page-info").textContent = `Стр. ${page} / ${pages}`;
document.getElementById("total-info").textContent = `Всего: ${currentTotal.toLocaleString("ru")}`;
document.getElementById("btn-prev").disabled = currentOffset <= 0;
document.getElementById("btn-next").disabled = currentOffset + PAGE_SIZE >= currentTotal;
}
// ── data load ─────────────────────────────────────────────────────────────────
async function loadData() {
setLoading(true);
try {
const resp = await fetch(`/api/schedule/data?${buildQuery()}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
currentTotal = data.total || 0;
renderTable(data.flights || []);
renderCards(data.flights || []);
updatePagination();
document.getElementById("last-updated").textContent =
"Обновлено: " + new Date().toLocaleTimeString("ru");
} catch (e) {
showError(e.message);
} finally {
setLoading(false);
}
}
// ── render table ──────────────────────────────────────────────────────────────
// Колонки (13): Дата | Рейс | Авиакомпания | ↑↓ | Маршрут | Аэропорт |
// По расп. | Взлёт факт | Посадка факт | Длит. | ВПП | ВС | Трек
function renderTable(flights) {
const tbody = document.getElementById("table-body");
if (!flights.length) {
tbody.innerHTML = `<tr><td colspan="12" class="state-msg">Нет данных по выбранным фильтрам</td></tr>`;
return;
}
tbody.innerHTML = flights.map(f => {
// 1. Дата
const dateStr = fmtDateShort(f.scheduled_at);
// 2. Рейс — номер + cat badge + source badge
const catBadge = categoryBadge(f.flight_category);
const srcBadge = f.sched_source === "fr24"
? `<span class="src-badge src-fr24" title="Источник: FR24 API">FR</span>`
: "";
const flightCell = `<strong>${esc(f.flight_number)}</strong>${catBadge}${srcBadge}`;
// 3. Авиакомпания + регистрация
const airlineCell = esc(f.airline || "—")
+ (f.registration ? `<br><small style="color:#6e7681">${esc(f.registration)}</small>` : "");
// 4. Направление
const dirIcon = f.direction === "departure"
? `<span class="dir-icon dir-dep" title="Вылет">↑</span>`
: `<span class="dir-icon dir-arr" title="Прилёт">↓</span>`;
// 5. Маршрут
const route = routeStr(f);
// 6. Аэропорт
const airport = esc(f.airport);
// 7. По расписанию (MSK)
const sched = fmtTime(f.scheduled_at);
// 8. Фактическое время: вылет → actual_takeoff, прилёт → actual_landed
const actTime = f.direction === "departure" ? f.actual_takeoff : f.actual_landed;
const actDelay = f.direction === "departure" ? f.delay_takeoff_min : f.delay_landed_min;
const actCell = actTime
? `<span class="act-time">${fmtTime(actTime)}</span> ${delayCell(actDelay)}`
: "—";
// 9. Длительность (FA flight_time > schedule duration_min)
const durationCell = fmtDuration(f.duration_eff);
// 11. ВПП (по направлению рейса)
const runway = f.direction === "departure"
? (f.runway_takeoff || "—")
: (f.runway_landed || "—");
// 12. Тип ВС
const acType = f.aircraft_type ? esc(f.aircraft_type) : "—";
// 13. Трек: ✈ FR24 + 🛰 RTL-SDR + FA badge
const trackCol = [
f.fr24_id
? `<a href="https://www.flightradar24.com/data/flights/${f.fr24_id}" target="_blank" class="track-link" title="Трек FR24">✈</a>`
: "",
f.track_source === "rtlsdr"
? `<span title="${f.track_points || 0} точек RTL-SDR" style="color:#3fb950;cursor:default">🛰</span>`
: "",
f.track_source === "fa"
? `<span title="${f.track_points || 0} точек FA" style="color:#d29922;cursor:default">FA</span>`
: "",
].filter(Boolean).join(" ") || "—";
return `<tr>
<td>${dateStr}</td>
<td>${flightCell}</td>
<td>${airlineCell}</td>
<td>${dirIcon}</td>
<td>${esc(route)}</td>
<td>${airport}</td>
<td>${sched}</td>
<td>${actCell}</td>
<td>${durationCell}</td>
<td>${runway}</td>
<td>${acType}</td>
<td>${trackCol}</td>
</tr>`;
}).join("");
}
// ── render cards (mobile) ─────────────────────────────────────────────────────
function renderCards(flights) {
const container = document.getElementById("cards-container");
if (!flights.length) {
container.innerHTML = `<div class="state-msg">Нет данных по выбранным фильтрам</div>`;
return;
}
container.innerHTML = flights.map(f => {
const dirLabel = f.direction === "departure" ? "↑ Вылет" : "↓ Прилёт";
const dirClass = f.direction === "departure" ? "dir-dep" : "dir-arr";
const route = routeStr(f);
const sched = fmtTime(f.scheduled_at);
const catBadge = categoryBadge(f.flight_category);
const srcBadge = f.sched_source === "fr24"
? `<span class="src-badge src-fr24" title="Источник: FR24 API">FR</span>`
: "";
// Track column for card
const trackLinks = [
f.fr24_id
? `<a href="https://www.flightradar24.com/data/flights/${f.fr24_id}" target="_blank" style="color:#58a6ff">✈ FR24</a>`
: "",
f.track_source === "rtlsdr"
? `<span title="${f.track_points || 0} точек RTL-SDR" style="color:#3fb950">🛰 RTL-SDR</span>`
: "",
f.track_source === "fa"
? `<span title="${f.track_points || 0} точек FA" style="color:#d29922">FA трек</span>`
: "",
].filter(Boolean).join(" ");
const acInfo = [
f.aircraft_type ? esc(f.aircraft_type) : "",
f.registration ? `<span style="color:#6e7681">${esc(f.registration)}</span>` : "",
].filter(Boolean).join(" / ");
const runway = f.direction === "departure"
? (f.runway_takeoff || "—")
: (f.runway_landed || "—");
return `<div class="card">
<div class="card-header">
<div>
<div class="card-flight">${esc(f.flight_number)}${catBadge}${srcBadge}</div>
<div class="card-airline">${esc(f.airline || "—")}</div>
</div>
</div>
<div class="card-row"><span>Аэропорт</span><span>${esc(f.airport)}</span></div>
<div class="card-row"><span>Направление</span><span class="${dirClass}">${dirLabel}</span></div>
<div class="card-row"><span>Маршрут</span><span>${esc(route)}</span></div>
<div class="card-row"><span>По расписанию</span><span>${sched}</span></div>
${f.actual_takeoff
? `<div class="card-row"><span>Взлёт факт</span><span>${fmtTime(f.actual_takeoff)} ${delayCell(f.delay_takeoff_min)}</span></div>`
: ""}
${f.actual_landed
? `<div class="card-row"><span>Посадка факт</span><span>${fmtTime(f.actual_landed)} ${delayCell(f.delay_landed_min)}</span></div>`
: ""}
${f.duration_eff
? `<div class="card-row"><span>Длительность</span><span>${fmtDuration(f.duration_eff)}</span></div>`
: ""}
${runway !== "—"
? `<div class="card-row"><span>ВПП</span><span>${runway}</span></div>`
: ""}
${acInfo
? `<div class="card-row"><span>ВС / Борт</span><span>${acInfo}</span></div>`
: ""}
${trackLinks
? `<div class="card-row"><span>Трек</span><span>${trackLinks}</span></div>`
: ""}
</div>`;
}).join("");
}
// ── CSV export ────────────────────────────────────────────────────────────────
function exportCsv() {
const qs = buildQuery({ limit: 100000, offset: 0 });
window.location.href = `/api/schedule/export?${qs}`;
}
// ── helpers ───────────────────────────────────────────────────────────────────
function routeStr(f) {
// 1. Яндекс thread_title (приоритет 1)
const tt = f.thread_title || "";
if (tt) return tt;
// 2. FR24 IATA коды → города (приоритет 2)
const fo = f.fa_origin_city || f.fa_origin_iata || "";
const fd = f.fa_dest_city || f.fa_dest_iata || "";
if (fo || fd) {
if (!fo) return `${fd}`;
if (!fd) return `${fo}`;
return `${fo}${fd}`;
}
// 3. Яндекс IATA → города (запасной вариант)
const o = f.origin || "";
const d = f.destination || "";
if (!o && !d) return "—";
if (!o) return `${d}`;
if (!d) return `${o}`;
return `${o}${d}`;
}
function fmtDuration(min) {
if (!min) return "—";
const h = Math.floor(min / 60);
const m = min % 60;
return h > 0 ? `${h}ч ${m}м` : `${m}м`;
}
function categoryBadge(category) {
if (!category) return "";
const cls = {
"Passenger": "cat-pax",
"Cargo": "cat-cargo",
"Military": "cat-mil",
}[category] || "cat-other";
const labels = {
"Passenger": "P",
"Cargo": "C",
"Military": "M",
};
return `<span class="cat-badge ${cls}" title="${category}">${labels[category] || category}</span>`;
}
function delayCell(min) {
if (min == null) return "";
if (min > 30) return `<span class="delay-critical">+${min}</span>`;
if (min > 0) return `<span class="delay-pos">+${min}</span>`;
if (min < 0) return `<span class="delay-neg">${min}</span>`;
return `<span class="delay-ok">0</span>`;
}
function fmtDate(d) {
return d.toISOString().slice(0, 10);
}
function fmtDateShort(iso) {
if (!iso) return "—";
return iso.slice(0, 10);
}
function fmtTime(iso) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleTimeString("ru", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Moscow" });
} catch { return iso.slice(11, 16); }
}
function esc(s) {
if (!s) return "—";
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function setLoading(on) {
if (on) {
document.getElementById("table-body").innerHTML =
`<tr><td colspan="12" class="state-msg">Загрузка…</td></tr>`;
document.getElementById("cards-container").innerHTML =
`<div class="state-msg">Загрузка…</div>`;
}
}
function showError(msg) {
document.getElementById("table-body").innerHTML =
`<tr><td colspan="12" class="state-msg" style="color:#f85149">Ошибка: ${esc(msg)}</td></tr>`;
document.getElementById("cards-container").innerHTML =
`<div class="state-msg" style="color:#f85149">Ошибка: ${esc(msg)}</div>`;
}