v2.5.7: Reservation overhaul, Day Detail Panel, i18n, paste support, auto dark mode

BREAKING: Reservations have been completely rebuilt. Existing place-level
reservations are no longer used. All reservations must be re-created via
the Bookings tab. Your trips, places, and other data are unaffected.

Reservation System (rebuilt from scratch):
- Reservations now link to specific day assignments instead of places
- Same place on different days can have independent reservations
- New assignment picker in booking modal (grouped by day, searchable)
- Removed day/place dropdowns from booking form
- Reservation badges in day plan sidebar with type-specific icons
- Reservation details in place inspector (only for selected assignment)
- Reservation summary in day detail panel

Day Detail Panel (new):
- Opens on day click in the sidebar
- Detailed weather: hourly forecast, precipitation, wind, sunrise/sunset
- Historical climate averages for dates beyond 16 days
- Accommodation management with check-in/check-out, confirmation number
- Hotel assignment across multiple days with day range picker
- Reservation overview for the day

Places:
- Places can now be assigned to the same day multiple times
- Start time + end time fields (replaces single time field)
- Map badges show multiple position numbers (e.g. "1 · 4")
- Route optimization fixed for duplicate places
- File attachments during place editing (not just creation)
- Cover image upload during trip creation (not just editing)
- Paste support (Ctrl+V) for images in trip, place, and file forms

Internationalization:
- 200+ hardcoded German strings translated to i18n (EN + DE)
- Server error messages in English
- Category seeds in English for new installations
- All planner, register, photo, packing components translated

UI/UX:
- Auto dark mode (follows system preference, configurable in settings)
- Navbar toggle switches light/dark (overrides auto)
- Sidebar minimize buttons z-index fixed
- Transport mode selector removed from day plan
- CustomSelect supports grouped headers (isHeader option)
- Optimistic updates for day notes (instant feedback)
- Booking cards redesigned with type-colored headers and structured details

Weather:
- Wind speed in mph when using Fahrenheit setting
- Weather description language matches app language

Admin:
- Weather info panel replaces OpenWeatherMap key input
- "Recommended" badge styling updated
This commit is contained in:
Maurice
2026-03-24 20:10:45 +01:00
parent e4607e426c
commit 0497032ed7
67 changed files with 2390 additions and 1322 deletions
+17 -14
View File
@@ -24,7 +24,7 @@ function escAttr(s) {
return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function createPlaceIcon(place, orderNumber, isSelected) {
function createPlaceIcon(place, orderNumbers, isSelected) {
const size = isSelected ? 44 : 36
const borderColor = isSelected ? '#111827' : 'white'
const borderWidth = isSelected ? 3 : 2.5
@@ -34,20 +34,23 @@ function createPlaceIcon(place, orderNumber, isSelected) {
const bgColor = place.category_color || '#6b7280'
const icon = place.category_icon || '📍'
// White semi-transparent number badge (bottom-right), only when orderNumber is set
const badgeHtml = orderNumber != null ? `
<span style="
position:absolute;bottom:-3px;right:-3px;
min-width:18px;height:18px;border-radius:9px;
padding:0 3px;
background:rgba(255,255,255,0.92);
border:1.5px solid rgba(0,0,0,0.18);
// Number badges (bottom-right), supports multiple numbers for duplicate places
let badgeHtml = ''
if (orderNumbers && orderNumbers.length > 0) {
const label = orderNumbers.join(' · ')
badgeHtml = `<span style="
position:absolute;bottom:-4px;right:-4px;
min-width:18px;height:${orderNumbers.length > 1 ? 16 : 18}px;border-radius:${orderNumbers.length > 1 ? 8 : 9}px;
padding:0 ${orderNumbers.length > 1 ? 4 : 3}px;
background:rgba(255,255,255,0.94);
border:1.5px solid rgba(0,0,0,0.15);
box-shadow:0 1px 4px rgba(0,0,0,0.18);
display:flex;align-items:center;justify-content:center;
font-size:9px;font-weight:800;color:#111827;
font-size:${orderNumbers.length > 1 ? 7.5 : 9}px;font-weight:800;color:#111827;
font-family:-apple-system,system-ui,sans-serif;line-height:1;
box-sizing:border-box;
">${orderNumber}</span>` : ''
box-sizing:border-box;white-space:nowrap;
">${label}</span>`
}
if (place.image_url) {
return L.divIcon({
@@ -249,8 +252,8 @@ export function MapView({
{places.map((place) => {
const isSelected = place.id === selectedPlaceId
const resolvedPhotoUrl = place.image_url || (place.google_place_id && photoUrls[place.google_place_id]) || null
const orderNumber = dayOrderMap[place.id] ?? null
const icon = createPlaceIcon({ ...place, image_url: resolvedPhotoUrl }, orderNumber, isSelected)
const orderNumbers = dayOrderMap[place.id] ?? null
const icon = createPlaceIcon({ ...place, image_url: resolvedPhotoUrl }, orderNumbers, isSelected)
return (
<Marker
+15 -12
View File
@@ -9,7 +9,7 @@ const OSRM_BASE = 'https://router.project-osrm.org/route/v1'
*/
export async function calculateRoute(waypoints, profile = 'driving') {
if (!waypoints || waypoints.length < 2) {
throw new Error('Mindestens 2 Wegpunkte erforderlich')
throw new Error('At least 2 waypoints required')
}
const coords = waypoints.map(p => `${p.lng},${p.lat}`).join(';')
@@ -18,13 +18,13 @@ export async function calculateRoute(waypoints, profile = 'driving') {
const response = await fetch(url)
if (!response.ok) {
throw new Error('Route konnte nicht berechnet werden')
throw new Error('Route could not be calculated')
}
const data = await response.json()
if (data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
throw new Error('Keine Route gefunden')
throw new Error('No route found')
}
const route = data.routes[0]
@@ -74,20 +74,23 @@ export function optimizeRoute(places) {
const visited = new Set()
const result = []
let current = valid[0]
visited.add(current.id)
visited.add(0)
result.push(current)
while (result.length < valid.length) {
let nearest = null
let nearestIdx = -1
let minDist = Infinity
for (const place of valid) {
if (visited.has(place.id)) continue
for (let i = 0; i < valid.length; i++) {
if (visited.has(i)) continue
const d = Math.sqrt(
Math.pow(place.lat - current.lat, 2) + Math.pow(place.lng - current.lng, 2)
Math.pow(valid[i].lat - current.lat, 2) + Math.pow(valid[i].lng - current.lng, 2)
)
if (d < minDist) { minDist = d; nearest = place }
if (d < minDist) { minDist = d; nearestIdx = i }
}
if (nearest) { visited.add(nearest.id); result.push(nearest); current = nearest }
if (nearestIdx === -1) break
visited.add(nearestIdx)
current = valid[nearestIdx]
result.push(current)
}
return result
}
@@ -103,7 +106,7 @@ function formatDuration(seconds) {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
if (h > 0) {
return `${h} Std. ${m} Min.`
return `${h} h ${m} min`
}
return `${m} Min.`
return `${m} min`
}