/* 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 = `
| Нет данных по выбранным фильтрам |
`;
return;
}
tbody.innerHTML = flights.map(f => {
const dirIcon = f.direction === "departure"
? `↑`
: `↓`;
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
? `✈`
: "";
// Enriched actual time display: show actual takeoff/landed if available
const actualDisplay = actTakeoff || actLanded
? `${actTakeoff || "—"}` +
(f.direction === "arrival" && actLanded ? ` ⇣${actLanded}` : "")
: actual;
return `
| ${dateStr} |
${esc(f.flight_number)} ${trackLink} |
${esc(f.airline || "—")} |
${esc(f.airport)} |
${dirIcon} |
${esc(route)} |
${sched} |
${actualDisplay} |
${delayTakeoff} |
${catBadge} |
${badge} |
${trackLink} |
`;
}).join("");
}
// ── render cards (mobile) ─────────────────────────────────────────────────────
function renderCards(flights) {
const container = document.getElementById("cards-container");
if (!flights.length) {
container.innerHTML = `Нет данных по выбранным фильтрам
`;
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
? `Трек`
: "";
const actTakeoff = f.actual_takeoff ? fmtTime(f.actual_takeoff) : "";
const actLanded = f.actual_landed ? fmtTime(f.actual_landed) : "";
return `
Аэропорт${esc(f.airport)}
Направление${dirLabel}
Маршрут${esc(route)}
Запланировано${sched}
Фактически${actual}
Задержка${delay}
${actTakeoff ? `
Взлёт факт${actTakeoff} ${delayCell(f.delay_takeoff_min)}
` : ""}
${actLanded ? `
Посадка факт${actLanded} ${delayCell(f.delay_landed_min)}
` : ""}
`;
}).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 `${labels[category] || category}`;
}
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 `${labels[status] || status || "—"}`;
}
function delayCell(min) {
if (min == null) return "—";
if (min > 30) return `+${min}`;
if (min > 0) return `+${min}`;
if (min < 0) return `${min}`;
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, "&")
.replace(//g, ">");
}
function setLoading(on) {
if (on) {
document.getElementById("table-body").innerHTML =
`| Загрузка… |
`;
document.getElementById("cards-container").innerHTML =
`Загрузка…
`;
}
}
function showError(msg) {
document.getElementById("table-body").innerHTML =
`| Ошибка: ${esc(msg)} |
`;
document.getElementById("cards-container").innerHTML =
`Ошибка: ${esc(msg)}
`;
}