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

258 lines
9.6 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="10" 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);
return `<tr>
<td>${dateStr}</td>
<td><strong>${esc(f.flight_number)}</strong></td>
<td>${esc(f.airline || "—")}</td>
<td>${esc(f.airport)}</td>
<td>${dirIcon}</td>
<td>${esc(route)}</td>
<td>${sched}</td>
<td>${actual}</td>
<td>${delay}</td>
<td>${badge}</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);
return `<div class="card">
<div class="card-header">
<div>
<div class="card-flight">${esc(f.flight_number)}</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>
</div>`;
}).join("");
}
// ── CSV export ────────────────────────────────────────────────────────────────
function exportCsv() {
const qs = buildQuery({ limit: 100000, offset: 0 });
window.location.href = `/api/schedule/export?${qs}`;
}
// ── helpers ───────────────────────────────────────────────────────────────────
function routeStr(f) {
const o = f.origin || "";
const d = f.destination || "";
if (!o && !d) return "—";
if (!o) return `${d}`;
if (!d) return `${o}`;
return `${o}${d}`;
}
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 > 0) return `<span class="delay-pos">+${min}</span>`;
if (min < 0) return `<span class="delay-neg">${min}</span>`;
return "0";
}
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="10" 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="10" 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>`;
}