Files
aiform_prod/docs/N8N_FLIGHTS_SIMPLE_BINARY.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

371 lines
15 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: Отчёт о рейсах (HTML → Binary + Base64 PDF)
// ============================================================================
// Упрощённая версия с возвратом binary HTML и подготовкой для PDF конвертации
// ============================================================================
const inputItems = $input.all();
// ================== FALLBACK ==================
if (!inputItems || inputItems.length === 0) {
const html = '<!DOCTYPE html><html><body><h1>Ошибка: данные не получены</h1></body></html>';
return [{
binary: {
data: Buffer.from(html, 'utf8'),
mimeType: 'text/html',
fileName: 'flights-report.html'
},
json: {
html: html,
flights_count: 0,
error: 'Нет входных данных'
}
}];
}
// ================== ИЗВЛЕЧЕНИЕ ДАННЫХ ==================
let flightAwareData = [];
let flightRadar24Data = [];
try {
const fa = inputItems[0]?.json?.body?.flights;
if (Array.isArray(fa)) flightAwareData = fa;
} catch (e) {
console.log('⚠️ Ошибка извлечения FlightAware:', e.message);
}
try {
const fr = inputItems[1]?.json?.body?.data;
if (Array.isArray(fr)) flightRadar24Data = fr;
} 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 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 ПО REGISTRATION ==================
const flightsMap = new Map();
flightAwareData.forEach(f => {
const reg = safeStr(f.registration).trim();
if (!reg) return;
if (!flightsMap.has(reg)) {
flightsMap.set(reg, {
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
});
} else {
flightsMap.get(reg).fa = f;
}
});
flightRadar24Data.forEach(f => {
const reg = safeStr(f.reg).trim();
if (!reg) return;
if (!flightsMap.has(reg)) {
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
});
} else {
flightsMap.get(reg).fr = f;
}
});
const flights = Array.from(flightsMap.values());
// ================== HTML GENERATION ==================
const generateFlightCard = f => {
const fa = f.fa;
const fr = f.fr;
let card = `
<div class="flight-card">
<div class="flight-header">
<h2>Рейс ${f.flightNumber || f.ident || '—'}</h2>
<span class="registration">${f.registration}</span>
</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">
<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.actual_out)}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Прилёт:</span>
<span class="timeline-value">${safeDate(fa.actual_in)}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Статус:</span>
<span class="timeline-value">${safeStr(fa.status || '—')}</span>
</div>
</div>
</div>
</div>`;
} else {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
<span class="source-missing">Данные не получены</span>
</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="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>
</div>
</div>`;
} else {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightradar24">FlightRadar24</span>
<span class="source-missing">Данные не получены</span>
</div>
</div>`;
}
card += `</div>`;
return card;
};
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, 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; }
.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">
${flights.length ? flights.map(generateFlightCard).join('') : '<div class="no-data">Данные о рейсах не найдены</div>'}
</div>
</div>
</body>
</html>`;
// ================== ПОДГОТОВКА ДАННЫХ ДЛЯ PDF КОНВЕРТАЦИИ ==================
// Настройки сервиса (замените на ваши)
const PDF_SERVICE_URL = 'https://api.htmlpdfapi.com/v1/pdf';
const PDF_API_KEY = 'YOUR_API_KEY'; // ⚠️ ЗАМЕНИТЕ на ваш API ключ
// ================== RETURN ==================
return [{
// Binary HTML файл (для использования в Convert to File ноде или сохранения)
binary: {
data: Buffer.from(html, 'utf8'),
mimeType: 'text/html',
fileName: `flights-report-${now.toISOString().split('T')[0]}.html`
},
// JSON данные
json: {
// HTML строка (для конвертации в PDF через HTTP Request)
html: html,
// Метаданные
flights_count: flights.length,
generated_at: now.toISOString(),
sources: {
flightaware: { available: flightAwareData.length > 0, count: flightAwareData.length },
flightradar24: { available: flightRadar24Data.length > 0, count: flightRadar24Data.length }
},
// Данные для конвертации в base64 PDF (используйте в следующей HTTP Request ноде)
pdf_request: {
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
})
},
// Удобные поля для HTTP Request ноды
pdf_request_method: 'POST',
pdf_request_url: PDF_SERVICE_URL,
pdf_request_headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${PDF_API_KEY}`
},
pdf_request_body: JSON.stringify({
html: html,
options: {
format: 'A4',
printBackground: true,
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }
},
base64: true
})
}
}];
// ============================================================================
// ИСПОЛЬЗОВАНИЕ:
// ============================================================================
// 1. Binary HTML можно использовать в ноде "Convert to File" или сохранить
// 2. JSON.html можно использовать для конвертации в PDF через HTTP Request
// 3. JSON.pdf_request_* поля готовы для использования в HTTP Request ноде
// 4. После HTTP Request используйте N8N_EXTRACT_BASE64_FROM_RESPONSE.js
// для извлечения base64 PDF из ответа
// ============================================================================