525 lines
20 KiB
JavaScript
525 lines
20 KiB
JavaScript
require('dotenv').config();
|
||
const express = require('express');
|
||
const axios = require('axios');
|
||
const cors = require('cors');
|
||
const path = require('path');
|
||
|
||
const app = express();
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
app.use(express.static(path.join(__dirname, 'public')));
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
|
||
// ─────────────────────────────────────────────
|
||
// IATA-LOOKUP-TABELLE
|
||
// ─────────────────────────────────────────────
|
||
|
||
const IATA_LOOKUP = {
|
||
// Deutschland
|
||
'stuttgart': 'STR', 'frankfurt': 'FRA', 'münchen': 'MUC', 'munich': 'MUC',
|
||
'berlin': 'BER', 'hamburg': 'HAM', 'düsseldorf': 'DUS', 'dusseldorf': 'DUS',
|
||
'köln': 'CGN', 'koeln': 'CGN', 'cologne': 'CGN', 'hannover': 'HAJ',
|
||
'nürnberg': 'NUE', 'nuremberg': 'NUE', 'leipzig': 'LEJ', 'bremen': 'BRE',
|
||
'dresden': 'DRS', 'karlsruhe': 'FKB', 'friedrichshafen': 'FDH',
|
||
'bad saulgau': 'FDH', 'ravensburg': 'FDH', 'biberach': 'FDH', 'sigmaringen': 'FDH',
|
||
'saarbrücken': 'SCN', 'erfurt': 'ERF', 'paderborn': 'PAD',
|
||
'münster': 'FMO', 'rostock': 'RLG', 'dortmund': 'DTM', 'weeze': 'NRN',
|
||
// Österreich
|
||
'wien': 'VIE', 'vienna': 'VIE', 'graz': 'GRZ', 'linz': 'LNZ',
|
||
'salzburg': 'SZG', 'innsbruck': 'INN', 'klagenfurt': 'KLU',
|
||
// Schweiz
|
||
'zürich': 'ZRH', 'zurich': 'ZRH', 'genf': 'GVA', 'geneva': 'GVA',
|
||
'basel': 'BSL', 'bern': 'BRN',
|
||
// Polen
|
||
'warschau': 'WAW', 'warsaw': 'WAW', 'warszawa': 'WAW',
|
||
'krakau': 'KRK', 'krakow': 'KRK', 'kraków': 'KRK',
|
||
'breslau': 'WRO', 'wroclaw': 'WRO', 'wrocław': 'WRO',
|
||
'danzig': 'GDN', 'gdansk': 'GDN', 'gdańsk': 'GDN',
|
||
'posen': 'POZ', 'poznan': 'POZ', 'poznań': 'POZ',
|
||
'katowice': 'KTW', 'lodz': 'LCJ', 'łódź': 'LCJ',
|
||
'rzeszow': 'RZE', 'rzeszów': 'RZE', 'bydgoszcz': 'BZG',
|
||
'szczecin': 'SZZ', 'lublin': 'LUZ',
|
||
// Tschechien
|
||
'prag': 'PRG', 'prague': 'PRG', 'praha': 'PRG',
|
||
'brünn': 'BRQ', 'brno': 'BRQ', 'ostrava': 'OSR',
|
||
// Ungarn
|
||
'budapest': 'BUD', 'debrecen': 'DEB',
|
||
// Slowakei
|
||
'bratislava': 'BTS', 'košice': 'KSC', 'kosice': 'KSC',
|
||
// Rumänien
|
||
'bukarest': 'OTP', 'bucharest': 'OTP', 'bucurești': 'OTP',
|
||
'cluj': 'CLJ', 'timisoara': 'TSR', 'timișoara': 'TSR',
|
||
// Niederlande
|
||
'amsterdam': 'AMS', 'eindhoven': 'EIN', 'rotterdam': 'RTM',
|
||
// Belgien
|
||
'brüssel': 'BRU', 'brussels': 'BRU', 'bruxelles': 'BRU',
|
||
'lüttich': 'LGG', 'liege': 'LGG', 'liège': 'LGG',
|
||
// Frankreich
|
||
'paris': 'CDG', 'lyon': 'LYS', 'marseille': 'MRS',
|
||
'nizza': 'NCE', 'nice': 'NCE', 'toulouse': 'TLS',
|
||
'bordeaux': 'BOD', 'strasbourg': 'SXB', 'straßburg': 'SXB',
|
||
'lille': 'LIL', 'nantes': 'NTE', 'montpellier': 'MPL',
|
||
// Spanien
|
||
'madrid': 'MAD', 'barcelona': 'BCN', 'sevilla': 'SVQ', 'seville': 'SVQ',
|
||
'valencia': 'VLC', 'bilbao': 'BIO', 'málaga': 'AGP', 'malaga': 'AGP',
|
||
'palma': 'PMI', 'teneriffa': 'TFS', 'tenerife': 'TFS',
|
||
'alicante': 'ALC', 'zaragoza': 'ZAZ',
|
||
// Italien
|
||
'rom': 'FCO', 'rome': 'FCO', 'roma': 'FCO',
|
||
'mailand': 'MXP', 'milan': 'MXP', 'milano': 'MXP',
|
||
'venedig': 'VCE', 'venice': 'VCE', 'venezia': 'VCE',
|
||
'florenz': 'FLR', 'florence': 'FLR', 'firenze': 'FLR',
|
||
'neapel': 'NAP', 'naples': 'NAP', 'napoli': 'NAP',
|
||
'bologna': 'BLQ', 'turin': 'TRN', 'torino': 'TRN',
|
||
'catania': 'CTA', 'palermo': 'PMO', 'bari': 'BRI', 'verona': 'VRN',
|
||
// UK
|
||
'london': 'LHR', 'manchester': 'MAN', 'birmingham': 'BHX',
|
||
'edinburgh': 'EDI', 'glasgow': 'GLA', 'bristol': 'BRS',
|
||
'newcastle': 'NCL', 'leeds': 'LBA', 'liverpool': 'LPL',
|
||
// Skandinavien
|
||
'stockholm': 'ARN', 'oslo': 'OSL', 'kopenhagen': 'CPH', 'copenhagen': 'CPH',
|
||
'helsinki': 'HEL', 'göteborg': 'GOT', 'gothenburg': 'GOT',
|
||
'bergen': 'BGO', 'stavanger': 'SVG', 'tampere': 'TMP',
|
||
// Osteuropa
|
||
'kiev': 'KBP', 'kiew': 'KBP', 'kyiv': 'KBP',
|
||
'riga': 'RIX', 'tallinn': 'TLL', 'vilnius': 'VNO', 'minsk': 'MSQ',
|
||
'sofia': 'SOF', 'zagreb': 'ZAG', 'belgrad': 'BEG', 'belgrade': 'BEG',
|
||
'sarajevo': 'SJJ', 'laibach': 'LJU', 'ljubljana': 'LJU',
|
||
'skopje': 'SKP', 'tirana': 'TIA', 'chisinau': 'KIV',
|
||
'thessaloniki': 'SKG', 'athen': 'ATH', 'athens': 'ATH', 'athina': 'ATH',
|
||
// Türkei
|
||
'istanbul': 'IST', 'ankara': 'ESB', 'izmir': 'ADB', 'antalya': 'AYT',
|
||
// Russland
|
||
'moskau': 'SVO', 'moscow': 'SVO', 'moskva': 'SVO',
|
||
'st. petersburg': 'LED', 'saint petersburg': 'LED', 'sankt petersburg': 'LED',
|
||
// Naher Osten
|
||
'dubai': 'DXB', 'abu dhabi': 'AUH', 'doha': 'DOH',
|
||
'riad': 'RUH', 'riyadh': 'RUH', 'kuwait': 'KWI',
|
||
'beirut': 'BEY', 'tel aviv': 'TLV', 'amman': 'AMM', 'muscat': 'MCT',
|
||
// Asien
|
||
'peking': 'PEK', 'beijing': 'PEK', 'shanghai': 'PVG',
|
||
'tokio': 'NRT', 'tokyo': 'NRT', 'osaka': 'KIX',
|
||
'seoul': 'ICN', 'singapur': 'SIN', 'singapore': 'SIN',
|
||
'hongkong': 'HKG', 'hong kong': 'HKG',
|
||
'bangkok': 'BKK', 'mumbai': 'BOM', 'delhi': 'DEL',
|
||
'jakarta': 'CGK', 'manila': 'MNL', 'kuala lumpur': 'KUL',
|
||
'taipei': 'TPE', 'colombo': 'CMB', 'karachi': 'KHI',
|
||
// Amerika
|
||
'new york': 'JFK', 'los angeles': 'LAX', 'chicago': 'ORD',
|
||
'miami': 'MIA', 'san francisco': 'SFO', 'boston': 'BOS',
|
||
'washington': 'IAD', 'toronto': 'YYZ', 'montreal': 'YUL',
|
||
'vancouver': 'YVR', 'mexico city': 'MEX', 'mexiko stadt': 'MEX',
|
||
'sao paulo': 'GRU', 'rio de janeiro': 'GIG',
|
||
'buenos aires': 'EZE', 'bogota': 'BOG', 'lima': 'LIM', 'santiago': 'SCL',
|
||
// Afrika / Ozeanien
|
||
'kairo': 'CAI', 'cairo': 'CAI',
|
||
'johannesburg': 'JNB', 'kapstadt': 'CPT', 'cape town': 'CPT',
|
||
'nairobi': 'NBO', 'casablanca': 'CMN', 'addis abeba': 'ADD',
|
||
'sydney': 'SYD', 'melbourne': 'MEL', 'auckland': 'AKL',
|
||
'brisbane': 'BNE', 'perth': 'PER',
|
||
};
|
||
|
||
function getIataCode(cityInput) {
|
||
if (!cityInput) return null;
|
||
const normalized = cityInput.toLowerCase().trim().split(',')[0].trim();
|
||
return IATA_LOOKUP[normalized] || null;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// HILFSFUNKTIONEN
|
||
// ─────────────────────────────────────────────
|
||
|
||
// Geocoding: Adresse → lat/lng + Stadtname (Google Maps oder Nominatim)
|
||
async function geocodeAddress(address) {
|
||
if (process.env.GOOGLE_MAPS_API_KEY && !process.env.GOOGLE_MAPS_API_KEY.startsWith('dein_')) {
|
||
try {
|
||
const res = await axios.get('https://maps.googleapis.com/maps/api/geocode/json', {
|
||
params: { address, key: process.env.GOOGLE_MAPS_API_KEY },
|
||
});
|
||
const result = res.data.results?.[0];
|
||
if (result) {
|
||
const loc = result.geometry.location;
|
||
const components = result.address_components || [];
|
||
const cityComp =
|
||
components.find((c) => c.types.includes('locality')) ||
|
||
components.find((c) => c.types.includes('administrative_area_level_1'));
|
||
return {
|
||
lat: loc.lat,
|
||
lng: loc.lng,
|
||
city: cityComp?.long_name || address.split(',')[0].trim(),
|
||
};
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
// Nominatim Fallback
|
||
const queries = [
|
||
address,
|
||
address
|
||
.replace(/Polen/gi, 'Poland').replace(/Warschau/gi, 'Warsaw')
|
||
.replace(/Deutschland/gi, 'Germany').replace(/Österreich/gi, 'Austria')
|
||
.replace(/Schweiz/gi, 'Switzerland').replace(/Frankreich/gi, 'France')
|
||
.replace(/München/gi, 'Munich').replace(/Wien/gi, 'Vienna')
|
||
.replace(/Prag/gi, 'Prague').replace(/Mailand/gi, 'Milan')
|
||
.replace(/Brüssel/gi, 'Brussels').replace(/Lissabon/gi, 'Lisbon')
|
||
.replace(/Köln/gi, 'Cologne').replace(/Athen/gi, 'Athens'),
|
||
address.split(',').slice(-2).join(',').trim(),
|
||
address.split(',')[0].trim(),
|
||
];
|
||
|
||
for (const q of queries) {
|
||
if (!q) continue;
|
||
try {
|
||
const res = await axios.get('https://nominatim.openstreetmap.org/search', {
|
||
params: { q, format: 'json', limit: 1, addressdetails: 1 },
|
||
headers: { 'User-Agent': 'TravelDesk/1.0' },
|
||
timeout: 5000,
|
||
});
|
||
if (res.data?.length > 0) {
|
||
const item = res.data[0];
|
||
const city =
|
||
item.address?.city ||
|
||
item.address?.municipality ||
|
||
item.address?.town ||
|
||
item.address?.village ||
|
||
item.address?.state ||
|
||
address.split(',')[0].trim();
|
||
return { lat: parseFloat(item.lat), lng: parseFloat(item.lon), city };
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
// Letzter Fallback: Stadtname direkt aus Input
|
||
const city = address.split(',')[0].trim();
|
||
throw new Error(`Adresse nicht gefunden: ${city}`);
|
||
}
|
||
|
||
// Distanz (Google Routes API oder Haversine-Fallback)
|
||
async function getDistanceMeters(originLat, originLng, destLat, destLng) {
|
||
if (process.env.GOOGLE_MAPS_API_KEY && !process.env.GOOGLE_MAPS_API_KEY.startsWith('dein_')) {
|
||
try {
|
||
const res = await axios.post(
|
||
'https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix',
|
||
{
|
||
origins: [{ waypoint: { location: { latLng: { latitude: originLat, longitude: originLng } } } }],
|
||
destinations: [{ waypoint: { location: { latLng: { latitude: destLat, longitude: destLng } } } }],
|
||
travelMode: 'WALK',
|
||
},
|
||
{
|
||
headers: {
|
||
'X-Goog-Api-Key': process.env.GOOGLE_MAPS_API_KEY,
|
||
'X-Goog-FieldMask': 'originIndex,destinationIndex,duration,distanceMeters',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
}
|
||
);
|
||
const entry = res.data?.[0];
|
||
if (entry?.distanceMeters) return entry.distanceMeters;
|
||
} catch (_) {}
|
||
}
|
||
// Haversine-Fallback (Luftlinie)
|
||
const R = 6371000;
|
||
const φ1 = (originLat * Math.PI) / 180;
|
||
const φ2 = (destLat * Math.PI) / 180;
|
||
const Δφ = ((destLat - originLat) * Math.PI) / 180;
|
||
const Δλ = ((destLng - originLng) * Math.PI) / 180;
|
||
const a =
|
||
Math.sin(Δφ / 2) ** 2 +
|
||
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
|
||
return Math.round(R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
|
||
}
|
||
|
||
// IATA via GPT-Fallback
|
||
async function getIataViaGpt(cityName) {
|
||
const apiKey = process.env.OPENAI_API_KEY;
|
||
if (!apiKey || apiKey.startsWith('dein_')) return null;
|
||
try {
|
||
const res = await axios.post(
|
||
'https://api.openai.com/v1/chat/completions',
|
||
{
|
||
model: 'gpt-4o-mini',
|
||
max_tokens: 5,
|
||
temperature: 0,
|
||
messages: [
|
||
{
|
||
role: 'system',
|
||
content:
|
||
'Du bist ein Flughafen-Experte. Antworte IMMER nur mit einem einzigen 3-buchstabigen IATA-Code für den nächstgelegenen Flughafen (auch Regionalflughafen). Nichts anderes.',
|
||
},
|
||
{ role: 'user', content: `IATA-Code des nächstgelegenen Flughafens für: ${cityName}` },
|
||
],
|
||
},
|
||
{ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }
|
||
);
|
||
const code = res.data.choices?.[0]?.message?.content?.trim().toUpperCase();
|
||
if (/^[A-Z]{3}$/.test(code)) return code;
|
||
} catch (_) {}
|
||
return null;
|
||
}
|
||
|
||
|
||
// ─────────────────────────────────────────────
|
||
// DEEP-LINK-GENERATOREN
|
||
// ─────────────────────────────────────────────
|
||
|
||
function generateFlightLink({ originIATA, destIATA, departDate, returnDate, adults, directOnly }) {
|
||
if (!originIATA || !destIATA) {
|
||
return `https://flights.booking.com/`;
|
||
}
|
||
const type = returnDate ? 'ROUNDTRIP' : 'ONEWAY';
|
||
let url =
|
||
`https://flights.booking.com/flights/${originIATA}-${destIATA}/` +
|
||
`?type=${type}` +
|
||
`&from=${originIATA}` +
|
||
`&to=${destIATA}` +
|
||
`&cabinClass=ECONOMY` +
|
||
`&sort=CHEAPEST` +
|
||
`&depart=${departDate}` +
|
||
`&adults=${adults}` +
|
||
`&locale=de-de` +
|
||
`&salesCurrency=EUR` +
|
||
`&customerCurrency=EUR`;
|
||
if (returnDate) url += `&return=${returnDate}`;
|
||
if (directOnly) url += `&stops=0`;
|
||
return url;
|
||
}
|
||
|
||
function generateHotelLink({ cityName, checkin, checkout, adults, refundableOnly }) {
|
||
const [cy, cm, cd] = checkin.split('-');
|
||
const [oy, om, od] = checkout.split('-');
|
||
let url =
|
||
`https://www.booking.com/searchresults.de.html` +
|
||
`?ss=${encodeURIComponent(cityName)}` +
|
||
`&checkin_year=${cy}` +
|
||
`&checkin_month=${parseInt(cm)}` +
|
||
`&checkin_monthday=${parseInt(cd)}` +
|
||
`&checkout_year=${oy}` +
|
||
`&checkout_month=${parseInt(om)}` +
|
||
`&checkout_monthday=${parseInt(od)}` +
|
||
`&group_adults=${adults}` +
|
||
`&group_children=0` +
|
||
`&no_rooms=1` +
|
||
`&order=price`;
|
||
if (refundableOnly) url += `&nflt=fc%3D1`;
|
||
return url;
|
||
}
|
||
|
||
function generateCarLink({ cityName, iataCode, pickupDate, dropoffDate, lat, lng }) {
|
||
const pu = new Date(pickupDate);
|
||
const doDate = new Date(dropoffDate);
|
||
const params = new URLSearchParams({
|
||
preflang: 'de',
|
||
prefcurrency: 'EUR',
|
||
driversAge: '30',
|
||
locationName: iataCode ? `Flughafen ${cityName}` : cityName,
|
||
locationIata: iataCode || '',
|
||
dropLocationName: iataCode ? `Flughafen ${cityName}` : cityName,
|
||
dropLocationIata: iataCode || '',
|
||
puDay: pu.getUTCDate(),
|
||
puMonth: pu.getUTCMonth() + 1,
|
||
puYear: pu.getUTCFullYear(),
|
||
puHour: '10',
|
||
puMinute: '0',
|
||
doDay: doDate.getUTCDate(),
|
||
doMonth: doDate.getUTCMonth() + 1,
|
||
doYear: doDate.getUTCFullYear(),
|
||
doHour: '10',
|
||
doMinute: '0',
|
||
ftsType: 'A',
|
||
dropFtsType: 'A',
|
||
});
|
||
if (lat && lng) {
|
||
params.set('coordinates', `${lat},${lng}`);
|
||
params.set('dropCoordinates', `${lat},${lng}`);
|
||
}
|
||
return `https://cars.booking.com/search-results?${params.toString()}`;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// HAUPT-ENDPOINT: POST /api/search
|
||
// ─────────────────────────────────────────────
|
||
|
||
app.post('/api/search', async (req, res) => {
|
||
const {
|
||
origin,
|
||
destStreet = '',
|
||
destHouseNumber = '',
|
||
destPostalCode = '',
|
||
destCity = '',
|
||
destCountry = '',
|
||
checkin,
|
||
checkout,
|
||
adults = 1,
|
||
directFlightsOnly = false,
|
||
refundableOnly = true,
|
||
hotelAddress = '',
|
||
specialRequirements = '',
|
||
} = req.body;
|
||
|
||
// Adresse aus Einzelfeldern zusammensetzen
|
||
const streetPart = [destStreet.trim(), destHouseNumber.trim()].filter(Boolean).join(' ');
|
||
const destination = [streetPart, destPostalCode.trim(), destCity.trim(), destCountry.trim()]
|
||
.filter(Boolean).join(', ');
|
||
|
||
const nights = Math.round((new Date(checkout) - new Date(checkin)) / 86400000);
|
||
|
||
// ── Schritt 1: Fabrikadresse geocoden ─────
|
||
let destCoords = null;
|
||
// Stadt direkt aus dem Formularfeld – kein Parsing nötig
|
||
let destinationCity = destCity.trim() || destination.split(',')[0].trim();
|
||
|
||
try {
|
||
destCoords = await geocodeAddress(destination);
|
||
// Geocoded city nur als Fallback wenn destCity leer
|
||
if (!destCity.trim()) destinationCity = destCoords.city;
|
||
} catch (_) {}
|
||
|
||
// ── Schritt 2: IATA-Codes ermitteln ───────
|
||
const originCity = origin.split(',')[0].trim();
|
||
|
||
let originIATA = getIataCode(originCity);
|
||
let destIATA = getIataCode(destinationCity) || getIataCode(destination.split(',')[0].trim());
|
||
let iataFallbackUsed = false;
|
||
let iataError = null;
|
||
|
||
if (!originIATA) {
|
||
originIATA = await getIataViaGpt(originCity);
|
||
if (originIATA) iataFallbackUsed = true;
|
||
}
|
||
if (!destIATA) {
|
||
destIATA = await getIataViaGpt(destinationCity);
|
||
if (destIATA) iataFallbackUsed = true;
|
||
}
|
||
|
||
if (!originIATA || !destIATA) {
|
||
iataError = {
|
||
origin: !originIATA ? originCity : null,
|
||
dest: !destIATA ? destinationCity : null,
|
||
};
|
||
}
|
||
|
||
// ── Schritt 3: Mietwagen-Logik ─────────────
|
||
let carNeeded = null;
|
||
let carMessage = 'Hotel noch nicht gebucht? Buche zuerst ein Hotel und trage die Adresse ein für automatischen Check.';
|
||
let distanceMeters = null;
|
||
let hotelAddressUsed = '';
|
||
|
||
if (hotelAddress && hotelAddress.trim()) {
|
||
hotelAddressUsed = hotelAddress.trim();
|
||
if (destCoords) {
|
||
try {
|
||
const hotelCoords = await geocodeAddress(hotelAddress);
|
||
distanceMeters = await getDistanceMeters(
|
||
hotelCoords.lat, hotelCoords.lng,
|
||
destCoords.lat, destCoords.lng
|
||
);
|
||
if (distanceMeters < 1500) {
|
||
carNeeded = false;
|
||
carMessage = `✅ Kein Mietwagen nötig – dein Hotel ist nur ${distanceMeters}m von der Fabrik entfernt (fußläufig).`;
|
||
} else {
|
||
carNeeded = true;
|
||
carMessage = `🚗 Mietwagen empfohlen – dein Hotel ist ${(distanceMeters / 1000).toFixed(1)} km von der Fabrik entfernt.`;
|
||
}
|
||
} catch (_) {
|
||
carNeeded = null;
|
||
carMessage = 'Hotel-Adresse konnte nicht gefunden werden. Bitte manuell prüfen.';
|
||
}
|
||
} else {
|
||
carMessage = 'Fabrikadresse konnte nicht geocoded werden – Mietwagen bitte manuell prüfen.';
|
||
}
|
||
}
|
||
|
||
// ── Schritt 4: Deep-Links generieren ──────
|
||
const flightLink = generateFlightLink({
|
||
originIATA, destIATA, departDate: checkin,
|
||
returnDate: checkout, adults, directOnly: directFlightsOnly,
|
||
});
|
||
const hotelLink = generateHotelLink({
|
||
cityName: destinationCity, checkin, checkout, adults, refundableOnly,
|
||
});
|
||
const carLink = generateCarLink({
|
||
cityName: destinationCity, iataCode: destIATA,
|
||
pickupDate: checkin, dropoffDate: checkout,
|
||
lat: destCoords?.lat, lng: destCoords?.lng,
|
||
});
|
||
|
||
res.json({
|
||
flightLink,
|
||
hotelLink,
|
||
carLink,
|
||
originIATA,
|
||
destIATA,
|
||
iataFallbackUsed,
|
||
iataError,
|
||
carNeeded,
|
||
carMessage,
|
||
distanceMeters,
|
||
hotelAddressUsed,
|
||
meta: {
|
||
origin: originCity,
|
||
destination,
|
||
destinationCity,
|
||
checkin,
|
||
checkout,
|
||
nights,
|
||
adults,
|
||
},
|
||
});
|
||
});
|
||
|
||
// ─────────────────────────────────────────────
|
||
// ENDPOINT: POST /api/check-distance
|
||
// ─────────────────────────────────────────────
|
||
|
||
app.post('/api/check-distance', async (req, res) => {
|
||
const { hotelAddress, fabrikAddress } = req.body;
|
||
|
||
if (!hotelAddress || !fabrikAddress) {
|
||
return res.status(400).json({ error: 'Beide Adressen werden benötigt.' });
|
||
}
|
||
|
||
try {
|
||
const [hotelCoords, fabrikCoords] = await Promise.all([
|
||
geocodeAddress(hotelAddress),
|
||
geocodeAddress(fabrikAddress),
|
||
]);
|
||
|
||
const distanceMeters = await getDistanceMeters(
|
||
hotelCoords.lat, hotelCoords.lng,
|
||
fabrikCoords.lat, fabrikCoords.lng
|
||
);
|
||
|
||
const carNeeded = distanceMeters >= 1500;
|
||
const destinationCity = fabrikCoords.city;
|
||
|
||
const carLink = generateCarLink({
|
||
cityName: destinationCity,
|
||
pickupDate: req.body.checkin || new Date().toISOString().split('T')[0],
|
||
dropoffDate: req.body.checkout || new Date().toISOString().split('T')[0],
|
||
lat: fabrikCoords.lat, lng: fabrikCoords.lng,
|
||
});
|
||
|
||
res.json({
|
||
distanceMeters,
|
||
carNeeded,
|
||
carMessage: carNeeded
|
||
? `🚗 Mietwagen empfohlen – ${(distanceMeters / 1000).toFixed(1)} km zur Fabrik`
|
||
: `✅ Kein Mietwagen nötig – ${distanceMeters}m (fußläufig)`,
|
||
carLink,
|
||
});
|
||
} catch (e) {
|
||
res.status(400).json({
|
||
error: 'Adresse nicht gefunden. Bitte prüfe die Schreibweise.',
|
||
});
|
||
}
|
||
});
|
||
|
||
// ─────────────────────────────────────────────
|
||
// SERVER STARTEN
|
||
// ─────────────────────────────────────────────
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`\n✅ TravelDesk läuft auf http://localhost:${PORT}\n`);
|
||
});
|