mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
74f19f3312
Real-Time Collaboration (WebSocket): - WebSocket server with JWT auth and trip-based rooms - Live sync for all CRUD operations (places, assignments, days, notes, budget, packing, reservations, files) - Socket-based exclusion to prevent duplicate updates - Auto-reconnect with exponential backoff - Assignment move sync between days Performance: - 16 database indexes on all foreign key columns - N+1 query fix in places, assignments and days endpoints - Marker clustering (react-leaflet-cluster) with configurable radius - List virtualization (react-window) for places sidebar - useMemo for filtered places - SQLite WAL mode + busy_timeout for concurrent writes - Weather API: server-side cache (1h forecast, 15min current) + client sessionStorage - Google Places photos: persisted to DB after first fetch - Google Details: 3-tier cache (memory → sessionStorage → API) Security: - CORS auto-configuration (production: same-origin, dev: open) - API keys removed from /auth/me response - Admin-only endpoint for reading API keys - Path traversal prevention in cover image deletion - JWT secret persisted to file (survives restarts) - Avatar upload file extension whitelist - API key fallback: normal users use admin's key without exposure - Case-insensitive email login Dark Mode: - Fixed hardcoded colors across PackingList, Budget, ReservationModal, ReservationsPanel - Mobile map buttons and sidebar sheets respect dark mode - Cluster markers always dark UI/UX: - Redesigned login page with animated planes, stars and feature cards - Admin: create user functionality with CustomSelect - Mobile: day-picker popup for assigning places to days - Mobile: touch-friendly reorder buttons (32px targets) - Mobile: responsive text (shorter labels on small screens) - Packing list: index-based category colors - i18n: translated date picker placeholder, fixed German labels - Default map tile: CartoDB Light
790 lines
26 KiB
JavaScript
790 lines
26 KiB
JavaScript
import { create } from 'zustand'
|
|
import { tripsApi, daysApi, placesApi, assignmentsApi, packingApi, tagsApi, categoriesApi, budgetApi, filesApi, reservationsApi, dayNotesApi } from '../api/client'
|
|
|
|
export const useTripStore = create((set, get) => ({
|
|
trip: null,
|
|
days: [],
|
|
places: [],
|
|
assignments: {}, // { [dayId]: [assignment objects] }
|
|
dayNotes: {}, // { [dayId]: [note objects] }
|
|
packingItems: [],
|
|
tags: [],
|
|
categories: [],
|
|
budgetItems: [],
|
|
files: [],
|
|
reservations: [],
|
|
selectedDayId: null,
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
setSelectedDay: (dayId) => set({ selectedDayId: dayId }),
|
|
|
|
// Handle remote WebSocket events without making API calls
|
|
handleRemoteEvent: (event) => {
|
|
const { type, ...payload } = event
|
|
|
|
set(state => {
|
|
switch (type) {
|
|
// Places
|
|
case 'place:created':
|
|
if (state.places.some(p => p.id === payload.place.id)) return {}
|
|
return { places: [payload.place, ...state.places] }
|
|
case 'place:updated':
|
|
return {
|
|
places: state.places.map(p => p.id === payload.place.id ? payload.place : p),
|
|
assignments: Object.fromEntries(
|
|
Object.entries(state.assignments).map(([dayId, items]) => [
|
|
dayId,
|
|
items.map(a => a.place?.id === payload.place.id ? { ...a, place: payload.place } : a)
|
|
])
|
|
),
|
|
}
|
|
case 'place:deleted':
|
|
return {
|
|
places: state.places.filter(p => p.id !== payload.placeId),
|
|
assignments: Object.fromEntries(
|
|
Object.entries(state.assignments).map(([dayId, items]) => [
|
|
dayId,
|
|
items.filter(a => a.place?.id !== payload.placeId)
|
|
])
|
|
),
|
|
}
|
|
|
|
// Assignments
|
|
case 'assignment:created': {
|
|
const dayKey = String(payload.assignment.day_id)
|
|
const existing = (state.assignments[dayKey] || [])
|
|
// Skip if already present (by id OR by place_id to handle optimistic temp ids)
|
|
const placeId = payload.assignment.place?.id || payload.assignment.place_id
|
|
if (existing.some(a => a.id === payload.assignment.id || (placeId && a.place?.id === placeId))) {
|
|
// Replace temp entry with server version if needed
|
|
const hasTempVersion = existing.some(a => a.id < 0 && a.place?.id === placeId)
|
|
if (hasTempVersion) {
|
|
return {
|
|
assignments: {
|
|
...state.assignments,
|
|
[dayKey]: existing.map(a => (a.id < 0 && a.place?.id === placeId) ? payload.assignment : a),
|
|
}
|
|
}
|
|
}
|
|
return {}
|
|
}
|
|
return {
|
|
assignments: {
|
|
...state.assignments,
|
|
[dayKey]: [...existing, payload.assignment],
|
|
}
|
|
}
|
|
}
|
|
case 'assignment:deleted': {
|
|
const dayKey = String(payload.dayId)
|
|
return {
|
|
assignments: {
|
|
...state.assignments,
|
|
[dayKey]: (state.assignments[dayKey] || []).filter(a => a.id !== payload.assignmentId),
|
|
}
|
|
}
|
|
}
|
|
case 'assignment:moved': {
|
|
const oldKey = String(payload.oldDayId)
|
|
const newKey = String(payload.newDayId)
|
|
const movedAssignment = payload.assignment
|
|
return {
|
|
assignments: {
|
|
...state.assignments,
|
|
[oldKey]: (state.assignments[oldKey] || []).filter(a => a.id !== movedAssignment.id),
|
|
[newKey]: [...(state.assignments[newKey] || []).filter(a => a.id !== movedAssignment.id), movedAssignment],
|
|
}
|
|
}
|
|
}
|
|
case 'assignment:reordered': {
|
|
const dayKey = String(payload.dayId)
|
|
const currentItems = state.assignments[dayKey] || []
|
|
const orderedIds = payload.orderedIds || []
|
|
const reordered = orderedIds.map((id, idx) => {
|
|
const item = currentItems.find(a => a.id === id)
|
|
return item ? { ...item, order_index: idx } : null
|
|
}).filter(Boolean)
|
|
return {
|
|
assignments: {
|
|
...state.assignments,
|
|
[dayKey]: reordered,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Days
|
|
case 'day:created':
|
|
if (state.days.some(d => d.id === payload.day.id)) return {}
|
|
return { days: [...state.days, payload.day] }
|
|
case 'day:updated':
|
|
return {
|
|
days: state.days.map(d => d.id === payload.day.id ? payload.day : d),
|
|
}
|
|
case 'day:deleted': {
|
|
const removedDayId = String(payload.dayId)
|
|
const newAssignments = { ...state.assignments }
|
|
delete newAssignments[removedDayId]
|
|
const newDayNotes = { ...state.dayNotes }
|
|
delete newDayNotes[removedDayId]
|
|
return {
|
|
days: state.days.filter(d => d.id !== payload.dayId),
|
|
assignments: newAssignments,
|
|
dayNotes: newDayNotes,
|
|
}
|
|
}
|
|
|
|
// Day Notes
|
|
case 'dayNote:created': {
|
|
const dayKey = String(payload.dayId)
|
|
const existingNotes = (state.dayNotes[dayKey] || [])
|
|
if (existingNotes.some(n => n.id === payload.note.id)) return {}
|
|
return {
|
|
dayNotes: {
|
|
...state.dayNotes,
|
|
[dayKey]: [...existingNotes, payload.note],
|
|
}
|
|
}
|
|
}
|
|
case 'dayNote:updated': {
|
|
const dayKey = String(payload.dayId)
|
|
return {
|
|
dayNotes: {
|
|
...state.dayNotes,
|
|
[dayKey]: (state.dayNotes[dayKey] || []).map(n => n.id === payload.note.id ? payload.note : n),
|
|
}
|
|
}
|
|
}
|
|
case 'dayNote:deleted': {
|
|
const dayKey = String(payload.dayId)
|
|
return {
|
|
dayNotes: {
|
|
...state.dayNotes,
|
|
[dayKey]: (state.dayNotes[dayKey] || []).filter(n => n.id !== payload.noteId),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Packing
|
|
case 'packing:created':
|
|
if (state.packingItems.some(i => i.id === payload.item.id)) return {}
|
|
return { packingItems: [...state.packingItems, payload.item] }
|
|
case 'packing:updated':
|
|
return {
|
|
packingItems: state.packingItems.map(i => i.id === payload.item.id ? payload.item : i),
|
|
}
|
|
case 'packing:deleted':
|
|
return {
|
|
packingItems: state.packingItems.filter(i => i.id !== payload.itemId),
|
|
}
|
|
|
|
// Budget
|
|
case 'budget:created':
|
|
if (state.budgetItems.some(i => i.id === payload.item.id)) return {}
|
|
return { budgetItems: [...state.budgetItems, payload.item] }
|
|
case 'budget:updated':
|
|
return {
|
|
budgetItems: state.budgetItems.map(i => i.id === payload.item.id ? payload.item : i),
|
|
}
|
|
case 'budget:deleted':
|
|
return {
|
|
budgetItems: state.budgetItems.filter(i => i.id !== payload.itemId),
|
|
}
|
|
|
|
// Reservations
|
|
case 'reservation:created':
|
|
if (state.reservations.some(r => r.id === payload.reservation.id)) return {}
|
|
return { reservations: [payload.reservation, ...state.reservations] }
|
|
case 'reservation:updated':
|
|
return {
|
|
reservations: state.reservations.map(r => r.id === payload.reservation.id ? payload.reservation : r),
|
|
}
|
|
case 'reservation:deleted':
|
|
return {
|
|
reservations: state.reservations.filter(r => r.id !== payload.reservationId),
|
|
}
|
|
|
|
// Trip
|
|
case 'trip:updated':
|
|
return { trip: payload.trip }
|
|
|
|
// Files
|
|
case 'file:created':
|
|
if (state.files.some(f => f.id === payload.file.id)) return {}
|
|
return { files: [payload.file, ...state.files] }
|
|
case 'file:updated':
|
|
return {
|
|
files: state.files.map(f => f.id === payload.file.id ? payload.file : f),
|
|
}
|
|
case 'file:deleted':
|
|
return {
|
|
files: state.files.filter(f => f.id !== payload.fileId),
|
|
}
|
|
|
|
default:
|
|
return {}
|
|
}
|
|
})
|
|
},
|
|
|
|
// Load everything for a trip
|
|
loadTrip: async (tripId) => {
|
|
set({ isLoading: true, error: null })
|
|
try {
|
|
const [tripData, daysData, placesData, packingData, tagsData, categoriesData] = await Promise.all([
|
|
tripsApi.get(tripId),
|
|
daysApi.list(tripId),
|
|
placesApi.list(tripId),
|
|
packingApi.list(tripId),
|
|
tagsApi.list(),
|
|
categoriesApi.list(),
|
|
])
|
|
|
|
const assignmentsMap = {}
|
|
const dayNotesMap = {}
|
|
for (const day of daysData.days) {
|
|
assignmentsMap[String(day.id)] = day.assignments || []
|
|
dayNotesMap[String(day.id)] = day.notes_items || []
|
|
}
|
|
|
|
set({
|
|
trip: tripData.trip,
|
|
days: daysData.days,
|
|
places: placesData.places,
|
|
assignments: assignmentsMap,
|
|
dayNotes: dayNotesMap,
|
|
packingItems: packingData.items,
|
|
tags: tagsData.tags,
|
|
categories: categoriesData.categories,
|
|
isLoading: false,
|
|
})
|
|
} catch (err) {
|
|
set({ isLoading: false, error: err.message })
|
|
throw err
|
|
}
|
|
},
|
|
|
|
refreshPlaces: async (tripId) => {
|
|
try {
|
|
const data = await placesApi.list(tripId)
|
|
set({ places: data.places })
|
|
} catch (err) {
|
|
console.error('Failed to refresh places:', err)
|
|
}
|
|
},
|
|
|
|
addPlace: async (tripId, placeData) => {
|
|
try {
|
|
const data = await placesApi.create(tripId, placeData)
|
|
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')
|
|
}
|
|
},
|
|
|
|
updatePlace: async (tripId, placeId, placeData) => {
|
|
try {
|
|
const data = await placesApi.update(tripId, placeId, placeData)
|
|
set(state => ({
|
|
places: state.places.map(p => p.id === placeId ? data.place : p),
|
|
assignments: Object.fromEntries(
|
|
Object.entries(state.assignments).map(([dayId, items]) => [
|
|
dayId,
|
|
items.map(a => a.place?.id === placeId ? { ...a, place: data.place } : a)
|
|
])
|
|
),
|
|
}))
|
|
return data.place
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Ortes')
|
|
}
|
|
},
|
|
|
|
deletePlace: async (tripId, placeId) => {
|
|
try {
|
|
await placesApi.delete(tripId, placeId)
|
|
set(state => ({
|
|
places: state.places.filter(p => p.id !== placeId),
|
|
assignments: Object.fromEntries(
|
|
Object.entries(state.assignments).map(([dayId, items]) => [
|
|
dayId,
|
|
items.filter(a => a.place?.id !== placeId)
|
|
])
|
|
),
|
|
}))
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Löschen des Ortes')
|
|
}
|
|
},
|
|
|
|
assignPlaceToDay: async (tripId, dayId, placeId, position) => {
|
|
const state = 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
|
|
const tempAssignment = {
|
|
id: tempId,
|
|
day_id: parseInt(dayId),
|
|
order_index: insertIdx,
|
|
notes: null,
|
|
place,
|
|
}
|
|
|
|
current.splice(insertIdx, 0, tempAssignment)
|
|
set(state => ({
|
|
assignments: {
|
|
...state.assignments,
|
|
[String(dayId)]: current,
|
|
}
|
|
}))
|
|
|
|
try {
|
|
const data = await assignmentsApi.create(tripId, dayId, { place_id: placeId })
|
|
const newAssignment = position != null
|
|
? { ...data.assignment, order_index: insertIdx }
|
|
: data.assignment
|
|
set(state => ({
|
|
assignments: {
|
|
...state.assignments,
|
|
[String(dayId)]: state.assignments[String(dayId)].map(
|
|
a => a.id === tempId ? newAssignment : a
|
|
),
|
|
}
|
|
}))
|
|
// Reihenfolge am Server aktualisieren und lokalen State mit Server-Antwort synchronisieren
|
|
if (position != null) {
|
|
const updated = get().assignments[String(dayId)] || []
|
|
const orderedIds = updated.map(a => a.id).filter(id => id > 0)
|
|
if (orderedIds.length > 0) {
|
|
try {
|
|
await assignmentsApi.reorder(tripId, dayId, orderedIds)
|
|
// Lokalen State auf die gesendete Reihenfolge setzen
|
|
set(state => {
|
|
const items = state.assignments[String(dayId)] || []
|
|
const reordered = orderedIds.map((id, idx) => {
|
|
const item = items.find(a => a.id === id)
|
|
return item ? { ...item, order_index: idx } : null
|
|
}).filter(Boolean)
|
|
return {
|
|
assignments: {
|
|
...state.assignments,
|
|
[String(dayId)]: reordered,
|
|
}
|
|
}
|
|
})
|
|
} catch {}
|
|
}
|
|
}
|
|
return data.assignment
|
|
} catch (err) {
|
|
set(state => ({
|
|
assignments: {
|
|
...state.assignments,
|
|
[String(dayId)]: state.assignments[String(dayId)].filter(a => a.id !== tempId),
|
|
}
|
|
}))
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Zuweisen des Ortes')
|
|
}
|
|
},
|
|
|
|
removeAssignment: async (tripId, dayId, assignmentId) => {
|
|
const prevAssignments = get().assignments
|
|
|
|
set(state => ({
|
|
assignments: {
|
|
...state.assignments,
|
|
[String(dayId)]: state.assignments[String(dayId)].filter(a => a.id !== assignmentId),
|
|
}
|
|
}))
|
|
|
|
try {
|
|
await assignmentsApi.delete(tripId, dayId, assignmentId)
|
|
} catch (err) {
|
|
set({ assignments: prevAssignments })
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Entfernen der Zuweisung')
|
|
}
|
|
},
|
|
|
|
reorderAssignments: async (tripId, dayId, orderedIds) => {
|
|
const prevAssignments = get().assignments
|
|
const dayItems = get().assignments[String(dayId)] || []
|
|
const reordered = orderedIds.map((id, idx) => {
|
|
const item = dayItems.find(a => a.id === id)
|
|
return item ? { ...item, order_index: idx } : null
|
|
}).filter(Boolean)
|
|
|
|
set(state => ({
|
|
assignments: {
|
|
...state.assignments,
|
|
[String(dayId)]: reordered,
|
|
}
|
|
}))
|
|
|
|
try {
|
|
await assignmentsApi.reorder(tripId, dayId, orderedIds)
|
|
} catch (err) {
|
|
set({ assignments: prevAssignments })
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Neuanordnen')
|
|
}
|
|
},
|
|
|
|
moveAssignment: async (tripId, assignmentId, fromDayId, toDayId, toOrderIndex = null) => {
|
|
const state = get()
|
|
const prevAssignments = state.assignments
|
|
const assignment = (state.assignments[String(fromDayId)] || []).find(a => a.id === assignmentId)
|
|
if (!assignment) return
|
|
|
|
const toItems = (state.assignments[String(toDayId)] || []).slice().sort((a, b) => a.order_index - b.order_index)
|
|
const insertAt = toOrderIndex !== null ? toOrderIndex : toItems.length
|
|
|
|
// Build new order for target day with item inserted at correct position
|
|
const newToItems = [...toItems]
|
|
newToItems.splice(insertAt, 0, { ...assignment, day_id: parseInt(toDayId) })
|
|
newToItems.forEach((a, i) => { a.order_index = i })
|
|
|
|
set(s => ({
|
|
assignments: {
|
|
...s.assignments,
|
|
[String(fromDayId)]: s.assignments[String(fromDayId)].filter(a => a.id !== assignmentId),
|
|
[String(toDayId)]: newToItems,
|
|
}
|
|
}))
|
|
|
|
try {
|
|
await assignmentsApi.move(tripId, assignmentId, toDayId, insertAt)
|
|
if (newToItems.length > 1) {
|
|
await assignmentsApi.reorder(tripId, toDayId, newToItems.map(a => a.id))
|
|
}
|
|
} catch (err) {
|
|
set({ assignments: prevAssignments })
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Verschieben der Zuweisung')
|
|
}
|
|
},
|
|
|
|
moveDayNote: async (tripId, fromDayId, toDayId, noteId, sort_order = 9999) => {
|
|
const state = get()
|
|
const note = (state.dayNotes[String(fromDayId)] || []).find(n => n.id === noteId)
|
|
if (!note) return
|
|
|
|
set(s => ({
|
|
dayNotes: {
|
|
...s.dayNotes,
|
|
[String(fromDayId)]: (s.dayNotes[String(fromDayId)] || []).filter(n => n.id !== noteId),
|
|
}
|
|
}))
|
|
|
|
try {
|
|
await dayNotesApi.delete(tripId, fromDayId, noteId)
|
|
const result = await dayNotesApi.create(tripId, toDayId, {
|
|
text: note.text, time: note.time, icon: note.icon, sort_order,
|
|
})
|
|
set(s => ({
|
|
dayNotes: {
|
|
...s.dayNotes,
|
|
[String(toDayId)]: [...(s.dayNotes[String(toDayId)] || []), result.note],
|
|
}
|
|
}))
|
|
} catch (err) {
|
|
set(s => ({
|
|
dayNotes: {
|
|
...s.dayNotes,
|
|
[String(fromDayId)]: [...(s.dayNotes[String(fromDayId)] || []), note],
|
|
}
|
|
}))
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Verschieben der Notiz')
|
|
}
|
|
},
|
|
|
|
setAssignments: (assignments) => {
|
|
set({ assignments })
|
|
},
|
|
|
|
addPackingItem: async (tripId, data) => {
|
|
try {
|
|
const result = await packingApi.create(tripId, data)
|
|
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')
|
|
}
|
|
},
|
|
|
|
updatePackingItem: async (tripId, id, data) => {
|
|
try {
|
|
const result = await packingApi.update(tripId, id, data)
|
|
set(state => ({
|
|
packingItems: state.packingItems.map(item => item.id === id ? result.item : item)
|
|
}))
|
|
return result.item
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Artikels')
|
|
}
|
|
},
|
|
|
|
deletePackingItem: async (tripId, id) => {
|
|
const prev = get().packingItems
|
|
set(state => ({ packingItems: state.packingItems.filter(item => item.id !== id) }))
|
|
try {
|
|
await packingApi.delete(tripId, id)
|
|
} catch (err) {
|
|
set({ packingItems: prev })
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Löschen des Artikels')
|
|
}
|
|
},
|
|
|
|
togglePackingItem: async (tripId, id, checked) => {
|
|
set(state => ({
|
|
packingItems: state.packingItems.map(item =>
|
|
item.id === id ? { ...item, checked: checked ? 1 : 0 } : item
|
|
)
|
|
}))
|
|
try {
|
|
await packingApi.update(tripId, id, { checked })
|
|
} catch (err) {
|
|
set(state => ({
|
|
packingItems: state.packingItems.map(item =>
|
|
item.id === id ? { ...item, checked: checked ? 0 : 1 } : item
|
|
)
|
|
}))
|
|
}
|
|
},
|
|
|
|
updateDayNotes: async (tripId, dayId, notes) => {
|
|
try {
|
|
await daysApi.update(tripId, dayId, { notes })
|
|
set(state => ({
|
|
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')
|
|
}
|
|
},
|
|
|
|
updateDayTitle: async (tripId, dayId, title) => {
|
|
try {
|
|
await daysApi.update(tripId, dayId, { title })
|
|
set(state => ({
|
|
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')
|
|
}
|
|
},
|
|
|
|
addTag: async (data) => {
|
|
try {
|
|
const result = await tagsApi.create(data)
|
|
set(state => ({ tags: [...state.tags, result.tag] }))
|
|
return result.tag
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Erstellen des Tags')
|
|
}
|
|
},
|
|
|
|
addCategory: async (data) => {
|
|
try {
|
|
const result = await categoriesApi.create(data)
|
|
set(state => ({ categories: [...state.categories, result.category] }))
|
|
return result.category
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Erstellen der Kategorie')
|
|
}
|
|
},
|
|
|
|
updateTrip: async (tripId, data) => {
|
|
try {
|
|
const result = await tripsApi.update(tripId, data)
|
|
set({ trip: result.trip })
|
|
const daysData = await daysApi.list(tripId)
|
|
const assignmentsMap = {}
|
|
const dayNotesMap = {}
|
|
for (const day of daysData.days) {
|
|
assignmentsMap[String(day.id)] = day.assignments || []
|
|
dayNotesMap[String(day.id)] = day.notes_items || []
|
|
}
|
|
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')
|
|
}
|
|
},
|
|
|
|
loadBudgetItems: async (tripId) => {
|
|
try {
|
|
const data = await budgetApi.list(tripId)
|
|
set({ budgetItems: data.items })
|
|
} catch (err) {
|
|
console.error('Failed to load budget items:', err)
|
|
}
|
|
},
|
|
|
|
addBudgetItem: async (tripId, data) => {
|
|
try {
|
|
const result = await budgetApi.create(tripId, data)
|
|
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')
|
|
}
|
|
},
|
|
|
|
updateBudgetItem: async (tripId, id, data) => {
|
|
try {
|
|
const result = await budgetApi.update(tripId, id, data)
|
|
set(state => ({
|
|
budgetItems: state.budgetItems.map(item => item.id === id ? result.item : item)
|
|
}))
|
|
return result.item
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren des Budget-Eintrags')
|
|
}
|
|
},
|
|
|
|
deleteBudgetItem: async (tripId, id) => {
|
|
const prev = get().budgetItems
|
|
set(state => ({ budgetItems: state.budgetItems.filter(item => item.id !== id) }))
|
|
try {
|
|
await budgetApi.delete(tripId, id)
|
|
} catch (err) {
|
|
set({ budgetItems: prev })
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Löschen des Budget-Eintrags')
|
|
}
|
|
},
|
|
|
|
loadFiles: async (tripId) => {
|
|
try {
|
|
const data = await filesApi.list(tripId)
|
|
set({ files: data.files })
|
|
} catch (err) {
|
|
console.error('Failed to load files:', err)
|
|
}
|
|
},
|
|
|
|
addFile: async (tripId, formData) => {
|
|
try {
|
|
const data = await filesApi.upload(tripId, formData)
|
|
set(state => ({ files: [data.file, ...state.files] }))
|
|
return data.file
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Hochladen der Datei')
|
|
}
|
|
},
|
|
|
|
deleteFile: async (tripId, id) => {
|
|
try {
|
|
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')
|
|
}
|
|
},
|
|
|
|
loadReservations: async (tripId) => {
|
|
try {
|
|
const data = await reservationsApi.list(tripId)
|
|
set({ reservations: data.reservations })
|
|
} catch (err) {
|
|
console.error('Failed to load reservations:', err)
|
|
}
|
|
},
|
|
|
|
addReservation: async (tripId, data) => {
|
|
try {
|
|
const result = await reservationsApi.create(tripId, data)
|
|
set(state => ({ reservations: [result.reservation, ...state.reservations] }))
|
|
return result.reservation
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Erstellen der Reservierung')
|
|
}
|
|
},
|
|
|
|
updateReservation: async (tripId, id, data) => {
|
|
try {
|
|
const result = await reservationsApi.update(tripId, id, data)
|
|
set(state => ({
|
|
reservations: state.reservations.map(r => r.id === id ? result.reservation : r)
|
|
}))
|
|
return result.reservation
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren der Reservierung')
|
|
}
|
|
},
|
|
|
|
toggleReservationStatus: async (tripId, id) => {
|
|
const prev = get().reservations
|
|
const current = prev.find(r => r.id === id)
|
|
if (!current) return
|
|
const newStatus = current.status === 'confirmed' ? 'pending' : 'confirmed'
|
|
set(state => ({
|
|
reservations: state.reservations.map(r => r.id === id ? { ...r, status: newStatus } : r)
|
|
}))
|
|
try {
|
|
await reservationsApi.update(tripId, id, { status: newStatus })
|
|
} catch {
|
|
set({ reservations: prev })
|
|
}
|
|
},
|
|
|
|
deleteReservation: async (tripId, id) => {
|
|
try {
|
|
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')
|
|
}
|
|
},
|
|
|
|
addDayNote: async (tripId, dayId, data) => {
|
|
try {
|
|
const result = await dayNotesApi.create(tripId, dayId, data)
|
|
set(state => ({
|
|
dayNotes: {
|
|
...state.dayNotes,
|
|
[String(dayId)]: [...(state.dayNotes[String(dayId)] || []), result.note],
|
|
}
|
|
}))
|
|
return result.note
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Hinzufügen der Notiz')
|
|
}
|
|
},
|
|
|
|
updateDayNote: async (tripId, dayId, id, data) => {
|
|
try {
|
|
const result = await dayNotesApi.update(tripId, dayId, id, data)
|
|
set(state => ({
|
|
dayNotes: {
|
|
...state.dayNotes,
|
|
[String(dayId)]: (state.dayNotes[String(dayId)] || []).map(n => n.id === id ? result.note : n),
|
|
}
|
|
}))
|
|
return result.note
|
|
} catch (err) {
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Aktualisieren der Notiz')
|
|
}
|
|
},
|
|
|
|
deleteDayNote: async (tripId, dayId, id) => {
|
|
const prev = get().dayNotes
|
|
set(state => ({
|
|
dayNotes: {
|
|
...state.dayNotes,
|
|
[String(dayId)]: (state.dayNotes[String(dayId)] || []).filter(n => n.id !== id),
|
|
}
|
|
}))
|
|
try {
|
|
await dayNotesApi.delete(tripId, dayId, id)
|
|
} catch (err) {
|
|
set({ dayNotes: prev })
|
|
throw new Error(err.response?.data?.error || 'Fehler beim Löschen der Notiz')
|
|
}
|
|
},
|
|
}))
|