feat(ui): add debug mode toggle, delete hotel button, and copy hotel to trip modal
This commit is contained in:
316
src/app/page.tsx
316
src/app/page.tsx
@@ -130,6 +130,32 @@ export default function TravelDashboard() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [showParkedModal, setShowParkedModal] = useState(false);
|
const [showParkedModal, setShowParkedModal] = useState(false);
|
||||||
|
|
||||||
|
// Scraper Debug-Modus Toggle
|
||||||
|
const [isDebugMode, setIsDebugMode] = useState(false);
|
||||||
|
|
||||||
|
// Initialisiere Debug-Modus aus localStorage im Client
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setIsDebugMode(localStorage.getItem('scraper_debug_mode') === 'true');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleDebugMode = () => {
|
||||||
|
const next = !isDebugMode;
|
||||||
|
setIsDebugMode(next);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('scraper_debug_mode', String(next));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hotel Löschen Status
|
||||||
|
const [deletingHotelId, setDeletingHotelId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Hotel in Trip kopieren Modal & Status
|
||||||
|
const [copyHotelTarget, setCopyHotelTarget] = useState<Hotel | null>(null);
|
||||||
|
const [selectedTargetTripId, setSelectedTargetTripId] = useState<string>('');
|
||||||
|
const [isCopyingHotel, setIsCopyingHotel] = useState(false);
|
||||||
|
|
||||||
// Mitreisende Modal
|
// Mitreisende Modal
|
||||||
const [showMembersModal, setShowMembersModal] = useState(false);
|
const [showMembersModal, setShowMembersModal] = useState(false);
|
||||||
const [newMemberName, setNewMemberName] = useState('');
|
const [newMemberName, setNewMemberName] = useState('');
|
||||||
@@ -284,13 +310,14 @@ export default function TravelDashboard() {
|
|||||||
const scanSingleHotel = async (hotel: Hotel) => {
|
const scanSingleHotel = async (hotel: Hotel) => {
|
||||||
const reviewCountDisplay = hotel.review_summary_count || hotel.review_count || 80;
|
const reviewCountDisplay = hotel.review_summary_count || hotel.review_count || 80;
|
||||||
const isConfirmed = confirm(
|
const isConfirmed = confirm(
|
||||||
`Möchtest du die KI-Review-Analyse für "${hotel.name}" wirklich neu starten?\n\nHinweis: Dabei werden bis zu ${reviewCountDisplay} Gästebewertungen gescrapt und per n8n/Ollama neu zusammengefasst.`
|
`Möchtest du die KI-Review-Analyse für "${hotel.name}" wirklich neu starten?\n\nHinweis: Dabei werden bis zu ${reviewCountDisplay} Gästebewertungen gescrapt und per n8n/Ollama neu zusammengefasst.${isDebugMode ? '\n\n🐛 Scraper-Debug-Modus ist AKTIV (HTML & Screenshots werden gespeichert).' : ''}`
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
|
|
||||||
setScanningHotelId(hotel.id);
|
setScanningHotelId(hotel.id);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/hotels/${hotel.id}/scan`, { method: 'POST' });
|
const debugQuery = isDebugMode ? '?debug=true' : '';
|
||||||
|
const res = await fetch(`/api/hotels/${hotel.id}/scan${debugQuery}`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.message || (data.success ? 'Scrape-Job erfolgreich gestartet!' : 'Fehler beim Starten'));
|
alert(data.message || (data.success ? 'Scrape-Job erfolgreich gestartet!' : 'Fehler beim Starten'));
|
||||||
setTimeout(loadData, 5000);
|
setTimeout(loadData, 5000);
|
||||||
@@ -304,7 +331,8 @@ export default function TravelDashboard() {
|
|||||||
const refreshHotelMetadata = async (hotel: Hotel) => {
|
const refreshHotelMetadata = async (hotel: Hotel) => {
|
||||||
setRefreshingMetadataHotelId(hotel.id);
|
setRefreshingMetadataHotelId(hotel.id);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/hotels/${hotel.id}/scan?mode=stammdaten`, { method: 'POST' });
|
const debugQuery = isDebugMode ? '&debug=true' : '';
|
||||||
|
const res = await fetch(`/api/hotels/${hotel.id}/scan?mode=stammdaten${debugQuery}`, { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.message || (data.success ? 'Stammdaten-Scan gestartet!' : 'Fehler beim Starten'));
|
alert(data.message || (data.success ? 'Stammdaten-Scan gestartet!' : 'Fehler beim Starten'));
|
||||||
setTimeout(loadData, 4000);
|
setTimeout(loadData, 4000);
|
||||||
@@ -315,6 +343,62 @@ export default function TravelDashboard() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteHotel = async (hotel: Hotel) => {
|
||||||
|
const isConfirmed = confirm(`Möchtest du das Hotel "${hotel.name}" wirklich unwiderruflich löschen?`);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
|
||||||
|
setDeletingHotelId(hotel.id);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/hotels/${hotel.id}`, { method: 'DELETE' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
if (selectedDetailHotel?.id === hotel.id) {
|
||||||
|
setSelectedDetailHotel(null);
|
||||||
|
}
|
||||||
|
setHotels(prev => prev.filter(h => h.id !== hotel.id));
|
||||||
|
alert(`Hotel "${hotel.name}" wurde erfolgreich gelöscht.`);
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Löschen: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
alert('Fehler beim Löschen: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
setDeletingHotelId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyHotelToTrip = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!copyHotelTarget || !selectedTargetTripId) return;
|
||||||
|
|
||||||
|
setIsCopyingHotel(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/hotels', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: copyHotelTarget.name,
|
||||||
|
booking_url: copyHotelTarget.booking_url,
|
||||||
|
trip_id: selectedTargetTripId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
const targetTripName = trips.find(t => t.id === selectedTargetTripId)?.name || 'dem Trip';
|
||||||
|
alert(`Hotel "${copyHotelTarget.name}" wurde erfolgreich zu "${targetTripName}" hinzugefügt!`);
|
||||||
|
setCopyHotelTarget(null);
|
||||||
|
setSelectedTargetTripId('');
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Hinzufügen: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
alert('Fehler beim Hinzufügen: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
setIsCopyingHotel(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HANDLERS: MANUELLE BAD-BILDER
|
// HANDLERS: MANUELLE BAD-BILDER
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -743,6 +827,61 @@ export default function TravelDashboard() {
|
|||||||
>
|
>
|
||||||
{scanningAllPrices ? '🔄 Scanne...' : '📊 Preise aktualisieren'}
|
{scanningAllPrices ? '🔄 Scanne...' : '📊 Preise aktualisieren'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* DEBUG SLIDER SWITCH */}
|
||||||
|
<label
|
||||||
|
title="Aktiviert das Speichern von hotel-debug.html und Debug-Screenshots bei Scraper-Jobs"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.55rem',
|
||||||
|
background: isDebugMode ? '#fef3c7' : '#f8fafc',
|
||||||
|
border: isDebugMode ? '1px solid #f59e0b' : '1px solid #cbd5e1',
|
||||||
|
padding: '0.45rem 0.85rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
color: isDebugMode ? '#92400e' : '#475569',
|
||||||
|
userSelect: 'none',
|
||||||
|
boxShadow: isDebugMode ? '0 0 0 1px #f59e0b' : 'none',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>🐛 Debug-Modus</span>
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDebugMode();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '38px',
|
||||||
|
height: '20px',
|
||||||
|
background: isDebugMode ? '#f59e0b' : '#cbd5e1',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
position: 'relative',
|
||||||
|
transition: 'background 0.2s ease',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '16px',
|
||||||
|
height: '16px',
|
||||||
|
background: '#ffffff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '2px',
|
||||||
|
left: isDebugMode ? '20px' : '2px',
|
||||||
|
transition: 'left 0.2s ease',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.25)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.75rem', fontWeight: '800', minWidth: '26px' }}>
|
||||||
|
{isDebugMode ? 'AN' : 'AUS'}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -1244,6 +1383,24 @@ export default function TravelDashboard() {
|
|||||||
>
|
>
|
||||||
{scanningHotelId === hotel.id ? '🔄 Scanne...' : '🤖 KI-Review neu erfassen'}
|
{scanningHotelId === hotel.id ? '🔄 Scanne...' : '🤖 KI-Review neu erfassen'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTargetTripId('');
|
||||||
|
setCopyHotelTarget(hotel);
|
||||||
|
}}
|
||||||
|
style={{ background: '#f8fafc', color: '#0369a1', border: '1px solid #bae6fd', padding: '0.35rem 0.6rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.75rem' }}
|
||||||
|
title="Dieses Hotel in einen anderen Trip kopieren / hinzufügen"
|
||||||
|
>
|
||||||
|
✈️ In Trip kopieren
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteHotel(hotel)}
|
||||||
|
disabled={deletingHotelId === hotel.id}
|
||||||
|
style={{ background: '#fef2f2', color: '#dc2626', border: '1px solid #fecaca', padding: '0.35rem 0.55rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.75rem', opacity: deletingHotelId === hotel.id ? 0.6 : 1 }}
|
||||||
|
title="Hotel unwiderruflich aus der Datenbank löschen"
|
||||||
|
>
|
||||||
|
{deletingHotelId === hotel.id ? '🗑️...' : '🗑️ Löschen'}
|
||||||
|
</button>
|
||||||
<a href={hotel.booking_url} target="_blank" rel="noopener noreferrer" style={{ color: '#2563eb', textDecoration: 'none', fontSize: '0.78rem', marginLeft: 'auto', fontWeight: '700' }}>
|
<a href={hotel.booking_url} target="_blank" rel="noopener noreferrer" style={{ color: '#2563eb', textDecoration: 'none', fontSize: '0.78rem', marginLeft: 'auto', fontWeight: '700' }}>
|
||||||
Booking ↗
|
Booking ↗
|
||||||
</a>
|
</a>
|
||||||
@@ -1294,7 +1451,7 @@ export default function TravelDashboard() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
|
<div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc', flexWrap: 'wrap', gap: '0.8rem' }}>
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: '800', color: '#0f172a' }}>
|
<h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: '800', color: '#0f172a' }}>
|
||||||
{selectedDetailHotel.name}
|
{selectedDetailHotel.name}
|
||||||
@@ -1303,13 +1460,35 @@ export default function TravelDashboard() {
|
|||||||
🔗 Auf Booking.com öffnen ↗
|
🔗 Auf Booking.com öffnen ↗
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setSelectedDetailHotel(null)}
|
type="button"
|
||||||
style={{ background: '#e2e8f0', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem' }}
|
onClick={() => {
|
||||||
>
|
setSelectedTargetTripId('');
|
||||||
✕
|
setCopyHotelTarget(selectedDetailHotel);
|
||||||
</button>
|
}}
|
||||||
|
style={{ background: '#f0f9ff', color: '#0369a1', border: '1px solid #bae6fd', padding: '0.4rem 0.75rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.8rem' }}
|
||||||
|
title="Dieses Hotel in einen anderen Trip kopieren"
|
||||||
|
>
|
||||||
|
✈️ In Trip kopieren
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDeleteHotel(selectedDetailHotel)}
|
||||||
|
disabled={deletingHotelId === selectedDetailHotel.id}
|
||||||
|
style={{ background: '#fef2f2', color: '#dc2626', border: '1px solid #fecaca', padding: '0.4rem 0.75rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.8rem', opacity: deletingHotelId === selectedDetailHotel.id ? 0.6 : 1 }}
|
||||||
|
title="Hotel unwiderruflich löschen"
|
||||||
|
>
|
||||||
|
{deletingHotelId === selectedDetailHotel.id ? '🗑️...' : '🗑️ Löschen'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedDetailHotel(null)}
|
||||||
|
style={{ background: '#e2e8f0', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Navigation mit dynamischer Review-Anzahl */}
|
{/* Tab Navigation mit dynamischer Review-Anzahl */}
|
||||||
@@ -2263,6 +2442,121 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ======================================================== */}
|
||||||
|
{/* MODAL: HOTEL IN ANDEREN TRIP KOPIEREN */}
|
||||||
|
{/* ======================================================== */}
|
||||||
|
{copyHotelTarget && (
|
||||||
|
<div
|
||||||
|
onClick={() => setCopyHotelTarget(null)}
|
||||||
|
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: 10000,
|
||||||
|
padding: '1rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
borderRadius: '16px',
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: '100%',
|
||||||
|
padding: '1.75rem',
|
||||||
|
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.35)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h3 style={{ margin: 0, fontSize: '1.2rem', color: '#0f172a', fontWeight: '800' }}>
|
||||||
|
✈️ Hotel in Trip kopieren
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCopyHotelTarget(null)}
|
||||||
|
style={{ background: '#f1f5f9', border: 'none', borderRadius: '50%', width: '30px', height: '30px', cursor: 'pointer', fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ margin: '0 0 1.25rem 0', color: '#475569', fontSize: '0.9rem', lineHeight: '1.4' }}>
|
||||||
|
Wähle die Ziel-Reise aus, zu der <strong>{copyHotelTarget.name}</strong> hinzugefügt werden soll:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleCopyHotelToTrip}>
|
||||||
|
<div style={{ marginBottom: '1.25rem' }}>
|
||||||
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: '700', color: '#334155', marginBottom: '0.4rem' }}>
|
||||||
|
Ziel-Reise / Trip *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedTargetTripId}
|
||||||
|
onChange={(e) => setSelectedTargetTripId(e.target.value)}
|
||||||
|
required
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.65rem 0.8rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #cbd5e1',
|
||||||
|
fontSize: '0.95rem',
|
||||||
|
background: '#ffffff',
|
||||||
|
color: '#0f172a'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">-- Bitte Reise wählen --</option>
|
||||||
|
{trips.map(t => (
|
||||||
|
<option key={t.id} value={String(t.id)}>
|
||||||
|
🌍 {t.name} {String(t.id) === String(copyHotelTarget.trip_id) ? '(Bereits in diesem Trip)' : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCopyHotelTarget(null)}
|
||||||
|
style={{
|
||||||
|
background: '#f1f5f9',
|
||||||
|
color: '#475569',
|
||||||
|
border: '1px solid #cbd5e1',
|
||||||
|
padding: '0.6rem 1.1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontWeight: '700',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isCopyingHotel || !selectedTargetTripId}
|
||||||
|
style={{
|
||||||
|
background: '#0284c7',
|
||||||
|
color: '#ffffff',
|
||||||
|
border: 'none',
|
||||||
|
padding: '0.6rem 1.3rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontWeight: '700',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: (isCopyingHotel || !selectedTargetTripId) ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCopyingHotel ? '⏳ Kopiere...' : '✈️ Zu Trip hinzufügen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user