diff --git a/client/src/components/Planner/PlacesSidebar.test.tsx b/client/src/components/Planner/PlacesSidebar.test.tsx
index 96e1a7304..e5d99f6b9 100644
--- a/client/src/components/Planner/PlacesSidebar.test.tsx
+++ b/client/src/components/Planner/PlacesSidebar.test.tsx
@@ -288,6 +288,113 @@ describe('Filter tabs', () => {
expect(screen.getByText('Planned Place')).toBeInTheDocument();
expect(screen.queryByText('Unplanned Place')).not.toBeInTheDocument();
});
+
+ // ── The open day narrows the pool, and says so ──────────────────────────────
+ //
+ // The map has followed the selected day on this filter since #2024 while the list
+ // did not, so a trip with everything planned read "55" in the pool beside five pins
+ // on the map. Reported twice in the same Discord thread as the map being broken.
+
+ it('FE-PLANNER-SIDEBAR-052: with a day open, "Planned" shows that day, not the whole trip', () => {
+ const today = buildPlace({ id: 91, name: 'On This Day' });
+ const otherDay = buildPlace({ id: 92, name: 'On Another Day' });
+ const assignments = {
+ '1': [buildAssignment({ place: today, day_id: 1 })],
+ '2': [buildAssignment({ place: otherDay, day_id: 2 })],
+ };
+ seedStore(useTripStore, { placesFilter: 'planned' });
+ render(
+ ,
+ );
+
+ expect(screen.getByText('On This Day')).toBeInTheDocument();
+ expect(screen.queryByText('On Another Day')).not.toBeInTheDocument();
+ });
+
+ it('FE-PLANNER-SIDEBAR-053: without a day open it is the whole trip again', () => {
+ const dayOne = buildPlace({ id: 91, name: 'On This Day' });
+ const dayTwo = buildPlace({ id: 92, name: 'On Another Day' });
+ const assignments = {
+ '1': [buildAssignment({ place: dayOne, day_id: 1 })],
+ '2': [buildAssignment({ place: dayTwo, day_id: 2 })],
+ };
+ seedStore(useTripStore, { placesFilter: 'planned' });
+ render(
+ ,
+ );
+
+ expect(screen.getByText('On This Day')).toBeInTheDocument();
+ expect(screen.getByText('On Another Day')).toBeInTheDocument();
+ });
+
+ it('FE-PLANNER-SIDEBAR-054: the note only appears while the day is actually narrowing', () => {
+ const today = buildPlace({ id: 91, name: 'On This Day' });
+ const assignments = { '1': [buildAssignment({ place: today, day_id: 1 })] };
+ const onClearSelectedDay = vi.fn();
+ seedStore(useTripStore, { placesFilter: 'planned' });
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.getByText('Showing the open day only')).toBeInTheDocument();
+
+ // Dismissing it asks for the day to be closed rather than changing the filter.
+ fireEvent.click(screen.getByLabelText('Show the whole trip'));
+ expect(onClearSelectedDay).toHaveBeenCalledTimes(1);
+
+ // No day open, nothing narrowed, no note.
+ rerender(
+ ,
+ );
+ expect(screen.queryByText('Showing the open day only')).not.toBeInTheDocument();
+ });
+
+ it('FE-PLANNER-SIDEBAR-055: "Unplanned" stays trip-wide while a day is open', () => {
+ // A place assigned to some other day is planned, whichever day happens to be open,
+ // so it must not reappear in the unplanned pool.
+ const otherDay = buildPlace({ id: 92, name: 'On Another Day' });
+ const loose = buildPlace({ id: 93, name: 'Not Planned At All' });
+ const assignments = { '2': [buildAssignment({ place: otherDay, day_id: 2 })] };
+ seedStore(useTripStore, { placesFilter: 'unplanned' });
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Not Planned At All')).toBeInTheDocument();
+ expect(screen.queryByText('On Another Day')).not.toBeInTheDocument();
+ expect(screen.queryByText('Showing the open day only')).not.toBeInTheDocument();
+ });
});
// ── Search ────────────────────────────────────────────────────────────────────
diff --git a/client/src/components/Planner/PlacesSidebarHeader.tsx b/client/src/components/Planner/PlacesSidebarHeader.tsx
index 6fb4eee5c..72d03dd7e 100644
--- a/client/src/components/Planner/PlacesSidebarHeader.tsx
+++ b/client/src/components/Planner/PlacesSidebarHeader.tsx
@@ -1,4 +1,4 @@
-import { Search, Plus, X, Upload, FileDown, ChevronDown, Check, MapPin, Star, CalendarPlus } from 'lucide-react'
+import { Search, Plus, X, Upload, FileDown, ChevronDown, Check, MapPin, Star, CalendarPlus, CalendarDays } from 'lucide-react'
import { getCategoryIcon } from '../shared/categoryIcons'
import Tooltip from '../shared/Tooltip'
import { useElementSize } from '../../hooks/useElementSize'
@@ -32,7 +32,7 @@ export function PlacesDropOverlay({ t }: SidebarState) {
export function PlacesHeader(S: SidebarState) {
const {
canEditPlaces, onAddPlace, onAddPlaceToSelectedDay, selectedDayId, t, setFileImportOpen, setListImportOpen, hasMultipleListImportProviders,
- places, categories, categoryFilters, search, setSearch, plannedIds, hasTracks,
+ places, categories, categoryFilters, search, setSearch, plannedIds, plannedFilterIds, dayScoped, onClearSelectedDay, hasTracks,
filter, setFilter, setSelectedIds, selectMode, setSelectMode,
catDropOpen, setCatDropOpen, toggleCategoryFilter, setCategoryFilters,
ratingFilter, setRatingFilter,
@@ -163,7 +163,11 @@ export function PlacesHeader(S: SidebarState) {
const counts = {
all: baseFiltered.length,
unplanned: baseFiltered.filter(p => !plannedIds.has(p.id)).length,
- planned: baseFiltered.filter(p => plannedIds.has(p.id)).length,
+ // While a day is open this counts that day's plan, the same set the list and
+ // the map show. Counting the whole trip here is what made the tab read 55
+ // beside five pins, with nothing to say the two were answering different
+ // questions.
+ planned: baseFiltered.filter(p => plannedFilterIds.has(p.id)).length,
tracks: baseFiltered.filter(p => p.route_geometry).length,
}
const tabs = ([
@@ -211,6 +215,42 @@ export function PlacesHeader(S: SidebarState) {
)
})()}
+ {/* Says out loud what the count above already narrowed to.
+ The map has followed the open day on this filter since #2024, and until now
+ nothing anywhere said so: the pool read 55, the map drew five, and the honest
+ conclusion was that the map was broken. It sits under the tabs rather than on
+ the map because a chip over the canvas is unreachable on a phone, which is
+ where this was reported from. */}
+ {dayScoped && (
+
diff --git a/client/src/components/Planner/usePlacesSidebar.ts b/client/src/components/Planner/usePlacesSidebar.ts
index 63f808ca0..4b1e62b6b 100644
--- a/client/src/components/Planner/usePlacesSidebar.ts
+++ b/client/src/components/Planner/usePlacesSidebar.ts
@@ -15,7 +15,7 @@ import { placeToSaveTarget } from '../Collections/saveTarget'
import type { Place, Category, Day, AssignmentsMap } from '../../types'
import { getGoogleMapsUrlForPlace } from './placeGoogleMaps'
import { safeHttpUrl } from '../../utils/safeUrl'
-import { plannedPlaceIds, type PlannedAccommodation } from '../../utils/plannedPlaces'
+import { plannedPlaceIds, plannedPlaceIdsForDay, type PlannedAccommodation } from '../../utils/plannedPlaces'
/** Stable identity — a fresh [] default would invalidate the planned memo on every render. */
const NO_ACCOMMODATIONS: PlannedAccommodation[] = []
@@ -40,6 +40,12 @@ export interface PlacesSidebarProps {
* through the day picker.
*/
onAddPlaceToSelectedDay?: () => void
+ /**
+ * Close the open day, from the note that says the pool is showing only that day.
+ * Absent leaves the note without its dismiss, which is what the mobile day picker
+ * wants: there the day is closed by the picker itself.
+ */
+ onClearSelectedDay?: () => void
onAssignToDay: (placeId: number, dayId: number) => void
onEditPlace: (place: Place) => void
onDeletePlace: (placeId: number) => void
@@ -60,7 +66,7 @@ export interface PlacesSidebarProps {
*/
export function usePlacesSidebar(props: PlacesSidebarProps) {
const {
- tripId, places, assignments, selectedDayId, accommodations = NO_ACCOMMODATIONS,
+ tripId, places, assignments, selectedDayId, days, accommodations = NO_ACCOMMODATIONS,
pushUndo, initialScrollTop, onScrollTopChange,
} = props
const { t } = useTranslation()
@@ -238,10 +244,30 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
[assignments, accommodations, reservations],
)
+ /**
+ * What "planned" means while a day is open: that day's plan, not the whole trip's.
+ *
+ * The map has narrowed to the selected day since #2024, and the list did not, which is
+ * how a trip with 55 planned places showed 55 in the pool and five pins on the map with
+ * nothing to explain the gap — read, reasonably, as the map being broken. The list now
+ * follows the map, and `dayScoped` tells the header to say so.
+ *
+ * Only "planned" narrows. "Unplanned" stays trip-wide on purpose: a place assigned to
+ * some other day is planned, whichever day happens to be open.
+ */
+ const plannedInDayIds = useMemo(
+ () => (selectedDayId
+ ? plannedPlaceIdsForDay(selectedDayId, days, { assignments, accommodations, reservations })
+ : null),
+ [selectedDayId, days, assignments, accommodations, reservations],
+ )
+ const plannedFilterIds = plannedInDayIds ?? plannedIds
+ const dayScoped = filter === 'planned' && plannedInDayIds !== null
+
const filtered = useMemo(() => {
const list = places.filter(p => {
if (filter === 'unplanned' && plannedIds.has(p.id)) return false
- if (filter === 'planned' && !plannedIds.has(p.id)) return false
+ if (filter === 'planned' && !plannedFilterIds.has(p.id)) return false
if (filter === 'tracks' && !p.route_geometry) return false
if (categoryFilters.size > 0) {
if (p.category_id == null) {
@@ -254,7 +280,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
return true
})
return list
- }, [places, filter, categoryFilters, search, plannedIds, ratingFilter])
+ }, [places, filter, categoryFilters, search, plannedIds, plannedFilterIds, ratingFilter])
const registerPlaceRow = useCallback((placeId: number, element: HTMLDivElement | null) => {
if (element) {
@@ -322,7 +348,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
markSelectionVisited, markVisitedBusy,
exitSelectMode, toggleSelected, toggleCategoryFilter, dayPickerPlace, setDayPickerPlace,
catDropOpen, setCatDropOpen, mobileShowDays, setMobileShowDays,
- hasTracks, plannedIds, filtered, registerPlaceRow, isAssignedToSelectedDay, inDaySet, openContextMenu,
+ hasTracks, plannedIds, plannedFilterIds, dayScoped, filtered, registerPlaceRow, isAssignedToSelectedDay, inDaySet, openContextMenu,
}
}
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx
index c531c8ad6..f13264469 100644
--- a/client/src/pages/TripPlannerPage.tsx
+++ b/client/src/pages/TripPlannerPage.tsx
@@ -685,6 +685,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
assignments={assignments}
accommodations={tripAccommodations}
selectedDayId={selectedDayId}
+ onClearSelectedDay={() => handleSelectDay(null)}
selectedPlaceId={selectedPlaceId}
onPlaceClick={handlePlaceClick}
onAddPlace={() => { setEditingPlace(null); setPlaceFormDayId(null); setShowPlaceForm(true) }}
@@ -852,7 +853,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {