Files
wiki/tasks/flightradar24/frontend/static/schedule.js
2026-04-21 18:50:01 +03:00

310 lines
12 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 ──────────────────────────────────────────────────────────────
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 => {
const dirIcon = f.direction === "departure"
? `<span class="dir-icon dir-dep" title="Вылет">↑</span>`
: `<span class="dir-icon dir-arr" title="Прилёт">↓</span>`;
const route = routeStr(f);
const sched = fmtTime(f.scheduled_at);
const actual = f.actual_at ? fmtTime(f.actual_at) : "—";
const delay = delayCell(f.delay_min);
const badge = statusBadge(f.status);
const dateStr = fmtDateShort(f.scheduled_at);
// Actual times from FR24 enrichment
const actTakeoff = f.actual_takeoff ? fmtTime(f.actual_takeoff) : "";
const actLanded = f.actual_landed ? fmtTime(f.actual_landed) : "";
const delayTakeoff = delayCell(f.delay_takeoff_min);
const delayLanded = delayCell(f.delay_landed_min);
// Category badge
const catBadge = categoryBadge(f.flight_category);
// FR24 track link
const trackLink = f.fr24_id
? `<a href="https://www.flightradar24.com/data/flights/${f.fr24_id}" target="_blank" class="track-link" title="Трек FR24">✈</a>`
: "";
// Enriched actual time display: show actual takeoff/landed if available
const actualDisplay = actTakeoff || actLanded
? `<span class="act-time">${actTakeoff || "—"}</span>` +
(f.direction === "arrival" && actLanded ? ` <span class="act-landed">⇣${actLanded}</span>` : "")
: actual;
return `<tr>
<td>${dateStr}</td>
<td><strong>${esc(f.flight_number)}</strong> ${trackLink}</td>
<td>${esc(f.airline || "—")}</td>
<td>${esc(f.airport)}</td>
<td>${dirIcon}</td>
<td>${esc(route)}</td>
<td>${sched}</td>
<td>${actualDisplay}</td>
<td>${delayTakeoff}</td>
<td>${catBadge}</td>
<td>${badge}</td>
<td>${trackLink}</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 actual = f.actual_at ? fmtTime(f.actual_at) : "—";
const delay = f.delay_min != null ? `${f.delay_min > 0 ? "+" : ""}${f.delay_min} мин` : "—";
const badge = statusBadge(f.status);
const catBadge = categoryBadge(f.flight_category);
const trackLink = f.fr24_id
? `<a href="https://www.flightradar24.com/data/flights/${f.fr24_id}" target="_blank" style="color:#58a6ff">Трек</a>`
: "";
const actTakeoff = f.actual_takeoff ? fmtTime(f.actual_takeoff) : "";
const actLanded = f.actual_landed ? fmtTime(f.actual_landed) : "";
return `<div class="card">
<div class="card-header">
<div>
<div class="card-flight">${esc(f.flight_number)} ${catBadge} ${trackLink}</div>
<div class="card-airline">${esc(f.airline || "—")}</div>
</div>
${badge}
</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>
<div class="card-row"><span>Фактически</span><span>${actual}</span></div>
<div class="card-row"><span>Задержка</span><span>${delay}</span></div>
${actTakeoff ? `<div class="card-row"><span>Взлёт факт</span><span>${actTakeoff} ${delayCell(f.delay_takeoff_min)}</span></div>` : ""}
${actLanded ? `<div class="card-row"><span>Посадка факт</span><span>${actLanded} ${delayCell(f.delay_landed_min)}</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) {
// Prefer thread_title from Yandex (most accurate)
const tt = f.thread_title || "";
if (tt) {
// "Москва — Алматы" → for departure show destination, for arrival show origin
return tt;
}
const o = f.origin || "";
const d = f.destination || "";
if (!o && !d) return "—";
if (!o) return `${d}`;
if (!d) return `${o}`;
return `${o}${d}`;
}
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 statusBadge(status) {
const map = {
scheduled: "badge-scheduled",
departed: "badge-departed",
arrived: "badge-arrived",
delayed: "badge-delayed",
cancelled: "badge-cancelled",
};
const cls = map[status] || "badge-scheduled";
const labels = {
scheduled: "По расписанию",
departed: "Вылетел",
arrived: "Прилетел",
delayed: "Задержан",
cancelled: "Отменён",
};
return `<span class="badge ${cls}">${labels[status] || status || "—"}</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>`;
}