diff --git a/client/src/components/Planner/PlacesSidebar.test.tsx b/client/src/components/Planner/PlacesSidebar.test.tsx
index 0c3325aaa..94bf45018 100644
--- a/client/src/components/Planner/PlacesSidebar.test.tsx
+++ b/client/src/components/Planner/PlacesSidebar.test.tsx
@@ -336,6 +336,39 @@ describe('Category filter dropdown', () => {
await user.click(parkOpts[parkOpts.length - 1]);
expect(screen.getByText(/2 categories/i)).toBeInTheDocument();
});
+
+ it('FE-PLANNER-SIDEBAR-047: category filter survives unmount/remount (#1541)', async () => {
+ const user = userEvent.setup();
+ const cat = buildCategory({ name: 'Hotel', color: '#3b82f6' });
+ const withCat = buildPlace({ name: 'Grand Palace', category_id: cat.id, address: 'Vienna' });
+ const noCat = buildPlace({ name: 'Street Market', category_id: null, address: 'Lisbon' });
+ const { unmount } = render();
+ await user.click(screen.getByText(/All Categories/i));
+ await user.click(screen.getByText('Hotel'));
+ expect(screen.queryByText('Street Market')).not.toBeInTheDocument();
+ // Switching planner tabs unmounts the sidebar; the filter must come back
+ // both applied and visible instead of silently sticking on the map only.
+ unmount();
+ render();
+ expect(screen.queryByText(/All Categories/i)).not.toBeInTheDocument();
+ expect(screen.getByText('Hotel')).toBeInTheDocument();
+ expect(screen.getByText('Grand Palace')).toBeInTheDocument();
+ expect(screen.queryByText('Street Market')).not.toBeInTheDocument();
+ });
+
+ it('FE-PLANNER-SIDEBAR-048: filter tab survives unmount/remount (#1541)', async () => {
+ const user = userEvent.setup();
+ const planned = buildPlace({ name: 'Planned Place' });
+ const unplanned = buildPlace({ name: 'Unplanned Place' });
+ const assignments = { '1': [buildAssignment({ place: planned, day_id: 1 })] };
+ const { unmount } = render();
+ await user.click(screen.getByRole('button', { name: /Unplanned/i }));
+ expect(screen.queryByText('Planned Place')).not.toBeInTheDocument();
+ unmount();
+ render();
+ expect(screen.queryByText('Planned Place')).not.toBeInTheDocument();
+ expect(screen.getByText('Unplanned Place')).toBeInTheDocument();
+ });
});
// ── Place list interaction ─────────────────────────────────────────────────────
diff --git a/client/src/components/Planner/PlacesSidebarHeader.tsx b/client/src/components/Planner/PlacesSidebarHeader.tsx
index 693b0fbf1..e014d9877 100644
--- a/client/src/components/Planner/PlacesSidebarHeader.tsx
+++ b/client/src/components/Planner/PlacesSidebarHeader.tsx
@@ -23,8 +23,8 @@ export function PlacesHeader(S: SidebarState) {
const {
canEditPlaces, onAddPlace, t, setFileImportOpen, setListImportOpen, hasMultipleListImportProviders,
places, categories, categoryFilters, search, setSearch, plannedIds, hasTracks,
- filter, setFilter, onPlacesFilterChange, setSelectedIds, selectMode, setSelectMode,
- catDropOpen, setCatDropOpen, toggleCategoryFilter, setCategoryFiltersLocal, onCategoryFilterChange,
+ filter, setFilter, setSelectedIds, selectMode, setSelectMode,
+ catDropOpen, setCatDropOpen, toggleCategoryFilter, setCategoryFilters,
} = S
return (
@@ -99,7 +99,7 @@ export function PlacesHeader(S: SidebarState) {
return (
diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts
index 6c2722c44..93e34c612 100644
--- a/client/src/pages/tripPlanner/useTripPlanner.ts
+++ b/client/src/pages/tripPlanner/useTripPlanner.ts
@@ -308,8 +308,10 @@ export function useTripPlanner() {
useTripWebSocket(tripId)
- const [mapCategoryFilter, setMapCategoryFilter] = useState>(new Set())
- const [mapPlacesFilter, setMapPlacesFilter] = useState('all')
+ // Same filter the places sidebar renders — shared via the store so tab
+ // switches can't desync the marker set from the filter UI (#1541).
+ const placesFilter = useTripStore((s) => s.placesFilter)
+ const placesCategoryFilter = useTripStore((s) => s.placesCategoryFilter)
const [expandedDayIds, setExpandedDayIds] = useState | null>(null)
@@ -335,23 +337,23 @@ export function useTripPlanner() {
}
// Build set of planned place IDs for unplanned filter
- const plannedIds = mapPlacesFilter === 'unplanned'
+ const plannedIds = placesFilter === 'unplanned'
? new Set(Object.values(assignments).flatMap(da => da.map(a => a.place?.id).filter(Boolean)))
: null
return places.filter(p => {
if (!p.lat || !p.lng) return false
- if (mapPlacesFilter === 'tracks' && !p.route_geometry) return false
- if (mapCategoryFilter.size > 0) {
+ if (placesFilter === 'tracks' && !p.route_geometry) return false
+ if (placesCategoryFilter.size > 0) {
if (p.category_id == null) {
- if (!mapCategoryFilter.has('uncategorized')) return false
- } else if (!mapCategoryFilter.has(String(p.category_id))) return false
+ if (!placesCategoryFilter.has('uncategorized')) return false
+ } else if (!placesCategoryFilter.has(String(p.category_id))) return false
}
if (hiddenPlaceIds.has(p.id)) return false
if (plannedIds && plannedIds.has(p.id)) return false
return true
})
- }, [places, mapCategoryFilter, mapPlacesFilter, assignments, expandedDayIds])
+ }, [places, placesCategoryFilter, placesFilter, assignments, expandedDayIds])
const { route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay } = useRouteCalculation({ assignments } as any, selectedDayId, routeShown, routeProfile, tripAccommodations)
@@ -915,7 +917,7 @@ export function useTripPlanner() {
mobileSidebarOpen, setMobileSidebarOpen, mobilePlanScrollTopRef, mobilePlacesScrollTopRef,
deletePlaceId, setDeletePlaceId, deletePlaceIds, setDeletePlaceIds,
visibleConnections, setVisibleConnections, toggleConnection, mapTransportDetail, setMapTransportDetail,
- isMobile, isTouch, mapCategoryFilter, setMapCategoryFilter, mapPlacesFilter, setMapPlacesFilter,
+ isMobile, isTouch,
expandedDayIds, setExpandedDayIds, mapPlaces,
route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay,
handleSelectDay, handlePlaceClick, handleMarkerClick, handleMapClick, handleMapContextMenu, openAddPlaceFromPoi,
diff --git a/client/src/store/tripStore.ts b/client/src/store/tripStore.ts
index 2597c0996..b5fcf6e87 100644
--- a/client/src/store/tripStore.ts
+++ b/client/src/store/tripStore.ts
@@ -60,10 +60,17 @@ export interface TripStoreState
files: TripFile[]
reservations: Reservation[]
selectedDayId: number | null
+ // Places filter (list + map markers). Lives here, not in the sidebar, so the
+ // applied filter and the filter UI can never drift apart when the Plan tab
+ // unmounts and remounts (#1541).
+ placesFilter: string
+ placesCategoryFilter: Set
isLoading: boolean
error: string | null
setSelectedDay: (dayId: number | null) => void
+ setPlacesFilter: (filter: string) => void
+ setPlacesCategoryFilter: (categoryIds: Set) => void
handleRemoteEvent: (event: WebSocketEvent) => void
resetTrip: () => void
loadTrip: (tripId: number | string) => Promise
@@ -88,10 +95,14 @@ export const useTripStore = create((set, get) => ({
files: [],
reservations: [],
selectedDayId: null,
+ placesFilter: 'all',
+ placesCategoryFilter: new Set(),
isLoading: false,
error: null,
setSelectedDay: (dayId: number | null) => set({ selectedDayId: dayId }),
+ setPlacesFilter: (filter: string) => set({ placesFilter: filter }),
+ setPlacesCategoryFilter: (categoryIds: Set) => set({ placesCategoryFilter: categoryIds }),
handleRemoteEvent: (event: WebSocketEvent) => handleRemoteEvent(set, get, event),
@@ -110,6 +121,8 @@ export const useTripStore = create((set, get) => ({
files: [],
reservations: [],
selectedDayId: null,
+ placesFilter: 'all',
+ placesCategoryFilter: new Set(),
error: null,
}),