/* 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 = `Нет данных по выбранным фильтрам`; 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" ? `FR` : ""; const flightCell = `${esc(f.flight_number)}${catBadge}${srcBadge}`; // 3. Авиакомпания + регистрация const airlineCell = esc(f.airline || "—") + (f.registration ? `
${esc(f.registration)}` : ""); // 4. Направление const dirIcon = f.direction === "departure" ? `` : ``; // 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 ? `${fmtTime(actTime)} ${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 ? `` : "", f.track_source === "rtlsdr" ? `🛰` : "", f.track_source === "fa" ? `FA` : "", ].filter(Boolean).join(" ") || "—"; return ` ${dateStr} ${flightCell} ${airlineCell} ${dirIcon} ${esc(route)} ${airport} ${sched} ${actCell} ${durationCell} ${runway} ${acType} ${trackCol} `; }).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 catBadge = categoryBadge(f.flight_category); const srcBadge = f.sched_source === "fr24" ? `FR` : ""; // Track column for card const trackLinks = [ f.fr24_id ? `✈ FR24` : "", f.track_source === "rtlsdr" ? `🛰 RTL-SDR` : "", f.track_source === "fa" ? `FA трек` : "", ].filter(Boolean).join(" "); const acInfo = [ f.aircraft_type ? esc(f.aircraft_type) : "", f.registration ? `${esc(f.registration)}` : "", ].filter(Boolean).join(" / "); const runway = f.direction === "departure" ? (f.runway_takeoff || "—") : (f.runway_landed || "—"); return `
${esc(f.flight_number)}${catBadge}${srcBadge}
${esc(f.airline || "—")}
Аэропорт${esc(f.airport)}
Направление${dirLabel}
Маршрут${esc(route)}
По расписанию${sched}
${f.actual_takeoff ? `
Взлёт факт${fmtTime(f.actual_takeoff)} ${delayCell(f.delay_takeoff_min)}
` : ""} ${f.actual_landed ? `
Посадка факт${fmtTime(f.actual_landed)} ${delayCell(f.delay_landed_min)}
` : ""} ${f.duration_eff ? `
Длительность${fmtDuration(f.duration_eff)}
` : ""} ${runway !== "—" ? `
ВПП${runway}
` : ""} ${acInfo ? `
ВС / Борт${acInfo}
` : ""} ${trackLinks ? `
Трек${trackLinks}
` : ""}
`; }).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 `${labels[category] || category}`; } 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)}
`; }