Files
crm.clientright.ru/codefly.js
Fedor 01c4fe80b5 chore: snapshot current working tree changes
Save all currently accumulated repository changes as a backup snapshot for Gitea so no local work is lost.
2026-03-26 14:19:01 +03:00

446 lines
21 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.

// ============================================================================
// n8n Code Node: Обработка данных о рейсах → Base64 HTML
// ============================================================================
// Вход: [{ data: [{ body: { flights: [...] }}, { body: { data: [...] }}] }]
// Выход: base64 HTML
// ============================================================================
const inputItems = $input.all();
// ================== FALLBACK ==================
if (!inputItems || inputItems.length === 0) {
const html = '<!DOCTYPE html><html><body><h1>Ошибка: данные не получены</h1></body></html>';
const htmlBase64 = Buffer.from(html, 'utf8').toString('base64');
return [{
json: {
html_base64: htmlBase64,
html: html,
flights_count: 0,
error: 'Нет входных данных'
}
}];
}
// ================== ИЗВЛЕЧЕНИЕ ДАННЫХ ==================
let flightAwareData = [];
let flightRadar24Data = [];
let requestData = null;
let flightRadar24Error = null;
try {
const firstItem = inputItems[0];
if (firstItem && firstItem.json && firstItem.json.data && Array.isArray(firstItem.json.data)) {
if (firstItem.json.data[0] && firstItem.json.data[0].body) {
if (firstItem.json.data[0].body.flights) {
flightAwareData = Array.isArray(firstItem.json.data[0].body.flights)
? firstItem.json.data[0].body.flights
: [];
}
}
if (firstItem.json.data[1]) {
if (firstItem.json.data[1].error) {
flightRadar24Error = firstItem.json.data[1].error;
flightRadar24Data = [];
} else if (firstItem.json.data[1].body && firstItem.json.data[1].body.data) {
flightRadar24Data = Array.isArray(firstItem.json.data[1].body.data)
? firstItem.json.data[1].body.data
: [];
}
}
if (firstItem.json.data[2] && firstItem.json.data[2].flight_number) {
requestData = {
flight_number: firstItem.json.data[2].flight_number,
departure_date_local: firstItem.json.data[2].departure_date_local || null,
arrival_date_local: firstItem.json.data[2].arrival_date_local || null
};
}
}
} catch (e) {
console.log('⚠️ Ошибка извлечения данных:', e.message);
}
// ================== УТИЛИТЫ ==================
const safeStr = v => (v == null ? '' : String(v));
const safeDate = v => {
if (!v) return '—';
try {
const d = new Date(v);
return isNaN(d.getTime()) ? '—' : d.toLocaleString('ru-RU', {
timeZone: 'UTC',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return '—';
}
};
const formatDuration = s => !s ? '—' : `${Math.floor(s / 3600)}ч ${Math.floor((s % 3600) / 60)}м`;
const formatDistance = km => !km ? '—' : `${Number(km).toFixed(2)} км`;
// ================== MERGE: все рейсы FA + FR24 по registration ==================
const flightsMap = new Map();
// Ключ для рейса без registration (уникальный per FA-рейс)
const faKey = (f, i) => {
const reg = safeStr(f.registration).trim();
if (reg) return reg;
return (f.fa_flight_id || `FA-${f.ident || 'X'}-${i}-${f.origin?.code_icao}-${f.destination?.code_icao}`).trim();
};
// Добавляем все рейсы из FlightAware (в т.ч. без registration, отменённые)
flightAwareData.forEach((f, i) => {
const key = faKey(f, i);
const reg = safeStr(f.registration).trim();
if (flightsMap.has(key)) {
flightsMap.get(key).fa = f;
} else {
flightsMap.set(key, {
registration: reg || '—',
flightNumber: safeStr(f.flight_number),
ident: safeStr(f.ident),
identIata: safeStr(f.ident_iata),
aircraftType: safeStr(f.aircraft_type),
fa: f,
fr: null
});
}
});
// Добавляем данные из FlightRadar24 (мерж только по registration)
flightRadar24Data.forEach(f => {
const reg = safeStr(f.reg).trim();
if (!reg) return;
if (flightsMap.has(reg)) {
flightsMap.get(reg).fr = f;
} else {
flightsMap.set(reg, {
registration: reg,
flightNumber: safeStr(f.flight),
ident: safeStr(f.callsign),
identIata: safeStr(f.flight),
aircraftType: safeStr(f.type),
fa: null,
fr: f
});
}
});
// ================== ДОБАВЛЕНИЕ ЗАПРОШЕННЫХ РЕЙСОВ БЕЗ ДАННЫХ ==================
const allInputItems = $input.all();
const firstItemForRequest = inputItems[0];
let requestedFlightNumbers = new Set();
let requestFlightNumber = null;
let requestDepartureDate = null;
let requestArrivalDate = null;
if (requestData) {
requestFlightNumber = requestData.flight_number;
requestDepartureDate = requestData.departure_date_local;
requestArrivalDate = requestData.arrival_date_local;
if (requestFlightNumber) requestedFlightNumbers.add(String(requestFlightNumber));
}
allInputItems.forEach(item => {
if (!item?.json) return;
if (item.json.flight_number && (item.json.departure_date_local || item.json.arrival_date_local) && !requestFlightNumber) {
requestFlightNumber = item.json.flight_number || item.json.ident || item.json.flight;
requestDepartureDate = item.json.departure_date_local || null;
requestArrivalDate = item.json.arrival_date_local || null;
if (requestFlightNumber) requestedFlightNumbers.add(String(requestFlightNumber));
}
if (item.json.request_flight_number && !requestFlightNumber) {
requestFlightNumber = item.json.request_flight_number;
requestDepartureDate = item.json.request_departure_date || null;
requestArrivalDate = item.json.request_arrival_date || null;
if (requestFlightNumber) requestedFlightNumbers.add(String(requestFlightNumber));
}
['flight_number', 'ident', 'flight'].forEach(k => { if (item.json[k]) requestedFlightNumbers.add(String(item.json[k])); });
});
if (firstItemForRequest?.json) {
if (Array.isArray(firstItemForRequest.json.requested_flights)) {
firstItemForRequest.json.requested_flights.forEach(flight => {
const n = typeof flight === 'string' ? flight : (flight.flight_number || flight.ident || flight);
if (n) requestedFlightNumbers.add(n);
});
}
const one = firstItemForRequest.json.flight_number || firstItemForRequest.json.ident || firstItemForRequest.json.flight;
if (one) requestedFlightNumbers.add(one);
if (Array.isArray(firstItemForRequest.json.flight_numbers)) {
firstItemForRequest.json.flight_numbers.forEach(n => { if (n) requestedFlightNumbers.add(String(n)); });
}
}
requestedFlightNumbers.forEach(flightNum => {
let found = false;
flightsMap.forEach((flight) => {
if (flight.flightNumber === flightNum || flight.ident === flightNum || flight.identIata === flightNum) found = true;
});
if (!found) {
flightsMap.set(`REQUESTED-${flightNum}`, {
registration: '—',
flightNumber: flightNum,
ident: flightNum,
identIata: flightNum,
aircraftType: '—',
fa: null,
fr: null,
isRequested: true
});
}
});
const flights = Array.from(flightsMap.values());
// ================== HTML: карточка рейса ==================
const generateFlightCard = (f) => {
const fa = f.fa;
const fr = f.fr;
if (f.isRequested && !fa && !fr) {
const matchesRequest = requestFlightNumber && (String(f.flightNumber) === String(requestFlightNumber) || String(f.ident) === String(requestFlightNumber));
let requestInfo = '';
if (matchesRequest) {
if (requestDepartureDate) requestInfo += `<div class="info-row"><span class="label">Дата вылета (запрос):</span><span class="value">${requestDepartureDate}</span></div>`;
if (requestArrivalDate) requestInfo += `<div class="info-row"><span class="label">Дата прилёта (запрос):</span><span class="value">${requestArrivalDate}</span></div>`;
}
return `
<div class="flight-card">
<div class="flight-header">
<h2>Рейс ${f.flightNumber || f.ident || '—'}</h2>
<span class="registration">Запрошен</span>
</div>
<div class="flight-info">
<div class="info-row"><span class="label">Запрошенный рейс:</span><span class="value">${f.flightNumber || f.ident || '—'}</span></div>
${requestInfo}
</div>
</div>`;
}
const isCancelled = !!(fa && fa.cancelled);
const headerBadge = isCancelled
? '<span class="badge-cancelled">Отменён</span>'
: `<span class="registration">${f.registration || '—'}</span>`;
let card = `
<div class="flight-card${isCancelled ? ' cancelled' : ''}">
<div class="flight-header">
<h2>Рейс ${f.flightNumber || f.ident || '—'}</h2>
${headerBadge}
</div>
<div class="flight-info">
<div class="info-row"><span class="label">Тип самолёта:</span><span class="value">${f.aircraftType || '—'}</span></div>
<div class="info-row"><span class="label">Идентификатор:</span><span class="value">${f.ident || '—'} (${f.identIata || '—'})</span></div>
</div>`;
if (fa) {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
</div>
<div class="source-content">
${isCancelled ? '<div class="status-cancelled">✕ Рейс отменён</div>' : ''}
<div class="route-info">
<div class="route-item">
<span class="route-label">Откуда:</span>
<span class="route-value">${safeStr(fa.origin?.name || fa.origin?.code_iata || '—')} (${safeStr(fa.origin?.code_iata || '—')})</span>
</div>
<div class="route-item">
<span class="route-label">Куда:</span>
<span class="route-value">${safeStr(fa.destination?.name || fa.destination?.code_iata || '—')} (${safeStr(fa.destination?.code_iata || '—')})</span>
</div>
</div>
<div class="timeline">
<div class="timeline-item">
<span class="timeline-label">Плановый вылет:</span>
<span class="timeline-value">${safeDate(fa.scheduled_out)}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Фактический вылет:</span>
<span class="timeline-value">${safeDate(fa.actual_out)}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Взлёт:</span>
<span class="timeline-value">${safeDate(fa.actual_off)} ${fa.actual_runway_off ? `(ВПП ${fa.actual_runway_off})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Посадка:</span>
<span class="timeline-value">${safeDate(fa.actual_on)} ${fa.actual_runway_on ? `(ВПП ${fa.actual_runway_on})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Фактический прилёт:</span>
<span class="timeline-value">${safeDate(fa.actual_in)}</span>
</div>
</div>
<div class="status-info">
<div class="status-item">
<span class="status-label">Статус:</span>
<span class="status-value">${safeStr(fa.status || '—')}</span>
</div>
${fa.departure_delay != null ? `
<div class="status-item">
<span class="status-label">Задержка вылета:</span>
<span class="status-value ${fa.departure_delay < 0 ? 'delay-negative' : 'delay-positive'}">${fa.departure_delay > 0 ? '+' : ''}${Math.floor(fa.departure_delay / 60)} мин</span>
</div>
` : ''}
${fa.arrival_delay != null ? `
<div class="status-item">
<span class="status-label">Задержка прилёта:</span>
<span class="status-value ${fa.arrival_delay < 0 ? 'delay-negative' : 'delay-positive'}">${fa.arrival_delay > 0 ? '+' : ''}${Math.floor(fa.arrival_delay / 60)} мин</span>
</div>
` : ''}
${fa.gate_origin ? `<div class="status-item"><span class="status-label">Гейт вылета:</span><span class="status-value">${fa.gate_origin}</span></div>` : ''}
${fa.gate_destination ? `<div class="status-item"><span class="status-label">Гейт прилёта:</span><span class="status-value">${fa.gate_destination}</span></div>` : ''}
${fa.baggage_claim ? `<div class="status-item"><span class="status-label">Выдача багажа:</span><span class="status-value">${fa.baggage_claim}</span></div>` : ''}
</div>
</div>
</div>`;
}
if (fr) {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightradar24">FlightRadar24</span>
</div>
<div class="source-content">
<div class="route-info">
<div class="route-item"><span class="route-label">Откуда:</span><span class="route-value">${safeStr(fr.orig_iata || '—')} (${safeStr(fr.orig_icao || '—')})</span></div>
<div class="route-item"><span class="route-label">Куда:</span><span class="route-value">${safeStr(fr.dest_iata || '—')} (${safeStr(fr.dest_icao || '—')})</span></div>
</div>
<div class="timeline">
<div class="timeline-item">
<span class="timeline-label">Взлёт:</span>
<span class="timeline-value">${safeDate(fr.datetime_takeoff)} ${fr.runway_takeoff ? `(ВПП ${fr.runway_takeoff})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Посадка:</span>
<span class="timeline-value">${safeDate(fr.datetime_landed)} ${fr.runway_landed ? `(ВПП ${fr.runway_landed})` : ''}</span>
</div>
</div>
<div class="status-info">
<div class="status-item"><span class="status-label">Время полёта:</span><span class="status-value">${formatDuration(fr.flight_time)}</span></div>
<div class="status-item"><span class="status-label">Фактическое расстояние:</span><span class="status-value">${formatDistance(fr.actual_distance)}</span></div>
<div class="status-item"><span class="status-label">Кратчайшее расстояние:</span><span class="status-value">${formatDistance(fr.circle_distance)}</span></div>
<div class="status-item"><span class="status-label">Статус полёта:</span><span class="status-value">${fr.flight_ended ? 'Завершён' : 'В процессе'}</span></div>
</div>
</div>
</div>`;
}
card += `</div>`;
return card;
};
// ================== HTML ==================
const now = new Date();
const reportDate = now.toLocaleString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const html = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Отчёт о рейсах</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.4; color: #333; background: #f5f5f5; padding: 15px; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.header { border-bottom: 3px solid #2563eb; padding-bottom: 8px; margin-bottom: 8px; }
.header h1 { color: #1e40af; font-size: 24px; margin-bottom: 4px; }
.header-meta { color: #666; font-size: 13px; }
.sources-info { display: flex; gap: 10px; margin-top: 4px; flex-wrap: wrap; }
.source-tag { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.source-tag.available { background: #d1fae5; color: #065f46; }
.source-tag.unavailable { background: #fee2e2; color: #991b1b; }
.flight-card { border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 18px; overflow: hidden; background: white; }
.flight-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 18px; display: flex; justify-content: space-between; align-items: center; }
.flight-header h2 { font-size: 20px; margin: 0; }
.registration { background: rgba(255,255,255,0.2); padding: 4px 10px; border-radius: 4px; font-weight: 600; font-size: 13px; }
.flight-info { padding: 12px 18px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
.info-row { display: flex; margin-bottom: 6px; }
.info-row:last-child { margin-bottom: 0; }
.info-row .label { font-weight: 600; color: #4b5563; width: 140px; flex-shrink: 0; font-size: 13px; }
.info-row .value { color: #111827; font-size: 13px; }
.source-section { border-top: 1px solid #e5e7eb; padding: 12px 18px; }
.source-section:first-of-type { border-top: none; }
.source-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.source-badge { display: inline-block; padding: 5px 12px; border-radius: 5px; font-size: 12px; font-weight: 600; color: white; }
.source-badge.source-flightaware { background: #3b82f6; }
.source-badge.source-flightradar24 { background: #10b981; }
.source-missing { color: #ef4444; font-size: 12px; font-style: italic; }
.source-content { margin-left: 0; }
.route-info { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; padding: 12px; background: #f9fafb; border-radius: 6px; }
.route-item { display: flex; flex-direction: column; }
.route-label { font-size: 11px; color: #6b7280; margin-bottom: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.route-value { font-size: 14px; font-weight: 600; color: #111827; }
.timeline { margin-bottom: 12px; }
.timeline-item { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #e5e7eb; }
.timeline-item:last-child { border-bottom: none; }
.timeline-label { font-weight: 500; color: #4b5563; width: 160px; flex-shrink: 0; font-size: 12px; }
.timeline-value { text-align: right; font-size: 12px; }
.status-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; padding: 12px; background: #f9fafb; border-radius: 6px; }
.status-item { display: flex; flex-direction: column; }
.status-label { font-size: 11px; color: #6b7280; margin-bottom: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.status-value { font-size: 13px; font-weight: 600; color: #111827; }
.delay-negative { color: #10b981; }
.delay-positive { color: #ef4444; }
.flight-card.cancelled { border-color: #dc2626; }
.flight-card.cancelled .flight-header { background: linear-gradient(135deg, #b91c1c 0%, #7f1d1d 100%); }
.status-cancelled { display: block; padding: 10px 14px; margin-bottom: 12px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 6px; color: #b91c1c; font-weight: 700; font-size: 14px; }
.badge-cancelled { background: #dc2626 !important; color: white; padding: 4px 10px; border-radius: 4px; font-weight: 600; font-size: 13px; }
.no-data { text-align: center; padding: 40px 20px; color: #6b7280; font-size: 16px; }
@media print { body { background: white; padding: 0; } .container { box-shadow: none; padding: 15px; } .flight-card { page-break-inside: avoid; margin-bottom: 15px; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Отчёт о рейсах</h1>
<div class="header-meta">
<div>Дата формирования: ${reportDate}</div>
<div class="sources-info">
<span class="source-tag ${flightAwareData.length > 0 ? 'available' : 'unavailable'}">
FlightAware: ${flightAwareData.length > 0 ? '✓ Данные получены' : '✗ Данные отсутствуют'}
</span>
<span class="source-tag ${flightRadar24Data.length > 0 ? 'available' : 'unavailable'}">
FlightRadar24: ${flightRadar24Data.length > 0 ? '✓ Данные получены' : '✗ Данные отсутствуют'}
</span>
</div>
</div>
</div>
<div class="flights-container">
${flights.length ? flights.map(f => generateFlightCard(f)).join('') : '<div class="no-data">Данные о рейсах не найдены</div>'}
</div>
</div>
</body>
</html>`;
const htmlBase64 = Buffer.from(html, 'utf8').toString('base64');
return [{
json: {
html_base64: htmlBase64,
html: html,
flights_count: flights.length,
sources: {
flightaware: { available: flightAwareData.length > 0, count: flightAwareData.length },
flightradar24: { available: flightRadar24Data.length > 0, count: flightRadar24Data.length }
},
generated_at: now.toISOString()
}
}];