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
+6 -6
View File
@@ -26,7 +26,7 @@ export const useAuthStore = create((set, get) => ({
connect(data.token)
return data
} catch (err) {
const error = err.response?.data?.error || 'Anmeldung fehlgeschlagen'
const error = err.response?.data?.error || 'Login failed'
set({ isLoading: false, error })
throw new Error(error)
}
@@ -47,7 +47,7 @@ export const useAuthStore = create((set, get) => ({
connect(data.token)
return data
} catch (err) {
const error = err.response?.data?.error || 'Registrierung fehlgeschlagen'
const error = err.response?.data?.error || 'Registration failed'
set({ isLoading: false, error })
throw new Error(error)
}
@@ -97,7 +97,7 @@ export const useAuthStore = create((set, get) => ({
user: { ...state.user, maps_api_key: key || null }
}))
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Speichern des API-Schlüssels')
throw new Error(err.response?.data?.error || 'Error saving API key')
}
},
@@ -106,7 +106,7 @@ export const useAuthStore = create((set, get) => ({
const data = await authApi.updateApiKeys(keys)
set({ user: data.user })
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Speichern der API-Schlüssel')
throw new Error(err.response?.data?.error || 'Error saving API keys')
}
},
@@ -115,7 +115,7 @@ export const useAuthStore = create((set, get) => ({
const data = await authApi.updateSettings(profileData)
set({ user: data.user })
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Profils')
throw new Error(err.response?.data?.error || 'Error updating profile')
}
},
@@ -156,7 +156,7 @@ export const useAuthStore = create((set, get) => ({
connect(data.token)
return data
} catch (err) {
const error = err.response?.data?.error || 'Demo-Login fehlgeschlagen'
const error = err.response?.data?.error || 'Demo login failed'
set({ isLoading: false, error })
throw new Error(error)
}
+2 -2
View File
@@ -38,7 +38,7 @@ export const useSettingsStore = create((set, get) => ({
await settingsApi.set(key, value)
} catch (err) {
console.error('Failed to save setting:', err)
throw new Error(err.response?.data?.error || 'Fehler beim Speichern der Einstellung')
throw new Error(err.response?.data?.error || 'Error saving setting')
}
},
@@ -55,7 +55,7 @@ export const useSettingsStore = create((set, get) => ({
await settingsApi.setBulk(settingsObj)
} catch (err) {
console.error('Failed to save settings:', err)
throw new Error(err.response?.data?.error || 'Fehler beim Speichern der Einstellungen')
throw new Error(err.response?.data?.error || 'Error saving settings')
}
},
}))
+58 -34
View File
@@ -76,6 +76,17 @@ export const useTripStore = create((set, get) => ({
}
}
}
case 'assignment:updated': {
const dayKey = String(payload.assignment.day_id)
return {
assignments: {
...state.assignments,
[dayKey]: (state.assignments[dayKey] || []).map(a =>
a.id === payload.assignment.id ? { ...a, ...payload.assignment } : a
),
}
}
}
case 'assignment:deleted': {
const dayKey = String(payload.dayId)
return {
@@ -279,7 +290,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ places: [data.place, ...state.places] }))
return data.place
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Hinzufügen des Ortes')
throw new Error(err.response?.data?.error || 'Error adding place')
}
},
@@ -297,7 +308,7 @@ export const useTripStore = create((set, get) => ({
}))
return data.place
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Ortes')
throw new Error(err.response?.data?.error || 'Error updating place')
}
},
@@ -314,7 +325,7 @@ export const useTripStore = create((set, get) => ({
),
}))
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Löschen des Ortes')
throw new Error(err.response?.data?.error || 'Error deleting place')
}
},
@@ -323,9 +334,6 @@ export const useTripStore = create((set, get) => ({
const place = state.places.find(p => p.id === parseInt(placeId))
if (!place) return
const existing = (state.assignments[String(dayId)] || []).find(a => a.place?.id === parseInt(placeId))
if (existing) return
const tempId = Date.now() * -1
const current = [...(state.assignments[String(dayId)] || [])]
const insertIdx = position != null ? position : current.length
@@ -347,9 +355,11 @@ export const useTripStore = create((set, get) => ({
try {
const data = await assignmentsApi.create(tripId, dayId, { place_id: placeId })
const newAssignment = position != null
? { ...data.assignment, order_index: insertIdx }
: data.assignment
const newAssignment = {
...data.assignment,
place: data.assignment.place || place,
order_index: position != null ? insertIdx : data.assignment.order_index,
}
set(state => ({
assignments: {
...state.assignments,
@@ -390,7 +400,7 @@ export const useTripStore = create((set, get) => ({
[String(dayId)]: state.assignments[String(dayId)].filter(a => a.id !== tempId),
}
}))
throw new Error(err.response?.data?.error || 'Fehler beim Zuweisen des Ortes')
throw new Error(err.response?.data?.error || 'Error assigning place')
}
},
@@ -408,7 +418,7 @@ export const useTripStore = create((set, get) => ({
await assignmentsApi.delete(tripId, dayId, assignmentId)
} catch (err) {
set({ assignments: prevAssignments })
throw new Error(err.response?.data?.error || 'Fehler beim Entfernen der Zuweisung')
throw new Error(err.response?.data?.error || 'Error removing assignment')
}
},
@@ -431,7 +441,7 @@ export const useTripStore = create((set, get) => ({
await assignmentsApi.reorder(tripId, dayId, orderedIds)
} catch (err) {
set({ assignments: prevAssignments })
throw new Error(err.response?.data?.error || 'Fehler beim Neuanordnen')
throw new Error(err.response?.data?.error || 'Error reordering')
}
},
@@ -464,7 +474,7 @@ export const useTripStore = create((set, get) => ({
}
} catch (err) {
set({ assignments: prevAssignments })
throw new Error(err.response?.data?.error || 'Fehler beim Verschieben der Zuweisung')
throw new Error(err.response?.data?.error || 'Error moving assignment')
}
},
@@ -498,7 +508,7 @@ export const useTripStore = create((set, get) => ({
[String(fromDayId)]: [...(s.dayNotes[String(fromDayId)] || []), note],
}
}))
throw new Error(err.response?.data?.error || 'Fehler beim Verschieben der Notiz')
throw new Error(err.response?.data?.error || 'Error moving note')
}
},
@@ -512,7 +522,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ packingItems: [...state.packingItems, result.item] }))
return result.item
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Hinzufügen des Artikels')
throw new Error(err.response?.data?.error || 'Error adding item')
}
},
@@ -524,7 +534,7 @@ export const useTripStore = create((set, get) => ({
}))
return result.item
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Artikels')
throw new Error(err.response?.data?.error || 'Error updating item')
}
},
@@ -535,7 +545,7 @@ export const useTripStore = create((set, get) => ({
await packingApi.delete(tripId, id)
} catch (err) {
set({ packingItems: prev })
throw new Error(err.response?.data?.error || 'Fehler beim Löschen des Artikels')
throw new Error(err.response?.data?.error || 'Error deleting item')
}
},
@@ -563,7 +573,7 @@ export const useTripStore = create((set, get) => ({
days: state.days.map(d => d.id === parseInt(dayId) ? { ...d, notes } : d)
}))
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren der Notizen')
throw new Error(err.response?.data?.error || 'Error updating notes')
}
},
@@ -574,7 +584,7 @@ export const useTripStore = create((set, get) => ({
days: state.days.map(d => d.id === parseInt(dayId) ? { ...d, title } : d)
}))
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Tagesnamens')
throw new Error(err.response?.data?.error || 'Error updating day name')
}
},
@@ -584,7 +594,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ tags: [...state.tags, result.tag] }))
return result.tag
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Erstellen des Tags')
throw new Error(err.response?.data?.error || 'Error creating tag')
}
},
@@ -594,7 +604,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ categories: [...state.categories, result.category] }))
return result.category
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Erstellen der Kategorie')
throw new Error(err.response?.data?.error || 'Error creating category')
}
},
@@ -612,7 +622,7 @@ export const useTripStore = create((set, get) => ({
set({ days: daysData.days, assignments: assignmentsMap, dayNotes: dayNotesMap })
return result.trip
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren der Reise')
throw new Error(err.response?.data?.error || 'Error updating trip')
}
},
@@ -631,7 +641,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ budgetItems: [...state.budgetItems, result.item] }))
return result.item
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Hinzufügen des Budget-Eintrags')
throw new Error(err.response?.data?.error || 'Error adding budget item')
}
},
@@ -643,7 +653,7 @@ export const useTripStore = create((set, get) => ({
}))
return result.item
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Budget-Eintrags')
throw new Error(err.response?.data?.error || 'Error updating budget item')
}
},
@@ -654,7 +664,7 @@ export const useTripStore = create((set, get) => ({
await budgetApi.delete(tripId, id)
} catch (err) {
set({ budgetItems: prev })
throw new Error(err.response?.data?.error || 'Fehler beim Löschen des Budget-Eintrags')
throw new Error(err.response?.data?.error || 'Error deleting budget item')
}
},
@@ -673,7 +683,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ files: [data.file, ...state.files] }))
return data.file
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Hochladen der Datei')
throw new Error(err.response?.data?.error || 'Error uploading file')
}
},
@@ -682,7 +692,7 @@ export const useTripStore = create((set, get) => ({
await filesApi.delete(tripId, id)
set(state => ({ files: state.files.filter(f => f.id !== id) }))
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Löschen der Datei')
throw new Error(err.response?.data?.error || 'Error deleting file')
}
},
@@ -701,7 +711,7 @@ export const useTripStore = create((set, get) => ({
set(state => ({ reservations: [result.reservation, ...state.reservations] }))
return result.reservation
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Erstellen der Reservierung')
throw new Error(err.response?.data?.error || 'Error creating reservation')
}
},
@@ -713,7 +723,7 @@ export const useTripStore = create((set, get) => ({
}))
return result.reservation
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren der Reservierung')
throw new Error(err.response?.data?.error || 'Error updating reservation')
}
},
@@ -737,22 +747,36 @@ export const useTripStore = create((set, get) => ({
await reservationsApi.delete(tripId, id)
set(state => ({ reservations: state.reservations.filter(r => r.id !== id) }))
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Löschen der Reservierung')
throw new Error(err.response?.data?.error || 'Error deleting reservation')
}
},
addDayNote: async (tripId, dayId, data) => {
const tempId = Date.now() * -1
const tempNote = { id: tempId, day_id: dayId, ...data, created_at: new Date().toISOString() }
set(state => ({
dayNotes: {
...state.dayNotes,
[String(dayId)]: [...(state.dayNotes[String(dayId)] || []), tempNote],
}
}))
try {
const result = await dayNotesApi.create(tripId, dayId, data)
set(state => ({
dayNotes: {
...state.dayNotes,
[String(dayId)]: [...(state.dayNotes[String(dayId)] || []), result.note],
[String(dayId)]: (state.dayNotes[String(dayId)] || []).map(n => n.id === tempId ? result.note : n),
}
}))
return result.note
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Hinzufügen der Notiz')
set(state => ({
dayNotes: {
...state.dayNotes,
[String(dayId)]: (state.dayNotes[String(dayId)] || []).filter(n => n.id !== tempId),
}
}))
throw new Error(err.response?.data?.error || 'Error adding note')
}
},
@@ -767,7 +791,7 @@ export const useTripStore = create((set, get) => ({
}))
return result.note
} catch (err) {
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren der Notiz')
throw new Error(err.response?.data?.error || 'Error updating note')
}
},
@@ -783,7 +807,7 @@ export const useTripStore = create((set, get) => ({
await dayNotesApi.delete(tripId, dayId, id)
} catch (err) {
set({ dayNotes: prev })
throw new Error(err.response?.data?.error || 'Fehler beim Löschen der Notiz')
throw new Error(err.response?.data?.error || 'Error deleting note')
}
},
}))