feat(ui): add debug mode toggle, delete hotel button, and copy hotel to trip modal

This commit is contained in:
2026-08-21 21:54:14 +00:00
parent f3b4f1b661
commit dea0e1ad88

View File

@@ -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,14 +1460,36 @@ export default function TravelDashboard() {
🔗 Auf Booking.com öffnen ↗ 🔗 Auf Booking.com öffnen ↗
</a> </a>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<button
type="button"
onClick={() => {
setSelectedTargetTripId('');
setCopyHotelTarget(selectedDetailHotel);
}}
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 <button
type="button" type="button"
onClick={() => setSelectedDetailHotel(null)} onClick={() => setSelectedDetailHotel(null)}
style={{ background: '#e2e8f0', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem' }} style={{ background: '#e2e8f0', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
> >
✕ ✕
</button> </button>
</div> </div>
</div>
{/* Tab Navigation mit dynamischer Review-Anzahl */} {/* Tab Navigation mit dynamischer Review-Anzahl */}
<div style={{ display: 'flex', gap: '1rem', borderBottom: '2px solid #e2e8f0', padding: '0 1.5rem', background: '#f8fafc' }}> <div style={{ display: 'flex', gap: '1rem', borderBottom: '2px solid #e2e8f0', padding: '0 1.5rem', background: '#f8fafc' }}>
@@ -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>
); );
} }