{ "nodes": [ { "parameters": { "jsCode": "// ============================================================================\n// n8n Code Node: Обработка данных о рейсах → Base64 HTML\n// ============================================================================\n// Вход: [{ data: [{ body: { flights: [...] }}, { body: { data: [...] }}] }]\n// Выход: base64 HTML\n// ============================================================================\n\nconst inputItems = $input.all();\n\n// ================== FALLBACK ==================\nif (!inputItems || inputItems.length === 0) {\n const html = '

Ошибка: данные не получены

';\n const htmlBase64 = Buffer.from(html, 'utf8').toString('base64');\n \n return [{\n json: {\n html_base64: htmlBase64,\n html: html,\n flights_count: 0,\n error: 'Нет входных данных'\n }\n }];\n}\n\n// ================== ИЗВЛЕЧЕНИЕ ДАННЫХ ==================\n// Структура: [{ data: [{ body: { flights: [...] }}, { body: { data: [...] }}] }]\nlet flightAwareData = [];\nlet flightRadar24Data = [];\n\ntry {\n const firstItem = inputItems[0];\n if (firstItem && firstItem.json && firstItem.json.data && Array.isArray(firstItem.json.data)) {\n // Первый элемент массива data - FlightAware\n if (firstItem.json.data[0] && firstItem.json.data[0].body && firstItem.json.data[0].body.flights) {\n flightAwareData = Array.isArray(firstItem.json.data[0].body.flights) \n ? firstItem.json.data[0].body.flights \n : [];\n }\n \n // Второй элемент массива data - FlightRadar24\n if (firstItem.json.data[1] && firstItem.json.data[1].body && firstItem.json.data[1].body.data) {\n flightRadar24Data = Array.isArray(firstItem.json.data[1].body.data) \n ? firstItem.json.data[1].body.data \n : [];\n }\n }\n} catch (e) {\n console.log('⚠️ Ошибка извлечения данных:', e.message);\n}\n\n// ================== УТИЛИТЫ ==================\nconst safeStr = v => (v == null ? '' : String(v));\nconst safeDate = v => {\n if (!v) return '—';\n try {\n const d = new Date(v);\n return isNaN(d.getTime()) ? '—' : d.toLocaleString('ru-RU', {\n timeZone: 'UTC',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit'\n });\n } catch {\n return '—';\n }\n};\n\nconst formatDuration = s => !s ? '—' : `${Math.floor(s / 3600)}ч ${Math.floor((s % 3600) / 60)}м`;\nconst formatDistance = km => !km ? '—' : `${Number(km).toFixed(2)} км`;\n\n// ================== MERGE ПО REGISTRATION ==================\nconst flightsMap = new Map();\n\n// Добавляем данные из FlightAware\nflightAwareData.forEach(f => {\n const reg = safeStr(f.registration).trim();\n if (!reg) return;\n if (!flightsMap.has(reg)) {\n flightsMap.set(reg, {\n registration: reg,\n flightNumber: safeStr(f.flight_number),\n ident: safeStr(f.ident),\n identIata: safeStr(f.ident_iata),\n aircraftType: safeStr(f.aircraft_type),\n fa: f,\n fr: null\n });\n } else {\n flightsMap.get(reg).fa = f;\n }\n});\n\n// Добавляем данные из FlightRadar24\nflightRadar24Data.forEach(f => {\n const reg = safeStr(f.reg).trim();\n if (!reg) return;\n if (!flightsMap.has(reg)) {\n flightsMap.set(reg, {\n registration: reg,\n flightNumber: safeStr(f.flight),\n ident: safeStr(f.callsign),\n identIata: safeStr(f.flight),\n aircraftType: safeStr(f.type),\n fa: null,\n fr: f\n });\n } else {\n flightsMap.get(reg).fr = f;\n }\n});\n\nconst flights = Array.from(flightsMap.values());\n\n// ================== HTML GENERATION ==================\nconst generateFlightCard = f => {\n const fa = f.fa;\n const fr = f.fr;\n \n let card = `\n
\n
\n

Рейс ${f.flightNumber || f.ident || '—'}

