mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
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:
@@ -24,7 +24,7 @@ function escAttr(s) {
|
||||
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user