e.currentTarget.style.boxShadow = '0 2px 12px rgba(0,0,0,0.06)'}
+ onMouseLeave={e => e.currentTarget.style.boxShadow = 'none'}
+ >
+ {/* Header */}
+
+
+
+
{confirmed ? t('reservations.confirmed') : t('reservations.pending')}
+
+
+ {t(typeInfo.labelKey)}
+
+
+
+
{r.title}
+ {canEdit && (
+
+ )}
+ {canEdit && (
+
+ )}
+
+
+
+ {/* Body */}
+
+ {/* Date / Time / Code row */}
+ {(hasDate || hasCode) && (
+
1 ? `repeat(${dateCols}, 1fr)` : '1fr' }}>
+ {hasDate && (
+
+
{t('reservations.date')}
+
+ {fmtDate(r.reservation_time)}
+ {r.reservation_end_time && (r.reservation_end_time.includes('T') ? r.reservation_end_time.split('T')[0] : r.reservation_end_time) !== r.reservation_time.split('T')[0] && (
+ <> – {fmtDate(r.reservation_end_time)}>
+ )}
+
+
+ )}
+ {hasTime && (
+
+
{t('reservations.time')}
+
+ {fmtTime(r.reservation_time)}{r.reservation_end_time ? ` – ${r.reservation_end_time.includes('T') ? fmtTime(r.reservation_end_time) : fmtTime(r.reservation_time.split('T')[0] + 'T' + r.reservation_end_time)}` : ''}
+
+
+ )}
+ {hasCode && (
+
+
{t('reservations.confirmationCode')}
+
blurCodes && setCodeRevealed(true)}
+ onMouseLeave={() => blurCodes && setCodeRevealed(false)}
+ onClick={() => blurCodes && setCodeRevealed(v => !v)}
+ style={{
+ ...fieldValueStyle, textAlign: 'center',
+ fontFamily: '"SF Mono", "JetBrains Mono", Menlo, monospace', fontSize: 12.5,
+ filter: blurCodes && !codeRevealed ? 'blur(5px)' : 'none',
+ cursor: blurCodes ? 'pointer' : 'default',
+ transition: 'filter 0.2s',
+ }}
+ >
+ {r.confirmation_number}
+
+
+ )}
+
)}
-
-
-
{t(typeInfo.labelKey)}
-
-
{r.title}
- {canEdit && (
-
+
+ {/* Type-specific metadata */}
+ {(() => {
+ const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {})
+ if (!meta || Object.keys(meta).length === 0) return null
+ const cells: { label: string; value: string }[] = []
+ if (meta.airline) cells.push({ label: t('reservations.meta.airline'), value: meta.airline })
+ if (meta.flight_number) cells.push({ label: t('reservations.meta.flightNumber'), value: meta.flight_number })
+ if (meta.departure_airport) cells.push({ label: t('reservations.meta.from'), value: meta.departure_airport })
+ if (meta.arrival_airport) cells.push({ label: t('reservations.meta.to'), value: meta.arrival_airport })
+ if (meta.train_number) cells.push({ label: t('reservations.meta.trainNumber'), value: meta.train_number })
+ if (meta.platform) cells.push({ label: t('reservations.meta.platform'), value: meta.platform })
+ if (meta.seat) cells.push({ label: t('reservations.meta.seat'), value: meta.seat })
+ if (meta.check_in_time) cells.push({ label: t('reservations.meta.checkIn'), value: fmtTime('2000-01-01T' + meta.check_in_time) })
+ if (meta.check_out_time) cells.push({ label: t('reservations.meta.checkOut'), value: fmtTime('2000-01-01T' + meta.check_out_time) })
+ if (cells.length === 0) return null
+ return (
+
1 ? `repeat(${Math.min(cells.length, 3)}, 1fr)` : '1fr' }}>
+ {cells.map((c, i) => (
+
+
{c.label}
+
{c.value}
+
+ ))}
+
+ )
+ })()}
+
+ {/* Location / Accommodation / Assignment */}
+ {r.location && (
+
+
{t('reservations.locationAddress')}
+
+
+ {r.location}
+
+
)}
- {canEdit && (
-
+ {r.accommodation_name && (
+
+
{t('reservations.meta.linkAccommodation')}
+
+
+ {r.accommodation_name}
+
+
+ )}
+ {linked && (
+
+
{t('reservations.linkAssignment')}
+
+
+
+ {linked.dayTitle || t('dayplan.dayN', { n: linked.dayNumber })} — {linked.placeName}
+ {linked.startTime ? ` · ${linked.startTime}${linked.endTime ? ' – ' + linked.endTime : ''}` : ''}
+
+
+
+ )}
+
+ {/* Notes */}
+ {r.notes && (
+
+
{t('reservations.notes')}
+
{r.notes}
+
+ )}
+
+ {/* Files */}
+ {attachedFiles.length > 0 && (
+
)}
- {/* Details */}
- {(r.reservation_time || r.confirmation_number || r.location || linked || r.metadata) && (
-
- {/* Row 1: Date, Time, Code */}
- {(r.reservation_time || r.confirmation_number) && (
-
- {r.reservation_time && (
-
-
{t('reservations.date')}
-
- {fmtDate(r.reservation_time)}
- {r.reservation_end_time && (r.reservation_end_time.includes('T') ? r.reservation_end_time.split('T')[0] : r.reservation_end_time) !== r.reservation_time.split('T')[0] && (
- <> – {fmtDate(r.reservation_end_time)}>
- )}
-
-
- )}
- {r.reservation_time?.includes('T') && (
-
-
{t('reservations.time')}
-
- {fmtTime(r.reservation_time)}{r.reservation_end_time ? ` – ${r.reservation_end_time.includes('T') ? fmtTime(r.reservation_end_time) : fmtTime(r.reservation_time.split('T')[0] + 'T' + r.reservation_end_time)}` : ''}
-
-
- )}
- {r.confirmation_number && (
-
-
{t('reservations.confirmationCode')}
-
blurCodes && setCodeRevealed(true)}
- onMouseLeave={() => blurCodes && setCodeRevealed(false)}
- onClick={() => blurCodes && setCodeRevealed(v => !v)}
- style={{
- fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', marginTop: 1,
- filter: blurCodes && !codeRevealed ? 'blur(5px)' : 'none',
- cursor: blurCodes ? 'pointer' : 'default',
- transition: 'filter 0.2s',
- }}
- >
- {r.confirmation_number}
-
-
- )}
-
- )}
- {/* Row 1b: Type-specific metadata */}
- {(() => {
- const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {})
- if (!meta || Object.keys(meta).length === 0) return null
- const cells: { label: string; value: string }[] = []
- if (meta.airline) cells.push({ label: t('reservations.meta.airline'), value: meta.airline })
- if (meta.flight_number) cells.push({ label: t('reservations.meta.flightNumber'), value: meta.flight_number })
- if (meta.departure_airport) cells.push({ label: t('reservations.meta.from'), value: meta.departure_airport })
- if (meta.arrival_airport) cells.push({ label: t('reservations.meta.to'), value: meta.arrival_airport })
- if (meta.train_number) cells.push({ label: t('reservations.meta.trainNumber'), value: meta.train_number })
- if (meta.platform) cells.push({ label: t('reservations.meta.platform'), value: meta.platform })
- if (meta.seat) cells.push({ label: t('reservations.meta.seat'), value: meta.seat })
- if (meta.check_in_time) cells.push({ label: t('reservations.meta.checkIn'), value: fmtTime('2000-01-01T' + meta.check_in_time) })
- if (meta.check_out_time) cells.push({ label: t('reservations.meta.checkOut'), value: fmtTime('2000-01-01T' + meta.check_out_time) })
- if (cells.length === 0) return null
- return (
-
- {cells.map((c, i) => (
-
-
{c.label}
-
{c.value}
-
- ))}
-
- )
- })()}
- {/* Row 2: Location + Assignment */}
- {(r.location || linked || r.accommodation_name) && (
-
- {r.location && (
-
-
{t('reservations.locationAddress')}
-
-
- {r.location}
-
-
- )}
- {r.accommodation_name && (
-
-
{t('reservations.meta.linkAccommodation')}
-
-
- {r.accommodation_name}
-
-
- )}
- {linked && (
-
-
{t('reservations.linkAssignment')}
-
-
-
- {linked.dayTitle || t('dayplan.dayN', { n: linked.dayNumber })} — {linked.placeName}
- {linked.startTime ? ` · ${linked.startTime}${linked.endTime ? ' – ' + linked.endTime : ''}` : ''}
-
-
-
- )}
-
- )}
-
- )}
-
- {/* Notes */}
- {r.notes && (
-
-
{t('reservations.notes')}
-
- {r.notes}
-
-
- )}
-
- {/* Files */}
- {attachedFiles.length > 0 && (
-
- )}
- {/* Delete confirmation popup */}
+ {/* Delete confirmation */}
{showDeleteConfirm && ReactDOM.createPortal(
+
- {open &&
{children}
}
+ {open && (
+
+ {children}
+
+ )}
)
}
@@ -353,55 +398,151 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
const canEdit = can('reservation_edit', trip)
const [showHint, setShowHint] = useState(() => !localStorage.getItem('hideReservationHint'))
+ const storageKey = `trek-reservation-filters-${tripId}`
+ const [typeFilters, setTypeFilters] = useState
>(() => {
+ try {
+ const saved = sessionStorage.getItem(storageKey)
+ return saved ? new Set(JSON.parse(saved)) : new Set()
+ } catch { return new Set() }
+ })
+
+ const toggleTypeFilter = (type: string) => {
+ setTypeFilters(prev => {
+ const next = new Set(prev)
+ if (next.has(type)) next.delete(type); else next.add(type)
+ sessionStorage.setItem(storageKey, JSON.stringify([...next]))
+ return next
+ })
+ }
+
const assignmentLookup = useMemo(() => buildAssignmentLookup(days, assignments), [days, assignments])
- const allPending = reservations.filter(r => r.status !== 'confirmed')
- const allConfirmed = reservations.filter(r => r.status === 'confirmed')
- const total = reservations.length
+ const filtered = useMemo(() =>
+ typeFilters.size === 0 ? reservations : reservations.filter(r => typeFilters.has(r.type)),
+ [reservations, typeFilters])
+
+ const allPending = filtered.filter(r => r.status !== 'confirmed')
+ const allConfirmed = filtered.filter(r => r.status === 'confirmed')
+ const total = filtered.length
+
+ const usedTypes = useMemo(() => new Set(reservations.map(r => r.type)), [reservations])
+ const typeCounts = useMemo(() => {
+ const counts: Record = {}
+ for (const r of reservations) counts[r.type] = (counts[r.type] || 0) + 1
+ return counts
+ }, [reservations])
return (
- {/* Header */}
-
-
-
{t('reservations.title')}
-
- {total === 0 ? t('reservations.empty') : t('reservations.summary', { confirmed: allConfirmed.length, pending: allPending.length })}
-
+ {/* Unified toolbar */}
+
+
+
+ {t('reservations.title')}
+
+
+ {reservations.length > 0 && (
+ <>
+
+
+
+ {TYPE_OPTIONS.filter(opt => usedTypes.has(opt.value)).map(opt => {
+ const active = typeFilters.has(opt.value)
+ const Icon = opt.Icon
+ return (
+
+ )
+ })}
+
+ >
+ )}
+
+ {canEdit && (
+
+ )}
- {canEdit && (
-
- )}
{/* Content */}
-
- {total === 0 ? (
+
+ {total === 0 && reservations.length === 0 ? (
{t('reservations.empty')}
{t('reservations.emptyHint')}
+ ) : total === 0 ? (
+
+
{t('places.noneFound')}
+
) : (
<>
{allPending.length > 0 && (
-
- {allPending.map(r => )}
-
+ {allPending.map(r => )}
)}
{allConfirmed.length > 0 && (
-
- {allConfirmed.map(r => )}
-
+ {allConfirmed.map(r => )}
)}
>
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx
index b12c818f..07be4f66 100644
--- a/client/src/pages/TripPlannerPage.tsx
+++ b/client/src/pages/TripPlannerPage.tsx
@@ -910,7 +910,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
)}
{activeTab === 'buchungen' && (
-