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') } }, }))