diff --git a/src/app/api/webhooks/scraper/route.ts b/src/app/api/webhooks/scraper/route.ts index 1ca9442..05c0973 100644 --- a/src/app/api/webhooks/scraper/route.ts +++ b/src/app/api/webhooks/scraper/route.ts @@ -32,28 +32,32 @@ export async function POST(req: NextRequest) { const numericPrice = parseGermanPrice(body.price); -// Prüfen, ob das Genius-Badge vom Scraper erkannt wurde (z.B. body.hasGenius oder body.isGenius) -const hasGeniusBadge = Boolean(body.hasGenius || body.isGenius || body.genius); + // Prüfen, ob das Genius-Badge vom Scraper erkannt wurde (z.B. body.hasGenius oder body.isGenius) + const hasGeniusBadge = Boolean(body.hasGenius || body.isGenius || body.genius); -// Wenn Genius vorhanden ist: 10% abziehen und runden -const geniusPriceNumeric = (numericPrice !== null && hasGeniusBadge) - ? Math.round(numericPrice * 0.9) - : null; + // Wenn Genius vorhanden ist: 10% abziehen und runden + const geniusPriceNumeric = (numericPrice !== null && hasGeniusBadge) + ? Math.round(numericPrice * 0.9) + : null; -if (numericPrice !== null && numericPrice > 0) { - try { - await query( - `INSERT INTO hotel_price_history (hotel_id, price_numeric, genius_price_numeric, price_raw, currency, nights, persons, tracked_at) + if (numericPrice !== null && numericPrice > 0) { + try { + await query( + `INSERT INTO hotel_price_history (hotel_id, price_numeric, genius_price_numeric, price_raw, currency, nights, persons, tracked_at) VALUES ($1::uuid, $2, $3, $4, 'EUR', $5, $6, NOW())`, - [hotelId, numericPrice, geniusPriceNumeric, String(body.price || ''), body.nights || 1, body.persons || 2] - ); - } catch (err: any) { - console.error('DB Insert Error (hotel_price_history):', err.message); - } -} + [hotelId, numericPrice, geniusPriceNumeric, String(body.price || ''), body.nights || 1, body.persons || 2] + ); + } catch (err: any) { + console.error('DB Insert Error (hotel_price_history):', err.message); + } + } - // 2. Stammdaten aktualisieren + // 2. Stammdaten & Bad-Bilder aktualisieren try { + const bathroomJson = (Array.isArray(body.bathroomImages) && body.bathroomImages.length > 0) + ? JSON.stringify(body.bathroomImages) + : null; + await query( `UPDATE hotels SET name = COALESCE($1, name), @@ -62,8 +66,9 @@ if (numericPrice !== null && numericPrice > 0) { address = COALESCE($4, address), image_url = COALESCE($5, image_url), room_size_m2 = COALESCE($6, room_size_m2), + bathroom_images = COALESCE($7::jsonb, bathroom_images), updated_at = NOW() - WHERE id = $7::uuid`, + WHERE id = $8::uuid`, [ body.hotelName || null, body.rating || null, @@ -71,6 +76,7 @@ if (numericPrice !== null && numericPrice > 0) { body.address || null, body.image || null, body.roomSizeM2 || null, + bathroomJson, hotelId ] ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 37ec02c..8bb789a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -76,6 +76,7 @@ interface Hotel { image_url?: string | null; address?: string | null; room_size_m2?: number | null; + bathroom_images?: string[] | string | null; review_summary_positive?: string | null; review_summary_passable?: string | null; review_summary_negative?: string | null; @@ -111,6 +112,10 @@ export default function TravelDashboard() { const [priceHistory, setPriceHistory] = useState([]); const [loadingPrices, setLoadingPrices] = useState(false); + // Bad-Fotos Modal & Lightbox + const [selectedBathroomHotel, setSelectedBathroomHotel] = useState(null); + const [lightboxImage, setLightboxImage] = useState(null); + // Reise & Hotel Modals const [showAddTripModal, setShowAddTripModal] = useState(false); const [newTripName, setNewTripName] = useState(''); @@ -144,19 +149,21 @@ export default function TravelDashboard() { const [isSubmittingSight, setIsSubmittingSight] = useState(false); // ========================================== - // EFFEKTE & DATEN LADEN + // HILFSFUNKTIONEN // ========================================== - useEffect(() => { - loadData(); - }, []); - - useEffect(() => { - if (activeTripId !== 'ALL') { - loadMembers(activeTripId); - } else { - setTripMembers([]); + const parseBathroomImages = (raw: any): string[] => { + if (!raw) return []; + if (Array.isArray(raw)) return raw; + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch (e) { + return []; + } } - }, [activeTripId, trips]); + return []; + }; const formatDateGerman = (dateStr?: string | null) => { if (!dateStr) return ''; @@ -175,6 +182,26 @@ export default function TravelDashboard() { return diffDays > 0 ? diffDays : null; }; + const getGoogleMapsPhotoUrl = (hotel: Hotel) => { + const query = encodeURIComponent(`${hotel.name} ${hotel.address || ''}`); + return `https://www.google.com/maps/search/${query}?hl=de`; + }; + + // ========================================== + // EFFEKTE & DATEN LADEN + // ========================================== + useEffect(() => { + loadData(); + }, []); + + useEffect(() => { + if (activeTripId !== 'ALL') { + loadMembers(activeTripId); + } else { + setTripMembers([]); + } + }, [activeTripId, trips]); + const loadData = async () => { setLoading(true); try { @@ -519,13 +546,14 @@ export default function TravelDashboard() { const res = await fetch(`/api/hotels/${hotelId}/scan`, { method: 'POST' }); const data = await res.json(); alert(data.message || data.error); + setTimeout(loadData, 4000); } finally { setScanningHotelId(null); } }; // ========================================== - // FILTER & CLUSTERING MIT AUTOMATISCHEM RANKING + // CLUSTERING & FILTER // ========================================== const currentHotels = hotels .filter(h => h.is_active) @@ -564,13 +592,12 @@ export default function TravelDashboard() { return acc; }, []); - // Normalpreise für Statistik + // Preishistorie Stats const pricesList = priceHistory.map(p => Number(p.price)).filter(p => p > 0); const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0; const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0; const currentPrice = pricesList.length > 0 ? pricesList[pricesList.length - 1] : 0; - // Geniuspreise für Statistik const geniusPricesList = priceHistory .map(p => (p.genius_price !== null && p.genius_price !== undefined ? Number(p.genius_price) : null)) .filter((p): p is number => p !== null && p > 0); @@ -589,7 +616,7 @@ export default function TravelDashboard() { 📍 Reise-Ablauf & Freunde-Ranking

- Hotelvergleich nach Schulnoten (1–6), Bestpreise, Genius-Tracking, Routen & Fahrzeiten + Hotelvergleich nach Schulnoten (1–6), Bad-Inspektion, Genius-Tracking & Routen

+ + 🗺️ Google Fotos ↗ + +
+ + {/* NOTENBOX */}
- {/* MITREISENDE ABSTIMMUNGS-STATUS */} - {tripMembers.length > 0 && ( -
-
- STIMMEN DER FREUNDE: - {hotel.ratings_json?.length || 0}/{tripMembers.length} abgegeben -
-
- {tripMembers.map(m => { - const vote = hotel.ratings_json?.find(r => r.member_id === m.id); - return ( - - {vote ? `✓ ${m.name}: ${Number(vote.grade).toFixed(1)}` : `⏳ ${m.name}`} - - ); - })} -
-
- )} - - {/* MINI-KOMMENTARE VORSCHAU */} - {hotel.ratings_json && hotel.ratings_json.filter(r => r.comment).length > 0 && ( -
- {hotel.ratings_json.filter(r => r.comment).slice(0, 2).map((r, cIdx) => ( -
- {r.name}: - - „{r.comment}“ - -
- ))} -
- )} - - {/* PREIS-BOX MIT GETRENNTEM GENIUS- & NORMALPREIS-VERGLEICH */} + {/* PREIS-BOX MIT DOPPELTEM BESTPREIS-VERGLEICH */} {totalPrice !== null ? (
- {/* 1. Normalpreis vs. Normaler Bestpreis */} + {/* Normalpreis */}
🏷️ REGULÄR @@ -997,7 +1028,7 @@ export default function TravelDashboard() { {totalPrice.toLocaleString('de-DE')} € - (🌙 {pricePerNight?.toLocaleString('de-DE')} € / N. · 👤 {pricePerPersonPerNight?.toLocaleString('de-DE')} € / P.) + (🌙 {pricePerNight?.toLocaleString('de-DE')} € / N.)
{hotel.best_price && ( @@ -1007,7 +1038,7 @@ export default function TravelDashboard() { )}
- {/* 2. Genius-Preis vs. Genius Bestpreis (wenn vorhanden) */} + {/* Genius Preis */} {hotel.latest_genius_price && (
@@ -1047,7 +1078,7 @@ export default function TravelDashboard() {
)} - {/* SEHENSWÜRDIGKEITEN & FAHRZEITEN */} + {/* SEHENSWÜRDIGKEITEN */}
@@ -1122,6 +1153,180 @@ export default function TravelDashboard() {
)} + {/* ======================================================== */} + {/* MODAL: BADEZIMMER & GAST-FOTOS (GARANTIERTER Z-INDEX) */} + {/* ======================================================== */} + {selectedBathroomHotel && ( +
setSelectedBathroomHotel(null)} + style={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'rgba(15, 23, 42, 0.75)', + backdropFilter: 'blur(4px)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 9999, + padding: '1rem' + }} + > +
e.stopPropagation()} + style={{ + background: '#ffffff', + borderRadius: '16px', + maxWidth: '850px', + width: '100%', + maxHeight: '90vh', + display: 'flex', + flexDirection: 'column', + boxShadow: '0 25px 50px -12px rgba(0,0,0,0.35)', + overflow: 'hidden', + position: 'relative' + }} + > + {/* Modal Header */} +
+
+

+ 🚽 Bad- & Klo-Inspektion (Google Maps Gästebilder) +

+

+ {selectedBathroomHotel.name} +

+
+ +
+ + {/* Modal Body */} +
+ {selectedBathroomHotel.review_summary_bathroom && ( +
+ + 🤖 KI-Zusammenfassung zu den Bädern: + + + {selectedBathroomHotel.review_summary_bathroom} + +
+ )} + + {(() => { + const photos = parseBathroomImages(selectedBathroomHotel.bathroom_images); + return photos.length > 0 ? ( +
+ {photos.map((imgUrl, i) => ( +
setLightboxImage(imgUrl)} + style={{ borderRadius: '10px', overflow: 'hidden', border: '1px solid #e2e8f0', position: 'relative', background: '#0f172a', cursor: 'pointer', aspectRatio: '4/3' }} + > + {`Bad { (e.target as HTMLElement).style.display = 'none'; }} + /> + + 🔎 Vergrößern + +
+ ))} +
+ ) : ( +
+ 📷 +

+ Noch keine Bad-Bilder direkt in der Datenbank gespeichert. +

+

+ Klicke auf „KI-Reviews Scannen“, um Fotos automatisch abzurufen, oder öffne direkt Google Maps: +

+
+ + + 🗺️ Auf Google Maps öffnen ↗ + +
+
+ ); + })()} +
+ + {/* Modal Footer */} +
+ + 🗺️ Alle Google Maps Fotos & Videos öffnen ↗ + + +
+
+
+ )} + + {/* ======================================================== */} + {/* LIGHTBOX GROSSANSICHT */} + {/* ======================================================== */} + {lightboxImage && ( +
setLightboxImage(null)} + style={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'rgba(0, 0, 0, 0.9)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 10000, + padding: '2rem' + }} + > + Bad Großansicht +
+ )} + {/* MODAL 1: STIMMABGABE */} {ratingHotel && (