Genius
This commit is contained in:
@@ -12,6 +12,7 @@ SELECT
|
|||||||
ph.price_numeric::float AS latest_price,
|
ph.price_numeric::float AS latest_price,
|
||||||
ph.price_per_night::float AS latest_price_per_night,
|
ph.price_per_night::float AS latest_price_per_night,
|
||||||
ph.price_per_person::float AS latest_price_per_person,
|
ph.price_per_person::float AS latest_price_per_person,
|
||||||
|
ph.genius_price_numeric::float AS latest_genius_price,
|
||||||
ph.nights AS latest_nights,
|
ph.nights AS latest_nights,
|
||||||
ph.persons AS latest_persons,
|
ph.persons AS latest_persons,
|
||||||
bp.best_price,
|
bp.best_price,
|
||||||
@@ -21,7 +22,7 @@ SELECT
|
|||||||
FROM hotels h
|
FROM hotels h
|
||||||
LEFT JOIN trips t ON h.trip_id = t.id
|
LEFT JOIN trips t ON h.trip_id = t.id
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT price_numeric, price_per_night, price_per_person, nights, persons
|
SELECT price_numeric, genius_price_numeric, price_per_night, price_per_person, nights, persons
|
||||||
FROM hotel_price_history
|
FROM hotel_price_history
|
||||||
WHERE hotel_id::text = h.id::text
|
WHERE hotel_id::text = h.id::text
|
||||||
ORDER BY tracked_at DESC NULLS LAST, id DESC
|
ORDER BY tracked_at DESC NULLS LAST, id DESC
|
||||||
|
|||||||
@@ -32,18 +32,25 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const numericPrice = parseGermanPrice(body.price);
|
const numericPrice = parseGermanPrice(body.price);
|
||||||
|
|
||||||
// 1. In hotel_price_history eintragen mit echten Spalten
|
// Prüfen, ob das Genius-Badge vom Scraper erkannt wurde (z.B. body.hasGenius oder body.isGenius)
|
||||||
if (numericPrice !== null && numericPrice > 0) {
|
const hasGeniusBadge = Boolean(body.hasGenius || body.isGenius || body.genius);
|
||||||
try {
|
|
||||||
await query(
|
// Wenn Genius vorhanden ist: 10% abziehen und runden
|
||||||
`INSERT INTO hotel_price_history (hotel_id, price_numeric, price_raw, currency, nights, persons, tracked_at)
|
const geniusPriceNumeric = (numericPrice !== null && hasGeniusBadge)
|
||||||
VALUES ($1::uuid, $2, $3, 'EUR', $4, $5, NOW())`,
|
? Math.round(numericPrice * 0.9)
|
||||||
[hotelId, numericPrice, String(body.price || ''), body.nights || 1, body.persons || 2]
|
: null;
|
||||||
);
|
|
||||||
} catch (err: any) {
|
if (numericPrice !== null && numericPrice > 0) {
|
||||||
console.error('DB Insert Error (hotel_price_history):', err.message);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Stammdaten aktualisieren
|
// 2. Stammdaten aktualisieren
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ interface Hotel {
|
|||||||
latest_price?: number | null;
|
latest_price?: number | null;
|
||||||
latest_price_per_night?: number | null;
|
latest_price_per_night?: number | null;
|
||||||
latest_price_per_person?: number | null;
|
latest_price_per_person?: number | null;
|
||||||
|
latest_genius_price?: number | null;
|
||||||
latest_nights?: number | null;
|
latest_nights?: number | null;
|
||||||
latest_persons?: number | null;
|
latest_persons?: number | null;
|
||||||
best_price?: number | null;
|
best_price?: number | null;
|
||||||
@@ -182,7 +183,7 @@ export default function TravelDashboard() {
|
|||||||
const dataTrips = await resTrips.json();
|
const dataTrips = await resTrips.json();
|
||||||
setTrips(dataTrips.trips || []);
|
setTrips(dataTrips.trips || []);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resHotels = await fetch('/api/hotels');
|
const resHotels = await fetch('/api/hotels');
|
||||||
@@ -190,7 +191,7 @@ export default function TravelDashboard() {
|
|||||||
const dataHotels = await resHotels.json();
|
const dataHotels = await resHotels.json();
|
||||||
setHotels(dataHotels.hotels || []);
|
setHotels(dataHotels.hotels || []);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resAttractions = await fetch('/api/attractions');
|
const resAttractions = await fetch('/api/attractions');
|
||||||
@@ -198,7 +199,7 @@ export default function TravelDashboard() {
|
|||||||
const dataAttractions = await resAttractions.json();
|
const dataAttractions = await resAttractions.json();
|
||||||
setAttractions(dataAttractions.attractions || []);
|
setAttractions(dataAttractions.attractions || []);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -572,7 +573,7 @@ export default function TravelDashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: '1100px', margin: '0 auto', padding: '2rem 1rem', fontFamily: 'system-ui, -apple-system, sans-serif', color: '#1f2937' }}>
|
<div style={{ maxWidth: '1100px', margin: '0 auto', padding: '2rem 1rem', fontFamily: 'system-ui, -apple-system, sans-serif', color: '#1f2937' }}>
|
||||||
|
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
||||||
<h1 style={{ fontSize: '2.2rem', marginBottom: '0.5rem', fontWeight: '800', color: '#0f172a' }}>
|
<h1 style={{ fontSize: '2.2rem', marginBottom: '0.5rem', fontWeight: '800', color: '#0f172a' }}>
|
||||||
@@ -804,11 +805,11 @@ export default function TravelDashboard() {
|
|||||||
const hotelNights = calculateNights(hotel.checkin_date, hotel.checkout_date) || hotel.latest_nights || nights || 1;
|
const hotelNights = calculateNights(hotel.checkin_date, hotel.checkout_date) || hotel.latest_nights || nights || 1;
|
||||||
const persons = hotel.latest_persons || 2;
|
const persons = hotel.latest_persons || 2;
|
||||||
const totalPrice = hotel.latest_price !== null && hotel.latest_price !== undefined ? Number(hotel.latest_price) : null;
|
const totalPrice = hotel.latest_price !== null && hotel.latest_price !== undefined ? Number(hotel.latest_price) : null;
|
||||||
const pricePerNight = hotel.latest_price_per_night
|
const pricePerNight = hotel.latest_price_per_night
|
||||||
? Math.round(Number(hotel.latest_price_per_night))
|
? Math.round(Number(hotel.latest_price_per_night))
|
||||||
: (totalPrice ? Math.round(totalPrice / hotelNights) : null);
|
: (totalPrice ? Math.round(totalPrice / hotelNights) : null);
|
||||||
const pricePerPersonPerNight = hotel.latest_price_per_person
|
const pricePerPersonPerNight = hotel.latest_price_per_person
|
||||||
? Math.round(Number(hotel.latest_price_per_person))
|
? Math.round(Number(hotel.latest_price_per_person))
|
||||||
: (totalPrice ? Math.round(totalPrice / (hotelNights * persons)) : null);
|
: (totalPrice ? Math.round(totalPrice / (hotelNights * persons)) : null);
|
||||||
|
|
||||||
const hasGrade = hotel.avg_grade !== null && hotel.avg_grade !== undefined;
|
const hasGrade = hotel.avg_grade !== null && hotel.avg_grade !== undefined;
|
||||||
@@ -969,31 +970,67 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PREIS-BOX MIT BESTPREIS-VERGLEICH */}
|
{/* PREIS-BOX MIT BESTPREIS-VERGLEICH & GENIUS */}
|
||||||
{totalPrice !== null ? (
|
{totalPrice !== null ? (
|
||||||
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.6rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.6rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.3rem' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', flexWrap: 'wrap' }}>
|
{hotel.latest_genius_price ? (
|
||||||
<span style={{ fontSize: '0.78rem', fontWeight: '700', color: '#475569' }}>Aktuell:</span>
|
<>
|
||||||
<span style={{ fontWeight: '800', color: '#0f172a', fontSize: '0.92rem' }}>
|
{/* Durchgestrichener Normalpreis bei aktivem Genius */}
|
||||||
💰 {totalPrice.toLocaleString('de-DE')} €
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.76rem', color: '#64748b' }}>
|
||||||
</span>
|
<span>Regulär: <span style={{ textDecoration: 'line-through' }}>{totalPrice.toLocaleString('de-DE')} €</span></span>
|
||||||
<span style={{ color: '#64748b', fontSize: '0.76rem' }}>
|
<span style={{ background: '#16a34a', color: '#fff', fontSize: '0.68rem', fontWeight: '800', padding: '0.1rem 0.35rem', borderRadius: '4px' }}>-10% GENIUS</span>
|
||||||
(🌙 {pricePerNight?.toLocaleString('de-DE')} € / N. · 👤 {pricePerPersonPerNight?.toLocaleString('de-DE')} € / P.)
|
</div>
|
||||||
</span>
|
|
||||||
|
{/* Aktive Genius-Zeile mit Tiefstpreis-Badge */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.3rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', flexWrap: 'wrap' }}>
|
||||||
|
<span style={{ fontSize: '0.78rem', fontWeight: '700', color: '#003580' }}>Genius:</span>
|
||||||
|
<span style={{ fontWeight: '900', color: '#003580', fontSize: '0.95rem' }}>
|
||||||
|
💙 {Number(hotel.latest_genius_price).toLocaleString('de-DE')} €
|
||||||
|
</span>
|
||||||
|
<span style={{ color: '#0052b4', fontSize: '0.76rem' }}>
|
||||||
|
(🌙 {Math.round(Number(hotel.latest_genius_price) / hotelNights).toLocaleString('de-DE')} € / N.)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hotel.best_price && Number(hotel.latest_genius_price) > Number(hotel.best_price) ? (
|
||||||
|
<span style={{ background: '#fee2e2', color: '#991b1b', border: '1px solid #fecaca', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '700' }}>
|
||||||
|
▲ +{(Number(hotel.latest_genius_price) - Number(hotel.best_price)).toLocaleString('de-DE')} €
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ background: '#ecfdf5', color: '#047857', border: '1px solid #a7f3d0', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '800' }}>
|
||||||
|
🔥 Tiefstpreis
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* Standard-Preiszeile mit Tiefstpreis-Badge */
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.3rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', flexWrap: 'wrap' }}>
|
||||||
|
<span style={{ fontSize: '0.78rem', fontWeight: '700', color: '#475569' }}>Aktuell:</span>
|
||||||
|
<span style={{ fontWeight: '800', color: '#0f172a', fontSize: '0.92rem' }}>
|
||||||
|
💰 {totalPrice.toLocaleString('de-DE')} €
|
||||||
|
</span>
|
||||||
|
<span style={{ color: '#64748b', fontSize: '0.76rem' }}>
|
||||||
|
(🌙 {pricePerNight?.toLocaleString('de-DE')} € / N. · 👤 {pricePerPersonPerNight?.toLocaleString('de-DE')} € / P.)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hotel.best_price && totalPrice > Number(hotel.best_price) ? (
|
||||||
|
<span style={{ background: '#fee2e2', color: '#991b1b', border: '1px solid #fecaca', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '700' }}>
|
||||||
|
▲ +{(totalPrice - Number(hotel.best_price)).toLocaleString('de-DE')} €
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ background: '#ecfdf5', color: '#047857', border: '1px solid #a7f3d0', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '800' }}>
|
||||||
|
🔥 Tiefstpreis
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{hotel.best_price && totalPrice > Number(hotel.best_price) ? (
|
{/* Allzeit-Bestpreis Zeile */}
|
||||||
<span style={{ background: '#fee2e2', color: '#991b1b', border: '1px solid #fecaca', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '700' }}>
|
|
||||||
▲ +{(totalPrice - Number(hotel.best_price)).toLocaleString('de-DE')} €
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span style={{ background: '#ecfdf5', color: '#047857', border: '1px solid #a7f3d0', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '800' }}>
|
|
||||||
🔥 Tiefstpreis
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hotel.best_price && (
|
{hotel.best_price && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', borderTop: '1px dashed #e2e8f0', paddingTop: '0.3rem', fontSize: '0.75rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', borderTop: '1px dashed #e2e8f0', paddingTop: '0.3rem', fontSize: '0.75rem' }}>
|
||||||
<span style={{ fontWeight: '700', color: '#059669' }}>🏆 Bestpreis:</span>
|
<span style={{ fontWeight: '700', color: '#059669' }}>🏆 Bestpreis:</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user