const { useState, useRef } = React;
// ─────────────────────────────────────────────
// HILFSFUNKTIONEN
// ─────────────────────────────────────────────
function formatDateDE(dateStr) {
if (!dateStr) return '';
const [y, m, d] = dateStr.split('-');
return `${d}.${m}.${y}`;
}
// ─────────────────────────────────────────────
// HEADER
// ─────────────────────────────────────────────
function Header() {
return (
✈️
TravelDesk
Intelligente Reiseplanung für Ihr Team
);
}
// ─────────────────────────────────────────────
// LADEANIMATION
// ─────────────────────────────────────────────
const LOADING_STEPS = [
{ icon: '📍', text: 'Fabrikadresse wird ermittelt...' },
{ icon: '✈️', text: 'Flug-Link wird vorbereitet...' },
{ icon: '🏨', text: 'Hotel-Link wird vorbereitet...' },
{ icon: '🚗', text: 'Mietwagen wird geprüft...' },
];
function LoadingState({ currentStep }) {
return (
{LOADING_STEPS.map((step, i) => (
{step.icon}
{step.text}
{i < currentStep && ✓}
))}
);
}
// ─────────────────────────────────────────────
// EINGABE-FORMULAR
// ─────────────────────────────────────────────
function SearchForm({ onSearch, loading }) {
const today = new Date().toISOString().split('T')[0];
const nextWeek = new Date(Date.now() + 7 * 86400000).toISOString().split('T')[0];
const nextWeek5 = new Date(Date.now() + 12 * 86400000).toISOString().split('T')[0];
const [form, setForm] = useState({
origin: 'Stuttgart, Deutschland',
destStreet: '',
destHouseNumber: '',
destPostalCode: '',
destCity: '',
destCountry: '',
checkin: nextWeek,
checkout: nextWeek5,
adults: 1,
refundableOnly: true,
directFlightsOnly: false,
hotelAddress: '',
specialRequirements: '',
});
const set = (field) => (e) => {
const val = e.target.type === 'checkbox' ? e.target.checked
: e.target.type === 'number' ? parseInt(e.target.value)
: e.target.value;
setForm((prev) => ({ ...prev, [field]: val }));
};
const handleSubmit = (e) => {
e.preventDefault();
if (!form.destCity.trim()) {
alert('Bitte gib mindestens die Zielstadt ein.');
return;
}
onSearch(form);
};
return (
);
}
// ─────────────────────────────────────────────
// DISTANZ-CHECK WIDGET (inline im Auto-Block)
// ─────────────────────────────────────────────
function DistanceCheckWidget({ fabrikAddress, checkin, checkout, carLink }) {
const [hotelAddr, setHotelAddr] = useState('');
const [checking, setChecking] = useState(false);
const [checkResult, setCheckResult] = useState(null);
const [checkError, setCheckError] = useState('');
const handleCheck = async () => {
if (!hotelAddr.trim()) return;
setChecking(true);
setCheckResult(null);
setCheckError('');
try {
const res = await fetch('/api/check-distance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hotelAddress: hotelAddr, fabrikAddress, checkin, checkout }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Fehler');
setCheckResult(data);
} catch (e) {
setCheckError(e.message || 'Adresse nicht gefunden. Bitte prüfe die Schreibweise.');
} finally {
setChecking(false);
}
};
return (
setHotelAddr(e.target.value)}
placeholder="Hotel-Adresse eintragen..."
className="input text-sm flex-1"
onKeyDown={(e) => e.key === 'Enter' && handleCheck()}
/>
{checkError && (
{checkError}
)}
{checkResult && (
{checkResult.carMessage}
{checkResult.carNeeded && (
Mietwagen →
)}
)}
);
}
// ─────────────────────────────────────────────
// ERGEBNIS-BLÖCKE
// ─────────────────────────────────────────────
function FlugBlock({ data }) {
const { flightLink, originIATA, destIATA, iataFallbackUsed, iataError, meta } = data;
return (
✈️
Flug buchen
{originIATA && destIATA ? (
<>
{originIATA} → {destIATA}
{formatDateDE(meta.checkin)} · {meta.adults} Person{meta.adults !== 1 ? 'en' : ''} · Economy
{iataFallbackUsed && (
ℹ️ IATA via KI ermittelt
)}
>
) : (
⚠️ IATA-Code für{' '}
{[iataError?.origin, iataError?.dest].filter(Boolean).join(' / ')}{' '}
nicht gefunden – bitte manuell auf Booking.com suchen
)}
);
}
function HotelBlock({ data }) {
const { hotelLink, meta } = data;
const nights = meta.nights;
return (
🏨
Hotel suchen
In {meta.destinationCity}
{formatDateDE(meta.checkin)} bis {formatDateDE(meta.checkout)} · {nights} Nacht{nights !== 1 ? 'e' : ''}
Sortiert nach günstigstem Preis{data.refundableOnly ? ' · Nur kostenlos stornierbar' : ''}
);
}
function AutoBlock({ data }) {
const { carLink, carNeeded, carMessage, distanceMeters, hotelAddressUsed, meta } = data;
// Szenario A: Hotel angegeben + Mietwagen empfohlen
if (carNeeded === true) {
return (
🚗
Mietwagen empfohlen
{carMessage}
📍 Hotel: {hotelAddressUsed}
🏭 Fabrik: {meta.destination}
📏 Entfernung: {distanceMeters}m ({(distanceMeters / 1000).toFixed(1)} km)
);
}
// Szenario B: Hotel angegeben + kein Mietwagen nötig
if (carNeeded === false) {
return (
✅
Kein Mietwagen nötig
{carMessage}
{distanceMeters}m – fußläufig erreichbar
);
}
// Szenario C: Kein Hotel angegeben
return (
🚗
Mietwagen – noch unbekannt
{carMessage}
);
}
// ─────────────────────────────────────────────
// ERGEBNIS-BEREICH
// ─────────────────────────────────────────────
function ResultSection({ data }) {
if (!data) return null;
return (
);
}
// ─────────────────────────────────────────────
// HAUPT-APP
// ─────────────────────────────────────────────
function App() {
const [loading, setLoading] = useState(false);
const [loadingStep, setLoadingStep] = useState(0);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const resultRef = useRef(null);
const handleSearch = async (formData) => {
setLoading(true);
setResult(null);
setError(null);
setLoadingStep(0);
const stepInterval = setInterval(() => {
setLoadingStep((s) => Math.min(s + 1, 4));
}, 1200);
try {
const res = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
clearInterval(stepInterval);
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || `Server-Fehler: ${res.status}`);
}
const data = await res.json();
// refundableOnly an data anhängen für HotelBlock
data.refundableOnly = formData.refundableOnly;
setResult(data);
setLoadingStep(5);
setTimeout(() => {
resultRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
} catch (e) {
clearInterval(stepInterval);
setError(e.message || 'Ein unbekannter Fehler ist aufgetreten.');
} finally {
setLoading(false);
}
};
return (
{loading &&
}
{error && (
Fehler: {error}
)}
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();