Files
aiform_prod/docs/N8N_CODE_PROCESS_FLIGHTS_DATA.js
AI Assistant 2e45786e46 feat: Telegram Mini App integration and UX improvements
- Добавлена полная интеграция с Telegram Mini App (динамическая загрузка SDK)
- Отдельный компактный дизайн для Telegram Mini App
- Добавлен loader при инициализации (предотвращает мелькание SMS-авторизации)
- Улучшена навигация: кнопки "Назад" и "К списку заявок" теперь сохраняют авторизацию
- Telegram Mini App: кнопка "Выход" просто закрывает приложение
- Telegram Mini App: заявки "В работе" скрыты из списка
- Веб-версия: для заявок "В работе" добавлена кнопка "Просмотреть в Telegram" (ссылка на @klientprav_bot)
- Telegram Mini App: кнопки действий в черновиках расположены вертикально
- Веб-версия: убрано отображение номера телефона в приветствии
- Исправлена проблема с возвратом к списку черновиков (не требует повторной SMS-авторизации)
- Заблокировано удаление и редактирование заявок со статусом "В работе"
- Добавлена документация по Telegram Mini App интеграции
2026-01-29 16:12:48 +03:00

699 lines
20 KiB
JavaScript
Raw Permalink 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: Обработка данных о рейсах из FlightAware и FlightRadar24
// ============================================================================
// Объединяет данные из двух источников и формирует красивый HTML для PDF
// ============================================================================
// ==== ПОЛУЧЕНИЕ ВХОДНЫХ ДАННЫХ ====
// Ожидаемая структура: массив с двумя элементами
// [0] - данные из FlightAware (body.flights[])
// [1] - данные из FlightRadar24 (body.data[])
const inputItems = $input.all();
if (!inputItems || inputItems.length === 0) {
return [{
json: {
error: 'Нет входных данных',
html: '<html><body><h1>Ошибка: данные не получены</h1></body></html>',
flights: [],
sources: { flightaware: false, flightradar24: false }
}
}];
}
// ==== ИЗВЛЕЧЕНИЕ ДАННЫХ ИЗ ИСТОЧНИКОВ ====
let flightAwareData = [];
let flightRadar24Data = [];
try {
// Первый элемент - FlightAware
const faItem = inputItems[0];
if (faItem && faItem.json && faItem.json.body && faItem.json.body.flights) {
flightAwareData = Array.isArray(faItem.json.body.flights)
? faItem.json.body.flights
: [];
}
} catch (e) {
console.log('⚠️ Ошибка извлечения FlightAware:', e.message);
}
try {
// Второй элемент - FlightRadar24
const fr24Item = inputItems[1];
if (fr24Item && fr24Item.json && fr24Item.json.body && fr24Item.json.body.data) {
flightRadar24Data = Array.isArray(fr24Item.json.body.data)
? fr24Item.json.body.data
: [];
}
} catch (e) {
console.log('⚠️ Ошибка извлечения FlightRadar24:', e.message);
}
// ==== УТИЛИТЫ ====
const safeStr = (v) => (v == null ? '' : String(v));
const safeDate = (v) => {
if (!v) return '—';
try {
const d = new Date(v);
return d.toLocaleString('ru-RU', {
timeZone: 'UTC',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return v;
}
};
const formatDuration = (seconds) => {
if (!seconds) return '—';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}ч ${minutes}м`;
};
const formatDistance = (km) => {
if (!km) return '—';
return `${Number(km).toFixed(2)} км`;
};
// ==== ОБЪЕДИНЕНИЕ ДАННЫХ ПО REGISTRATION ====
// Создаём карту для быстрого поиска
const flightsMap = new Map();
// Добавляем данные из FlightAware
flightAwareData.forEach(flight => {
const reg = safeStr(flight.registration).trim();
if (!reg) return;
if (!flightsMap.has(reg)) {
flightsMap.set(reg, {
registration: reg,
flightNumber: safeStr(flight.flight_number),
ident: safeStr(flight.ident),
identIata: safeStr(flight.ident_iata),
aircraftType: safeStr(flight.aircraft_type),
flightAware: flight,
flightRadar24: null
});
} else {
flightsMap.get(reg).flightAware = flight;
}
});
// Добавляем данные из FlightRadar24
flightRadar24Data.forEach(flight => {
const reg = safeStr(flight.reg).trim();
if (!reg) return;
if (!flightsMap.has(reg)) {
flightsMap.set(reg, {
registration: reg,
flightNumber: safeStr(flight.flight),
ident: safeStr(flight.callsign),
identIata: safeStr(flight.flight),
aircraftType: safeStr(flight.type),
flightAware: null,
flightRadar24: flight
});
} else {
flightsMap.get(reg).flightRadar24 = flight;
}
});
// Преобразуем Map в массив
const mergedFlights = Array.from(flightsMap.values());
// ==== ГЕНЕРАЦИЯ HTML ====
const generateFlightCard = (flight) => {
const fa = flight.flightAware;
const fr24 = flight.flightRadar24;
let html = `
<div class="flight-card">
<div class="flight-header">
<h2>Рейс ${flight.flightNumber || flight.ident || 'N/A'}</h2>
<span class="registration">${flight.registration}</span>
</div>
<div class="flight-info">
<div class="info-row">
<span class="label">Тип самолёта:</span>
<span class="value">${flight.aircraftType || '—'}</span>
</div>
<div class="info-row">
<span class="label">Идентификатор:</span>
<span class="value">${flight.ident || '—'} (${flight.identIata || '—'})</span>
</div>
</div>
`;
// Данные из FlightAware
if (fa) {
html += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
</div>
<div class="source-content">
<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 && fa.departure_delay !== undefined ? `
<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 && fa.arrival_delay !== undefined ? `
<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>
`;
} else {
html += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
<span class="source-missing">Данные не получены</span>
</div>
</div>
`;
}
// Данные из FlightRadar24
if (fr24) {
html += `
<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(fr24.orig_iata || '—')} (${safeStr(fr24.orig_icao || '—')})</span>
</div>
<div class="route-item">
<span class="route-label">Куда:</span>
<span class="route-value">${safeStr(fr24.dest_iata || '—')} (${safeStr(fr24.dest_icao || '—')})</span>
</div>
</div>
<div class="timeline">
<div class="timeline-item">
<span class="timeline-label">Взлёт:</span>
<span class="timeline-value">${safeDate(fr24.datetime_takeoff)} ${fr24.runway_takeoff ? `(ВПП ${fr24.runway_takeoff})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Посадка:</span>
<span class="timeline-value">${safeDate(fr24.datetime_landed)} ${fr24.runway_landed ? `(ВПП ${fr24.runway_landed})` : ''}</span>
</div>
</div>
<div class="status-info">
<div class="status-item">
<span class="status-label">Время полёта:</span>
<span class="status-value">${formatDuration(fr24.flight_time)}</span>
</div>
<div class="status-item">
<span class="status-label">Фактическое расстояние:</span>
<span class="status-value">${formatDistance(fr24.actual_distance)}</span>
</div>
<div class="status-item">
<span class="status-label">Кратчайшее расстояние:</span>
<span class="status-value">${formatDistance(fr24.circle_distance)}</span>
</div>
<div class="status-item">
<span class="status-label">Статус полёта:</span>
<span class="status-value">${fr24.flight_ended ? 'Завершён' : 'В процессе'}</span>
</div>
</div>
</div>
</div>
`;
} else {
html += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightradar24">FlightRadar24</span>
<span class="source-missing">Данные не получены</span>
</div>
</div>
`;
}
html += `</div>`;
return html;
};
// ==== ГЕНЕРАЦИЯ ПОЛНОГО HTML ДОКУМЕНТА ====
const generateFullHTML = (flights) => {
const now = new Date();
const reportDate = now.toLocaleString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
let flightsHTML = '';
if (flights.length === 0) {
flightsHTML = '<div class="no-data">Данные о рейсах не найдены</div>';
} else {
flightsHTML = flights.map(flight => generateFlightCard(flight)).join('');
}
return `<!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.6;
color: #333;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
border-bottom: 3px solid #2563eb;
padding-bottom: 20px;
margin-bottom: 30px;
}
.header h1 {
color: #1e40af;
font-size: 28px;
margin-bottom: 10px;
}
.header-meta {
color: #666;
font-size: 14px;
}
.sources-info {
display: flex;
gap: 15px;
margin-top: 10px;
flex-wrap: wrap;
}
.source-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
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: 25px;
overflow: hidden;
background: white;
}
.flight-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.flight-header h2 {
font-size: 24px;
margin: 0;
}
.registration {
background: rgba(255,255,255,0.2);
padding: 6px 12px;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
}
.flight-info {
padding: 15px 20px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.info-row {
display: flex;
margin-bottom: 8px;
}
.info-row .label {
font-weight: 600;
color: #4b5563;
width: 150px;
flex-shrink: 0;
}
.info-row .value {
color: #111827;
}
.source-section {
border-top: 1px solid #e5e7eb;
padding: 20px;
}
.source-section:first-of-type {
border-top: none;
}
.source-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.source-badge {
display: inline-block;
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
color: white;
}
.source-badge.source-flightaware {
background: #3b82f6;
}
.source-badge.source-flightradar24 {
background: #10b981;
}
.source-missing {
color: #ef4444;
font-size: 13px;
font-style: italic;
}
.source-content {
margin-left: 0;
}
.route-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: #f9fafb;
border-radius: 6px;
}
.route-item {
display: flex;
flex-direction: column;
}
.route-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.route-value {
font-size: 16px;
font-weight: 600;
color: #111827;
}
.timeline {
margin-bottom: 20px;
}
.timeline-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #e5e7eb;
}
.timeline-item:last-child {
border-bottom: none;
}
.timeline-label {
font-weight: 500;
color: #4b5563;
width: 180px;
flex-shrink: 0;
}
.timeline-value {
color: #111827;
text-align: right;
}
.status-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
padding: 15px;
background: #f9fafb;
border-radius: 6px;
}
.status-item {
display: flex;
flex-direction: column;
}
.status-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-value {
font-size: 14px;
font-weight: 600;
color: #111827;
}
.delay-negative {
color: #10b981;
}
.delay-positive {
color: #ef4444;
}
.no-data {
text-align: center;
padding: 60px 20px;
color: #6b7280;
font-size: 18px;
}
@media print {
body {
background: white;
padding: 0;
}
.container {
box-shadow: none;
padding: 20px;
}
.flight-card {
page-break-inside: avoid;
margin-bottom: 20px;
}
}
</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">
${flightsHTML}
</div>
</div>
</body>
</html>`;
};
// ==== ФОРМИРОВАНИЕ РЕЗУЛЬТАТА ====
const html = generateFullHTML(mergedFlights);
// ==== ПОДГОТОВКА ДАННЫХ ДЛЯ КОНВЕРТАЦИИ В BASE64 PDF ====
// Эти данные будут использованы в следующей HTTP Request ноде
// для конвертации HTML в PDF и получения base64
// Настройки сервиса конвертации (замените на ваши)
const PDF_SERVICE_URL = 'https://api.htmlpdfapi.com/v1/pdf'; // Или другой сервис
const PDF_API_KEY = 'YOUR_API_KEY'; // ⚠️ ЗАМЕНИТЕ на ваш API ключ
// Подготовка запроса для HTTP Request ноды
const pdfRequestData = {
method: 'POST',
url: PDF_SERVICE_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${PDF_API_KEY}`
},
body: JSON.stringify({
html: html,
options: {
format: 'A4',
printBackground: true,
margin: {
top: '20mm',
right: '15mm',
bottom: '20mm',
left: '15mm'
}
},
base64: true // Запрашиваем base64 напрямую
})
};
return [{
json: {
html: html,
flights: mergedFlights,
flights_count: mergedFlights.length,
sources: {
flightaware: {
available: flightAwareData.length > 0,
count: flightAwareData.length
},
flightradar24: {
available: flightRadar24Data.length > 0,
count: flightRadar24Data.length
}
},
generated_at: new Date().toISOString(),
// Данные для конвертации в PDF (используйте в следующей HTTP Request ноде)
pdf_request: pdfRequestData,
pdf_request_method: pdfRequestData.method,
pdf_request_url: pdfRequestData.url,
pdf_request_headers: pdfRequestData.headers,
pdf_request_body: pdfRequestData.body
}
}];