diff --git a/client/src/components/Planner/usePlacesSidebar.test.tsx b/client/src/components/Planner/usePlacesSidebar.test.tsx
index 3ec5b6ca2..07aaf764b 100644
--- a/client/src/components/Planner/usePlacesSidebar.test.tsx
+++ b/client/src/components/Planner/usePlacesSidebar.test.tsx
@@ -188,12 +188,9 @@ describe('usePlacesSidebar filtering', () => {
expect(names()).toEqual(['GPX Track']);
});
- it('FE-PLANNER-PSHOOK-008: a tracks filter without any track falls back to "all"', async () => {
- seedStore(useTripStore, { placesFilter: 'tracks' });
- render();
- await waitFor(() => expect(useTripStore.getState().placesFilter).toBe('all'));
- expect(names()).toEqual(['Plain Spot']);
- });
+ // FE-PLANNER-PSHOOK-008 (a tracks filter without any track falls back to "all")
+ // moved to useTripPlanner (FE-TP-HOOK-177): the fallback now lives there, so it
+ // also holds while no places list is mounted.
it('FE-PLANNER-PSHOOK-009: a category filter keeps only that category', () => {
const tagged = buildPlace({ name: 'Tagged', category_id: 4 });
diff --git a/client/src/components/Planner/usePlacesSidebar.ts b/client/src/components/Planner/usePlacesSidebar.ts
index cc5843c07..6a6c9abf5 100644
--- a/client/src/components/Planner/usePlacesSidebar.ts
+++ b/client/src/components/Planner/usePlacesSidebar.ts
@@ -259,8 +259,9 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
const pickFilter = (next: PlacesFilter) => { setFilter(next); setSelectedIds(new Set()) }
// Alle geplanten Ort-IDs abrufen (einem Tag zugewiesen)
+ // Whether to offer the "Tracks" tab. useTripPlanner moves the filter back to
+ // "all" once the last track is gone, whichever screen is in front.
const hasTracks = useMemo(() => places.some(p => p.route_geometry), [places])
- useEffect(() => { if (filter === 'tracks' && !hasTracks) setFilter('all') }, [hasTracks, filter])
const plannedIds = useMemo(
() => plannedPlaceIds({ assignments, accommodations, reservations }),
diff --git a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx
index db8d6a21b..fb9fe9252 100644
--- a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx
+++ b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx
@@ -63,10 +63,9 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
if (shell.browseFromEdit) setFilter('unplanned')
}, [shell.browseFromEdit, setFilter])
+ // Whether to offer the "Tracks" chip; useTripPlanner moves the filter back to
+ // "all" once the last track is gone.
const hasTracks = useMemo(() => places.some(p => p.route_geometry), [places])
- useEffect(() => {
- if (filter === 'tracks' && !hasTracks) setFilter('all')
- }, [filter, hasTracks, setFilter])
// A hotel is linked through its stay and a venue through its booking; neither is
// ever dragged onto a day, and the pool used to call both unplanned (#2072).
diff --git a/client/src/pages/tripPlanner/useTripPlanner.test.tsx b/client/src/pages/tripPlanner/useTripPlanner.test.tsx
index c4ea889cc..33cf5c255 100644
--- a/client/src/pages/tripPlanner/useTripPlanner.test.tsx
+++ b/client/src/pages/tripPlanner/useTripPlanner.test.tsx
@@ -1,4 +1,4 @@
-// FE-TP-HOOK-001 to FE-TP-HOOK-176
+// FE-TP-HOOK-001 to FE-TP-HOOK-177
import React from 'react'
import { renderHook, act, waitFor } from '@testing-library/react'
import { TranslationProvider } from '../../i18n/TranslationContext'
@@ -644,6 +644,23 @@ describe('useTripPlanner — map derivations', () => {
expect(result.current.mapPlaces.map(p => p.id)).toEqual([2])
})
+ it('FE-TP-HOOK-177: losing the last track moves the "Tracks" filter back to all', async () => {
+ // The phone map has no places list mounted, so this hook is what keeps the
+ // filter from pointing at a pool no control offers any more.
+ seedTrip({
+ places: [geo(1), geo(2, { route_geometry: '[[1,2]]' })],
+ placesFilter: 'tracks',
+ })
+
+ const { result } = await renderPlanner()
+ expect(useTripStore.getState().placesFilter).toBe('tracks')
+
+ act(() => { useTripStore.setState({ places: [geo(1)] }) })
+
+ await waitFor(() => expect(useTripStore.getState().placesFilter).toBe('all'))
+ expect(result.current.mapPlaces.map(p => p.id)).toEqual([1])
+ })
+
it('FE-TP-HOOK-027: the category filter honours the uncategorized bucket', async () => {
seedTrip({
places: [geo(1, { category_id: 3 }), geo(2, { category_id: null })],
diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts
index bc65e3ac5..25b85e4c4 100644
--- a/client/src/pages/tripPlanner/useTripPlanner.ts
+++ b/client/src/pages/tripPlanner/useTripPlanner.ts
@@ -768,6 +768,17 @@ export function useTripPlanner() {
})
}, [places, placesCategoryFilter, placesFilter, placesRatingFilter, assignments, expandedDayIds, selectedDayId, days, tripAccommodations, reservations, compactUnplanned])
+ // The "Tracks" pool is only offered while a place carries a track. When the last
+ // one goes (deleted here or by a collaborator), fall back to "all" — here, because
+ // this hook is mounted whatever is on screen: the lists that used to do it are not
+ // mounted while the phone map is in front, and the map would sit empty under a
+ // "Tracks" filter no control offers any more.
+ const setPlacesFilter = useTripStore((s) => s.setPlacesFilter)
+ const hasTracks = useMemo(() => places.some(p => p.route_geometry), [places])
+ useEffect(() => {
+ if (placesFilter === 'tracks' && !hasTracks) setPlacesFilter('all')
+ }, [placesFilter, hasTracks, setPlacesFilter])
+
const { route, routeWalking, routeSegments, routeVias, routeInfo, setRoute, setRouteInfo, updateRouteForDay } = useRouteCalculation({ assignments } as any, selectedDayId, routeShown, routeProfile, tripAccommodations)
// Road trip mode already draws the whole trip its own way, so the overview stands
// down there rather than drawing a second set of lines over it.
diff --git a/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx b/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
index a2e352d6a..a2eebd252 100644
--- a/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
+++ b/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
@@ -124,11 +124,11 @@ describe('MPlacesBrowser', () => {
expect(screen.queryByText('Louvre')).not.toBeInTheDocument()
})
- it('FE-MOB-PBROW-007: hides the tracks chip and falls back to all when the last track is gone', () => {
- seedStore(useTripStore, { placesFilter: 'tracks' })
+ // The fall back to "all" once the last track is gone lives in useTripPlanner
+ // (FE-TP-HOOK-177), which stays mounted while this browser is not.
+ it('FE-MOB-PBROW-007: hides the tracks chip when no place carries a track', () => {
renderBrowser(makePlanner({ places: [LOUVRE, EIFFEL] } as Partial))
expect(screen.queryByRole('button', { name: 'places.filterTracks' })).not.toBeInTheDocument()
- expect(useTripStore.getState().placesFilter).toBe('all')
expect(screen.getByText('places.count:2')).toBeInTheDocument()
})