diff --git a/client/src/components/Tours/ToursSidebar.tsx b/client/src/components/Tours/ToursSidebar.tsx index b8d21dee5..aba251dde 100644 --- a/client/src/components/Tours/ToursSidebar.tsx +++ b/client/src/components/Tours/ToursSidebar.tsx @@ -12,6 +12,7 @@ import { NEUTRAL_TINT, fs } from '../shared/DialogShell' import { Tooltip } from '../shared/Tooltip' import EmptyState from '../shared/EmptyState' import { TourDayMenu } from './tourParts' +import { filterTours, type TourFilter } from './tourPresentation' const MAX_FILE_BYTES = 10 * 1024 * 1024 @@ -44,7 +45,7 @@ export default function ToursSidebar({ tripId, days, tours, loading = false, sel const { canEdit, canAssign } = useTourPermissions({ tripId, canEdit: editPermission, canAssign: assignPermission }) const toast = useToast() const loadTrip = useTripStore(s => s.loadTrip) - const [filter, setFilter] = useState<'all' | 'unplanned' | 'planned'>('all') + const [filter, setFilter] = useState('all') const [importing, setImporting] = useState(false) const fileInputRef = useRef(null) const filterRef = useRef(null) @@ -95,11 +96,7 @@ export default function ToursSidebar({ tripId, days, tours, loading = false, sel unplanned: tours.filter(tr => !tr.planned).length, planned: tours.filter(tr => tr.planned).length, } - const filtered = tours.filter(tr => { - if (filter === 'unplanned') return !tr.planned - if (filter === 'planned') return tr.planned - return true - }) + const filtered = filterTours(tours, filter) useEffect(() => { if (!filterOpen) return diff --git a/client/src/components/Tours/tourPresentation.test.ts b/client/src/components/Tours/tourPresentation.test.ts index 31f7e209c..1efac2686 100644 --- a/client/src/components/Tours/tourPresentation.test.ts +++ b/client/src/components/Tours/tourPresentation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { hikeSourceBadgeLabel, tourSource } from './tourPresentation' +import { filterTours, hikeSourceBadgeLabel, tourSource } from './tourPresentation' describe('tourPresentation', () => { it('uses the source abstraction for TREK, GPX import, and wanderer badges', () => { @@ -12,4 +12,19 @@ describe('tourPresentation', () => { expect(tourSource({ wanderer_ref: 'wanderer:123' })).toBe('wanderer') expect(hikeSourceBadgeLabel({ wanderer_ref: 'wanderer:123' }, t)).toBe('tours.badge.wanderer') }) + + it('filters tours by All, Unplanned and Planned, keeping their order', () => { + const tours = [ + { id: 1, planned: true }, + { id: 2, planned: false }, + { id: 3, planned: true }, + { id: 4, planned: false }, + ] + + expect(filterTours(tours, 'all')).toEqual(tours) + expect(filterTours(tours, 'all')).not.toBe(tours) + expect(filterTours(tours, 'unplanned').map(tour => tour.id)).toEqual([2, 4]) + expect(filterTours(tours, 'planned').map(tour => tour.id)).toEqual([1, 3]) + expect(filterTours([], 'planned')).toEqual([]) + }) }) diff --git a/client/src/components/Tours/tourPresentation.ts b/client/src/components/Tours/tourPresentation.ts index c26294ff8..5ff5e0f52 100644 --- a/client/src/components/Tours/tourPresentation.ts +++ b/client/src/components/Tours/tourPresentation.ts @@ -2,6 +2,9 @@ import type { TourListItem } from '@trek/shared' export type TourSource = 'trek' | 'gpx' | 'wanderer' +/** All / Unplanned / Planned, the same three states as the Places pool. */ +export type TourFilter = 'all' | 'unplanned' | 'planned' + type Translate = (key: string, params?: Record) => string /** @@ -20,3 +23,12 @@ export function hikeSourceBadgeLabel( ): string { return t(`tours.badge.${tourSource(tour)}`) } + +/** The tours a list shows under the All / Unplanned / Planned filter, in their order. */ +export function filterTours>(tours: T[], filter: TourFilter): T[] { + return tours.filter(tr => { + if (filter === 'unplanned') return !tr.planned + if (filter === 'planned') return tr.planned + return true + }) +} diff --git a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx index 7fdae169b..8097f0f20 100644 --- a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx +++ b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx @@ -20,6 +20,7 @@ import MPlacesBulkCategorySheet from './MPlacesBulkCategorySheet' import MPlacesSaveToCollectionSheet from './MPlacesSaveToCollectionSheet' import MPlacesToursModeSwitch from './MPlacesToursModeSwitch' import MToursSelectionList from './MToursSelectionList' +import type { TourFilter } from '../../../../components/Tours/tourPresentation' import { filterPool, firstPlannedDayNumbers, plannedPlaceIds } from './placesBrowserModel' import { MCategoryFilterList, MRatingFloorChips, SquareCheck } from './MPlacesFilterControls' import { countActivePlacesFilters } from '../../../../utils/placesFilter' @@ -46,7 +47,7 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps) // Top level of the places browser, mirroring the desktop right add-panel's // Places <-> Tours switch placement. const [toursMode, setToursMode] = useState(false) - const [toursFilter, setToursFilter] = useState<'all' | 'unplanned' | 'planned'>('all') + const [toursFilter, setToursFilter] = useState('all') const poolPlaces = useMemo( // Places that are tours stay out of the Places pool while the addon is on. diff --git a/client/src/mobile/screens/trip/places/MToursSelectionList.tsx b/client/src/mobile/screens/trip/places/MToursSelectionList.tsx index c711b7418..661b2abbd 100644 --- a/client/src/mobile/screens/trip/places/MToursSelectionList.tsx +++ b/client/src/mobile/screens/trip/places/MToursSelectionList.tsx @@ -2,7 +2,7 @@ import { ArrowDown, ArrowUp, Mountain, AlertTriangle, CalendarDays } from 'lucid import type { TourListItem } from '@trek/shared' import { useSettingsStore } from '../../../../store/settingsStore' import { formatDistance, formatElevation } from '../../../../utils/units' -import { hikeSourceBadgeLabel } from '../../../../components/Tours/tourPresentation' +import { filterTours, hikeSourceBadgeLabel, type TourFilter } from '../../../../components/Tours/tourPresentation' import type { TripPlanner, MTripShellApi } from '../MTripShell' import { useTourPermissions, type TourPermissionProps } from '../../../../components/Tours/useTourPermissions' @@ -10,7 +10,7 @@ interface MToursSelectionListProps extends TourPermissionProps { planner: TripPlanner shell: MTripShellApi /** All / Unplanned / Planned — same three states as the Places pool. */ - filter: 'all' | 'unplanned' | 'planned' + filter: TourFilter } /** @@ -28,11 +28,7 @@ export default function MToursSelectionList({ planner, shell, filter, canAssign: const distanceUnit = useSettingsStore(s => s.settings.distance_unit) const tours: TourListItem[] = planner.tours - const filtered = tours.filter(tr => { - if (filter === 'unplanned') return !tr.planned - if (filter === 'planned') return tr.planned - return true - }) + const filtered = filterTours(tours, filter) if (filtered.length === 0) { return (