\n ${f.registration}\n
\n
\n
\n Тип самолёта:\n ${f.aircraftType || '—'}\n
\n
\n Идентификатор:\n ${f.ident || '—'} (${f.identIata || '—'})\n
\n
`;\n\n // Данные из FlightAware\n if (fa) {\n card += `\n
\n
\n FlightAware\n
\n
\n
\n
\n Откуда:\n ${safeStr(fa.origin?.name || fa.origin?.code_iata || '—')} (${safeStr(fa.origin?.code_iata || '—')})\n
\n
\n Куда:\n ${safeStr(fa.destination?.name || fa.destination?.code_iata || '—')} (${safeStr(fa.destination?.code_iata || '—')})\n
\n
\n
\n
\n Плановый вылет:\n ${safeDate(fa.scheduled_out)}\n
\n
\n Фактический вылет:\n ${safeDate(fa.actual_out)}\n
\n
\n Взлёт:\n ${safeDate(fa.actual_off)} ${fa.actual_runway_off ? `(ВПП ${fa.actual_runway_off})` : ''}\n
\n
\n Посадка:\n ${safeDate(fa.actual_on)} ${fa.actual_runway_on ? `(ВПП ${fa.actual_runway_on})` : ''}\n
\n
\n Фактический прилёт:\n ${safeDate(fa.actual_in)}\n
\n
\n
\n
\n Статус:\n ${safeStr(fa.status || '—')}\n
\n ${fa.departure_delay !== null && fa.departure_delay !== undefined ? `\n
\n Задержка вылета:\n ${fa.departure_delay > 0 ? '+' : ''}${Math.floor(fa.departure_delay / 60)} мин\n
\n ` : ''}\n ${fa.arrival_delay !== null && fa.arrival_delay !== undefined ? `\n
\n Задержка прилёта:\n ${fa.arrival_delay > 0 ? '+' : ''}${Math.floor(fa.arrival_delay / 60)} мин\n
\n ` : ''}\n ${fa.gate_origin ? `\n
\n Гейт вылета:\n ${fa.gate_origin}\n
\n ` : ''}\n ${fa.gate_destination ? `\n
\n Гейт прилёта:\n ${fa.gate_destination}\n
\n ` : ''}\n ${fa.baggage_claim ? `\n
\n Выдача багажа:\n ${fa.baggage_claim}\n
\n ` : ''}\n
\n
\n
`;\n } else {\n card += `\n
\n
\n FlightAware\n Данные не получены\n
\n
`;\n }\n\n // Данные из FlightRadar24\n if (fr) {\n card += `\n
\n
\n FlightRadar24\n
\n
\n
\n
\n Откуда:\n ${safeStr(fr.orig_iata || '—')} (${safeStr(fr.orig_icao || '—')})\n
\n
\n Куда:\n ${safeStr(fr.dest_iata || '—')} (${safeStr(fr.dest_icao || '—')})\n
\n
\n
\n
\n Взлёт:\n ${safeDate(fr.datetime_takeoff)} ${fr.runway_takeoff ? `(ВПП ${fr.runway_takeoff})` : ''}\n
\n
\n Посадка:\n ${safeDate(fr.datetime_landed)} ${fr.runway_landed ? `(ВПП ${fr.runway_landed})` : ''}\n
\n
\n
\n
\n Время полёта:\n ${formatDuration(fr.flight_time)}\n
\n
\n Фактическое расстояние:\n ${formatDistance(fr.actual_distance)}\n
\n
\n Кратчайшее расстояние:\n ${formatDistance(fr.circle_distance)}\n
\n
\n Статус полёта:\n ${fr.flight_ended ? 'Завершён' : 'В процессе'}\n
\n
\n
\n
`;\n } else {\n card += `\n
\n
\n FlightRadar24\n Данные не получены\n
\n
`;\n }\n\n card += `
`;\n return card;\n};\n\n// Генерация полного HTML\nconst now = new Date();\nconst reportDate = now.toLocaleString('ru-RU', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n});\n\nconst html = `\n\n\n \n \n Отчёт о рейсах\n \n\n\n
\n
\n

Отчёт о рейсах

\n
\n
Дата формирования: ${reportDate}
\n
\n 0 ? 'available' : 'unavailable'}\">\n FlightAware: ${flightAwareData.length > 0 ? '✓ Данные получены' : '✗ Данные отсутствуют'}\n \n 0 ? 'available' : 'unavailable'}\">\n FlightRadar24: ${flightRadar24Data.length > 0 ? '✓ Данные получены' : '✗ Данные отсутствуют'}\n \n
\n
\n
\n
\n ${flights.length ? flights.map(generateFlightCard).join('') : '
Данные о рейсах не найдены
'}\n
\n
\n\n`;\n\n// ================== HTML → BASE64 ==================\nconst htmlBase64 = Buffer.from(html, 'utf8').toString('base64');\n\n// ================== RETURN ==================\nreturn [{\n json: {\n html_base64: htmlBase64,\n html: html,\n flights_count: flights.length,\n sources: {\n flightaware: { available: flightAwareData.length > 0, count: flightAwareData.length },\n flightradar24: { available: flightRadar24Data.length > 0, count: flightRadar24Data.length }\n },\n generated_at: now.toISOString()\n }\n}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 384, 128 ], "id": "a44a16f4-0ae7-4947-8f2b-3d70a6e6cfe0", "name": "причесываем данные" }, { "parameters": { "method": "POST", "url": "http://147.45.146.17:3000/pdf?token=9ahhnpjkchxtcho9", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"url\": \"data:text/html;base64, {{ $json.html_base64 }}\",\n \"options\": {\n \"format\": \"A4\",\n \"printBackground\": true,\n \"margin\": {\n \"top\": \"20mm\",\n \"right\": \"15mm\",\n \"bottom\": \"20mm\",\n \"left\": \"20mm\"\n }\n }\n}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ 624, 128 ], "id": "fbbf533a-b3b8-4aba-8a38-106545a5a9e2", "name": "HTTP Request: Browserless PDF" } ], "connections": { "причесываем данные": { "main": [ [ { "node": "HTTP Request: Browserless PDF", "type": "main", "index": 0 } ] ] } }, "pinData": {}, "meta": { "templateCredsSetupCompleted": true, "instanceId": "ad5e78bf27056f72474a633d21d938f3223861ac866f2ebe4e46b867e404f489" } }