Bilder
This commit is contained in:
@@ -32,15 +32,15 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const numericPrice = parseGermanPrice(body.price);
|
const numericPrice = parseGermanPrice(body.price);
|
||||||
|
|
||||||
// Prüfen, ob das Genius-Badge vom Scraper erkannt wurde (z.B. body.hasGenius oder body.isGenius)
|
// 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);
|
const hasGeniusBadge = Boolean(body.hasGenius || body.isGenius || body.genius);
|
||||||
|
|
||||||
// Wenn Genius vorhanden ist: 10% abziehen und runden
|
// Wenn Genius vorhanden ist: 10% abziehen und runden
|
||||||
const geniusPriceNumeric = (numericPrice !== null && hasGeniusBadge)
|
const geniusPriceNumeric = (numericPrice !== null && hasGeniusBadge)
|
||||||
? Math.round(numericPrice * 0.9)
|
? Math.round(numericPrice * 0.9)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (numericPrice !== null && numericPrice > 0) {
|
if (numericPrice !== null && numericPrice > 0) {
|
||||||
try {
|
try {
|
||||||
await query(
|
await query(
|
||||||
`INSERT INTO hotel_price_history (hotel_id, price_numeric, genius_price_numeric, price_raw, currency, nights, persons, tracked_at)
|
`INSERT INTO hotel_price_history (hotel_id, price_numeric, genius_price_numeric, price_raw, currency, nights, persons, tracked_at)
|
||||||
@@ -50,10 +50,14 @@ if (numericPrice !== null && numericPrice > 0) {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('DB Insert Error (hotel_price_history):', err.message);
|
console.error('DB Insert Error (hotel_price_history):', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Stammdaten aktualisieren
|
// 2. Stammdaten & Bad-Bilder aktualisieren
|
||||||
try {
|
try {
|
||||||
|
const bathroomJson = (Array.isArray(body.bathroomImages) && body.bathroomImages.length > 0)
|
||||||
|
? JSON.stringify(body.bathroomImages)
|
||||||
|
: null;
|
||||||
|
|
||||||
await query(
|
await query(
|
||||||
`UPDATE hotels
|
`UPDATE hotels
|
||||||
SET name = COALESCE($1, name),
|
SET name = COALESCE($1, name),
|
||||||
@@ -62,8 +66,9 @@ if (numericPrice !== null && numericPrice > 0) {
|
|||||||
address = COALESCE($4, address),
|
address = COALESCE($4, address),
|
||||||
image_url = COALESCE($5, image_url),
|
image_url = COALESCE($5, image_url),
|
||||||
room_size_m2 = COALESCE($6, room_size_m2),
|
room_size_m2 = COALESCE($6, room_size_m2),
|
||||||
|
bathroom_images = COALESCE($7::jsonb, bathroom_images),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $7::uuid`,
|
WHERE id = $8::uuid`,
|
||||||
[
|
[
|
||||||
body.hotelName || null,
|
body.hotelName || null,
|
||||||
body.rating || null,
|
body.rating || null,
|
||||||
@@ -71,6 +76,7 @@ if (numericPrice !== null && numericPrice > 0) {
|
|||||||
body.address || null,
|
body.address || null,
|
||||||
body.image || null,
|
body.image || null,
|
||||||
body.roomSizeM2 || null,
|
body.roomSizeM2 || null,
|
||||||
|
bathroomJson,
|
||||||
hotelId
|
hotelId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
343
src/app/page.tsx
343
src/app/page.tsx
@@ -76,6 +76,7 @@ interface Hotel {
|
|||||||
image_url?: string | null;
|
image_url?: string | null;
|
||||||
address?: string | null;
|
address?: string | null;
|
||||||
room_size_m2?: number | null;
|
room_size_m2?: number | null;
|
||||||
|
bathroom_images?: string[] | string | null;
|
||||||
review_summary_positive?: string | null;
|
review_summary_positive?: string | null;
|
||||||
review_summary_passable?: string | null;
|
review_summary_passable?: string | null;
|
||||||
review_summary_negative?: string | null;
|
review_summary_negative?: string | null;
|
||||||
@@ -111,6 +112,10 @@ export default function TravelDashboard() {
|
|||||||
const [priceHistory, setPriceHistory] = useState<PriceEntry[]>([]);
|
const [priceHistory, setPriceHistory] = useState<PriceEntry[]>([]);
|
||||||
const [loadingPrices, setLoadingPrices] = useState(false);
|
const [loadingPrices, setLoadingPrices] = useState(false);
|
||||||
|
|
||||||
|
// Bad-Fotos Modal & Lightbox
|
||||||
|
const [selectedBathroomHotel, setSelectedBathroomHotel] = useState<Hotel | null>(null);
|
||||||
|
const [lightboxImage, setLightboxImage] = useState<string | null>(null);
|
||||||
|
|
||||||
// Reise & Hotel Modals
|
// Reise & Hotel Modals
|
||||||
const [showAddTripModal, setShowAddTripModal] = useState(false);
|
const [showAddTripModal, setShowAddTripModal] = useState(false);
|
||||||
const [newTripName, setNewTripName] = useState('');
|
const [newTripName, setNewTripName] = useState('');
|
||||||
@@ -144,19 +149,21 @@ export default function TravelDashboard() {
|
|||||||
const [isSubmittingSight, setIsSubmittingSight] = useState(false);
|
const [isSubmittingSight, setIsSubmittingSight] = useState(false);
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// EFFEKTE & DATEN LADEN
|
// HILFSFUNKTIONEN
|
||||||
// ==========================================
|
// ==========================================
|
||||||
useEffect(() => {
|
const parseBathroomImages = (raw: any): string[] => {
|
||||||
loadData();
|
if (!raw) return [];
|
||||||
}, []);
|
if (Array.isArray(raw)) return raw;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
useEffect(() => {
|
try {
|
||||||
if (activeTripId !== 'ALL') {
|
const parsed = JSON.parse(raw);
|
||||||
loadMembers(activeTripId);
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
} else {
|
} catch (e) {
|
||||||
setTripMembers([]);
|
return [];
|
||||||
}
|
}
|
||||||
}, [activeTripId, trips]);
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
const formatDateGerman = (dateStr?: string | null) => {
|
const formatDateGerman = (dateStr?: string | null) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
@@ -175,6 +182,26 @@ export default function TravelDashboard() {
|
|||||||
return diffDays > 0 ? diffDays : null;
|
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 () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -519,13 +546,14 @@ export default function TravelDashboard() {
|
|||||||
const res = await fetch(`/api/hotels/${hotelId}/scan`, { method: 'POST' });
|
const res = await fetch(`/api/hotels/${hotelId}/scan`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.message || data.error);
|
alert(data.message || data.error);
|
||||||
|
setTimeout(loadData, 4000);
|
||||||
} finally {
|
} finally {
|
||||||
setScanningHotelId(null);
|
setScanningHotelId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FILTER & CLUSTERING MIT AUTOMATISCHEM RANKING
|
// CLUSTERING & FILTER
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const currentHotels = hotels
|
const currentHotels = hotels
|
||||||
.filter(h => h.is_active)
|
.filter(h => h.is_active)
|
||||||
@@ -564,13 +592,12 @@ export default function TravelDashboard() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Normalpreise für Statistik
|
// Preishistorie Stats
|
||||||
const pricesList = priceHistory.map(p => Number(p.price)).filter(p => p > 0);
|
const pricesList = priceHistory.map(p => Number(p.price)).filter(p => p > 0);
|
||||||
const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0;
|
const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0;
|
||||||
const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0;
|
const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0;
|
||||||
const currentPrice = pricesList.length > 0 ? pricesList[pricesList.length - 1] : 0;
|
const currentPrice = pricesList.length > 0 ? pricesList[pricesList.length - 1] : 0;
|
||||||
|
|
||||||
// Geniuspreise für Statistik
|
|
||||||
const geniusPricesList = priceHistory
|
const geniusPricesList = priceHistory
|
||||||
.map(p => (p.genius_price !== null && p.genius_price !== undefined ? Number(p.genius_price) : null))
|
.map(p => (p.genius_price !== null && p.genius_price !== undefined ? Number(p.genius_price) : null))
|
||||||
.filter((p): p is number => p !== null && p > 0);
|
.filter((p): p is number => p !== null && p > 0);
|
||||||
@@ -589,7 +616,7 @@ export default function TravelDashboard() {
|
|||||||
📍 Reise-Ablauf & Freunde-Ranking
|
📍 Reise-Ablauf & Freunde-Ranking
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ color: '#64748b', margin: 0, fontSize: '0.95rem' }}>
|
<p style={{ color: '#64748b', margin: 0, fontSize: '0.95rem' }}>
|
||||||
Hotelvergleich nach Schulnoten (1–6), Bestpreise, Genius-Tracking, Routen & Fahrzeiten
|
Hotelvergleich nach Schulnoten (1–6), Bad-Inspektion, Genius-Tracking & Routen
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: '0.8rem', justifyContent: 'center', marginTop: '1.5rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '0.8rem', justifyContent: 'center', marginTop: '1.5rem', flexWrap: 'wrap' }}>
|
||||||
<button
|
<button
|
||||||
@@ -742,7 +769,7 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TIMELINE MIT AUTOMATISCHEM RANKING */}
|
{/* TIMELINE */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ textAlign: 'center', color: '#64748b', padding: '3rem 0' }}>Lade Daten...</p>
|
<p style={{ textAlign: 'center', color: '#64748b', padding: '3rem 0' }}>Lade Daten...</p>
|
||||||
) : clusteredStops.length === 0 ? (
|
) : clusteredStops.length === 0 ? (
|
||||||
@@ -838,6 +865,8 @@ export default function TravelDashboard() {
|
|||||||
(a.assignments || []).some(asgn => asgn.hotel_id === hotel.id)
|
(a.assignments || []).some(asgn => asgn.hotel_id === hotel.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hotelBathImages = parseBathroomImages(hotel.bathroom_images);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={hotel.id}
|
key={hotel.id}
|
||||||
@@ -890,13 +919,60 @@ export default function TravelDashboard() {
|
|||||||
<div style={{ flex: 1, minWidth: '160px' }}>
|
<div style={{ flex: 1, minWidth: '160px' }}>
|
||||||
<h3 style={{ margin: '0 0 0.25rem 0', fontSize: '1.05rem', color: '#0f172a', lineHeight: 1.3 }}>{hotel.name}</h3>
|
<h3 style={{ margin: '0 0 0.25rem 0', fontSize: '1.05rem', color: '#0f172a', lineHeight: 1.3 }}>{hotel.name}</h3>
|
||||||
<p style={{ margin: '0 0 0.3rem 0', fontSize: '0.78rem', color: '#64748b' }}>📍 {hotel.address || 'Keine Adresse'}</p>
|
<p style={{ margin: '0 0 0.3rem 0', fontSize: '0.78rem', color: '#64748b' }}>📍 {hotel.address || 'Keine Adresse'}</p>
|
||||||
<div style={{ fontSize: '0.75rem', color: '#475569' }}>
|
<div style={{ fontSize: '0.75rem', color: '#475569', display: 'flex', gap: '0.4rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
⭐ <strong>{hotel.rating || 'N/A'}</strong>/10 · {hotel.review_count || 0} Bew.
|
<span>⭐ <strong>{hotel.rating || 'N/A'}</strong>/10 ({hotel.review_count || 0})</span>
|
||||||
|
{hotel.room_size_m2 && <span>· 📐 {hotel.room_size_m2} m²</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* NOTENBOX: FREUNDE BEWERTUNG */}
|
{/* BAD & GOOGLE-FOTOS BUTTON-LEISTE */}
|
||||||
|
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedBathroomHotel(hotel);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: hotelBathImages.length > 0 ? '#eff6ff' : '#f8fafc',
|
||||||
|
border: hotelBathImages.length > 0 ? '1.5px solid #93c5fd' : '1px solid #e2e8f0',
|
||||||
|
color: hotelBathImages.length > 0 ? '#1d4ed8' : '#64748b',
|
||||||
|
padding: '0.35rem 0.65rem',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: '700',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.35rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🚽 Bad-Fotos ({hotelBathImages.length > 0 ? hotelBathImages.length : '0'})
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={getGoogleMapsPhotoUrl(hotel)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
background: '#f8fafc',
|
||||||
|
border: '1px solid #cbd5e1',
|
||||||
|
color: '#334155',
|
||||||
|
padding: '0.35rem 0.65rem',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.3rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗺️ Google Fotos ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* NOTENBOX */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: gradeBg,
|
background: gradeBg,
|
||||||
border: `1px solid ${gradeBorder}`,
|
border: `1px solid ${gradeBorder}`,
|
||||||
@@ -929,56 +1005,11 @@ export default function TravelDashboard() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MITREISENDE ABSTIMMUNGS-STATUS */}
|
{/* PREIS-BOX MIT DOPPELTEM BESTPREIS-VERGLEICH */}
|
||||||
{tripMembers.length > 0 && (
|
|
||||||
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.5rem 0.7rem', marginBottom: '0.75rem' }}>
|
|
||||||
<div style={{ fontSize: '0.7rem', fontWeight: '700', color: '#64748b', marginBottom: '0.3rem', display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<span>STIMMEN DER FREUNDE:</span>
|
|
||||||
<span>{hotel.ratings_json?.length || 0}/{tripMembers.length} abgegeben</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem' }}>
|
|
||||||
{tripMembers.map(m => {
|
|
||||||
const vote = hotel.ratings_json?.find(r => r.member_id === m.id);
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={m.id}
|
|
||||||
style={{
|
|
||||||
fontSize: '0.72rem',
|
|
||||||
padding: '0.1rem 0.4rem',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontWeight: '600',
|
|
||||||
background: vote ? '#ecfdf5' : '#f1f5f9',
|
|
||||||
color: vote ? '#065f46' : '#94a3b8',
|
|
||||||
border: vote ? '1px solid #a7f3d0' : '1px dashed #cbd5e1'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{vote ? `✓ ${m.name}: ${Number(vote.grade).toFixed(1)}` : `⏳ ${m.name}`}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* MINI-KOMMENTARE VORSCHAU */}
|
|
||||||
{hotel.ratings_json && hotel.ratings_json.filter(r => r.comment).length > 0 && (
|
|
||||||
<div style={{ background: '#fafafa', borderTop: '1px dashed #e2e8f0', paddingTop: '0.4rem', marginBottom: '0.75rem', fontSize: '0.75rem' }}>
|
|
||||||
{hotel.ratings_json.filter(r => r.comment).slice(0, 2).map((r, cIdx) => (
|
|
||||||
<div key={cIdx} style={{ display: 'flex', gap: '0.35rem', marginBottom: '0.2rem', alignItems: 'baseline' }}>
|
|
||||||
<span style={{ fontWeight: '700', color: '#334155' }}>{r.name}:</span>
|
|
||||||
<span style={{ color: '#64748b', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
„{r.comment}“
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* PREIS-BOX MIT GETRENNTEM GENIUS- & NORMALPREIS-VERGLEICH */}
|
|
||||||
{totalPrice !== null ? (
|
{totalPrice !== null ? (
|
||||||
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.65rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.65rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
|
||||||
{/* 1. Normalpreis vs. Normaler Bestpreis */}
|
{/* Normalpreis */}
|
||||||
<div style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.45rem 0.6rem' }}>
|
<div style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.45rem 0.6rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.74rem', color: '#64748b', marginBottom: '0.2rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.74rem', color: '#64748b', marginBottom: '0.2rem' }}>
|
||||||
<span style={{ fontWeight: '700' }}>🏷️ REGULÄR</span>
|
<span style={{ fontWeight: '700' }}>🏷️ REGULÄR</span>
|
||||||
@@ -997,7 +1028,7 @@ export default function TravelDashboard() {
|
|||||||
{totalPrice.toLocaleString('de-DE')} €
|
{totalPrice.toLocaleString('de-DE')} €
|
||||||
</span>
|
</span>
|
||||||
<span style={{ color: '#64748b', fontSize: '0.74rem' }}>
|
<span style={{ color: '#64748b', fontSize: '0.74rem' }}>
|
||||||
(🌙 {pricePerNight?.toLocaleString('de-DE')} € / N. · 👤 {pricePerPersonPerNight?.toLocaleString('de-DE')} € / P.)
|
(🌙 {pricePerNight?.toLocaleString('de-DE')} € / N.)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{hotel.best_price && (
|
{hotel.best_price && (
|
||||||
@@ -1007,7 +1038,7 @@ export default function TravelDashboard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Genius-Preis vs. Genius Bestpreis (wenn vorhanden) */}
|
{/* Genius Preis */}
|
||||||
{hotel.latest_genius_price && (
|
{hotel.latest_genius_price && (
|
||||||
<div style={{ background: 'linear-gradient(135deg, #f0f7ff, #e0f2fe)', border: '1.5px solid #93c5fd', borderRadius: '8px', padding: '0.5rem 0.6rem' }}>
|
<div style={{ background: 'linear-gradient(135deg, #f0f7ff, #e0f2fe)', border: '1.5px solid #93c5fd', borderRadius: '8px', padding: '0.5rem 0.6rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.2rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.2rem' }}>
|
||||||
@@ -1047,7 +1078,7 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* SEHENSWÜRDIGKEITEN & FAHRZEITEN */}
|
{/* SEHENSWÜRDIGKEITEN */}
|
||||||
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.7rem' }}>
|
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.7rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.4rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.4rem' }}>
|
||||||
<span style={{ fontWeight: '700', fontSize: '0.8rem', color: '#334155' }}>
|
<span style={{ fontWeight: '700', fontSize: '0.8rem', color: '#334155' }}>
|
||||||
@@ -1122,6 +1153,180 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ======================================================== */}
|
||||||
|
{/* MODAL: BADEZIMMER & GAST-FOTOS (GARANTIERTER Z-INDEX) */}
|
||||||
|
{/* ======================================================== */}
|
||||||
|
{selectedBathroomHotel && (
|
||||||
|
<div
|
||||||
|
onClick={() => 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'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => 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 */}
|
||||||
|
<div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: '800', color: '#0f172a' }}>
|
||||||
|
🚽 Bad- & Klo-Inspektion (Google Maps Gästebilder)
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: '0.2rem 0 0 0', fontSize: '0.82rem', color: '#64748b' }}>
|
||||||
|
{selectedBathroomHotel.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedBathroomHotel(null)}
|
||||||
|
style={{ background: '#e2e8f0', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div style={{ padding: '1.5rem', overflowY: 'auto' }}>
|
||||||
|
{selectedBathroomHotel.review_summary_bathroom && (
|
||||||
|
<div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: '10px', padding: '0.8rem 1rem', marginBottom: '1.2rem' }}>
|
||||||
|
<span style={{ fontWeight: '800', fontSize: '0.82rem', color: '#166534', display: 'block', marginBottom: '0.2rem' }}>
|
||||||
|
🤖 KI-Zusammenfassung zu den Bädern:
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '0.85rem', color: '#14532d' }}>
|
||||||
|
{selectedBathroomHotel.review_summary_bathroom}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const photos = parseBathroomImages(selectedBathroomHotel.bathroom_images);
|
||||||
|
return photos.length > 0 ? (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.8rem' }}>
|
||||||
|
{photos.map((imgUrl, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
onClick={() => setLightboxImage(imgUrl)}
|
||||||
|
style={{ borderRadius: '10px', overflow: 'hidden', border: '1px solid #e2e8f0', position: 'relative', background: '#0f172a', cursor: 'pointer', aspectRatio: '4/3' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imgUrl}
|
||||||
|
alt={`Bad Foto ${i + 1}`}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.2s' }}
|
||||||
|
onError={(e) => { (e.target as HTMLElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
<span style={{ position: 'absolute', bottom: '6px', right: '6px', background: 'rgba(0,0,0,0.65)', color: '#fff', fontSize: '0.7rem', padding: '0.15rem 0.4rem', borderRadius: '4px', fontWeight: 'bold' }}>
|
||||||
|
🔎 Vergrößern
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '2.5rem 1rem', background: '#f8fafc', borderRadius: '12px', border: '1px dashed #cbd5e1' }}>
|
||||||
|
<span style={{ fontSize: '2.5rem', display: 'block', marginBottom: '0.5rem' }}>📷</span>
|
||||||
|
<p style={{ margin: '0 0 0.5rem 0', fontWeight: '700', color: '#1e293b' }}>
|
||||||
|
Noch keine Bad-Bilder direkt in der Datenbank gespeichert.
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: '0 0 1.2rem 0', fontSize: '0.85rem', color: '#64748b' }}>
|
||||||
|
Klicke auf „KI-Reviews Scannen“, um Fotos automatisch abzurufen, oder öffne direkt Google Maps:
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.6rem', justifyContent: 'center' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const hId = selectedBathroomHotel.id;
|
||||||
|
setSelectedBathroomHotel(null);
|
||||||
|
scanSingleHotel(hId);
|
||||||
|
}}
|
||||||
|
style={{ background: '#8b5cf6', color: '#fff', border: 'none', padding: '0.6rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer', fontSize: '0.85rem' }}
|
||||||
|
>
|
||||||
|
🤖 Jetzt Scan starten
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={getGoogleMapsPhotoUrl(selectedBathroomHotel)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ background: '#2563eb', color: '#fff', padding: '0.6rem 1.2rem', borderRadius: '8px', fontWeight: '700', textDecoration: 'none', fontSize: '0.85rem' }}
|
||||||
|
>
|
||||||
|
🗺️ Auf Google Maps öffnen ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div style={{ padding: '1rem 1.5rem', borderTop: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
|
||||||
|
<a
|
||||||
|
href={getGoogleMapsPhotoUrl(selectedBathroomHotel)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#2563eb', fontSize: '0.85rem', fontWeight: '700', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
🗺️ Alle Google Maps Fotos & Videos öffnen ↗
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedBathroomHotel(null)}
|
||||||
|
style={{ background: '#2563eb', color: '#ffffff', border: 'none', padding: '0.5rem 1.2rem', borderRadius: '6px', fontWeight: '700', cursor: 'pointer', fontSize: '0.85rem' }}
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ======================================================== */}
|
||||||
|
{/* LIGHTBOX GROSSANSICHT */}
|
||||||
|
{/* ======================================================== */}
|
||||||
|
{lightboxImage && (
|
||||||
|
<div
|
||||||
|
onClick={() => 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'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={lightboxImage}
|
||||||
|
alt="Bad Großansicht"
|
||||||
|
style={{ maxWidth: '90vw', maxHeight: '85vh', borderRadius: '8px', boxShadow: '0 10px 30px rgba(0,0,0,0.5)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* MODAL 1: STIMMABGABE */}
|
{/* MODAL 1: STIMMABGABE */}
|
||||||
{ratingHotel && (
|
{ratingHotel && (
|
||||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user