diff --git a/client/src/components/Map/MapView.test.tsx b/client/src/components/Map/MapView.test.tsx index 8d843e6e9..a9e3396b9 100644 --- a/client/src/components/Map/MapView.test.tsx +++ b/client/src/components/Map/MapView.test.tsx @@ -4,7 +4,7 @@ import { render, screen } from '../../../tests/helpers/render' import { fireEvent, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { resetAllStores } from '../../../tests/helpers/store' -import { buildPlace } from '../../../tests/helpers/factories' +import { buildPlace, buildReservation } from '../../../tests/helpers/factories' import * as photoService from '../../services/photoService' const mapMock = vi.hoisted(() => ({ @@ -15,6 +15,7 @@ const mapMock = vi.hoisted(() => ({ on: vi.fn(), off: vi.fn(), panBy: vi.fn(), + latLngToContainerPoint: vi.fn(() => ({ x: 0, y: 0, distanceTo: () => 1000 })), })) vi.mock('react-leaflet', () => ({ @@ -42,6 +43,7 @@ vi.mock('react-leaflet', () => ({ Polyline: ({ positions }: any) =>
, CircleMarker: () =>
, Circle: () =>
, + Tooltip: ({ children }: any) => <>{children}, useMap: () => mapMock, useMapEvents: () => ({}), })) @@ -349,4 +351,30 @@ describe('MapView', () => { expect(zoom).toBe(2) }) }) + + it('FE-COMP-MAPVIEW-023: a routable reservation not in visibleConnectionIds draws no route', () => { + const reservation = buildReservation({ + id: 43, + type: 'flight', + endpoints: [ + { role: 'from', sequence: 0, name: 'A', code: 'AAA', lat: 1, lng: 2, timezone: null, local_time: null, local_date: null }, + { role: 'to', sequence: 1, name: 'B', code: 'BBB', lat: 3, lng: 4, timezone: null, local_time: null, local_date: null }, + ], + } as any) + render() + expect(screen.queryByTestId('polyline')).not.toBeInTheDocument() + }) + + it('FE-COMP-MAPVIEW-024: a routable reservation in visibleConnectionIds draws its route', () => { + const reservation = buildReservation({ + id: 42, + type: 'flight', + endpoints: [ + { role: 'from', sequence: 0, name: 'A', code: 'AAA', lat: 1, lng: 2, timezone: null, local_time: null, local_date: null }, + { role: 'to', sequence: 1, name: 'B', code: 'BBB', lat: 3, lng: 4, timezone: null, local_time: null, local_date: null }, + ], + } as any) + render() + expect(screen.getAllByTestId('polyline').length).toBeGreaterThan(0) + }) }) diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx index 862670db5..94db37c93 100644 --- a/client/src/components/Map/MapView.tsx +++ b/client/src/components/Map/MapView.tsx @@ -11,6 +11,7 @@ import { getCategoryIcon, CATEGORY_ICON_MAP } from '../shared/categoryIcons' import ReservationOverlay from './ReservationOverlay' import { PluginMapMarkers } from './MapPluginMarkers' import { useTransportRoutes } from '../../hooks/useTransportRoutes' +import { visibleRouteReservations } from '../../utils/reservationRoutes' import type { Reservation } from '../../types' import { POI_CATEGORY_BY_KEY, type Poi } from './poiCategories' import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults' @@ -478,12 +479,9 @@ export const MapView = memo(function MapView({ {poi.name} )), [pois, onPoiClick]) - const visibleReservations = useMemo(() => { - const set = new Set(visibleConnectionIds || []) - // Transit journeys ride the route toggle — they are part of the computed - // day route, so hiding the route hides them too (#1065). - return reservations.filter((r: Reservation) => (r.type === 'transit' && showTransitRoutes) || set.has(r.id)) - }, [reservations, visibleConnectionIds, showTransitRoutes]) + const visibleReservations = useMemo(() => ( + visibleRouteReservations(reservations, { visibleConnectionIds, showTransitRoutes }) + ), [reservations, visibleConnectionIds, showTransitRoutes]) // Real road geometry for car/bus/taxi/bicycle bookings (straight line until it loads/if it fails). const transportRoutes = useTransportRoutes(visibleReservations) // Dynamic padding: account for sidebars + bottom inspector + day detail panel diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index 0ba8fd298..aa81048d3 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -12,6 +12,7 @@ import { isStandardFamily, supportsCustom3d, wantsTerrain, addCustom3dBuildings, import { attachLocationMarker, type LocationMarkerHandle } from './locationMarkerMapbox' import { ReservationMapboxOverlay } from './reservationsMapbox' import { useTransportRoutes } from '../../hooks/useTransportRoutes' +import { visibleRouteReservations } from '../../utils/reservationRoutes' import { MAPBOX_DEFAULT_STYLE, styleForActiveProvider, basemapLanguage, type GlMapProvider } from './glProviders' import LocationButton from './LocationButton' import { useGeolocation } from '../../hooks/useGeolocation' @@ -928,12 +929,9 @@ export function MapViewGL({ // `visibleConnectionIds` is driven by the per-reservation toggle in // DayPlanSidebar — nothing is rendered until the user enables a // booking's route, matching the Leaflet MapView's behaviour. - const visibleReservations = useMemo(() => { - const set = new Set(visibleConnectionIds || []) - // Transit journeys ride the route toggle — they are part of the computed - // day route, so hiding the route hides them too (#1065). - return reservations.filter(r => (r.type === 'transit' && showTransitRoutes) || set.has(r.id)) - }, [reservations, visibleConnectionIds, showTransitRoutes]) + const visibleReservations = useMemo(() => ( + visibleRouteReservations(reservations, { visibleConnectionIds, showTransitRoutes }) + ), [reservations, visibleConnectionIds, showTransitRoutes]) // Real road geometry for car/bus/taxi/bicycle bookings (straight line until it loads/if it fails). const transportRoutes = useTransportRoutes(visibleReservations) diff --git a/client/src/components/Planner/DayPlanSidebar.test.tsx b/client/src/components/Planner/DayPlanSidebar.test.tsx index 971ff1b23..96d3040c3 100644 --- a/client/src/components/Planner/DayPlanSidebar.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.test.tsx @@ -392,6 +392,53 @@ describe('DayPlanSidebar', () => { expect(screen.queryByText('Alexanderplatz')).not.toBeInTheDocument() }) + it('FE-PLANNER-DAYPLAN-106: the toolbar button shows "show all" and calls onToggleAllConnections when not all routes are shown', async () => { + const user = userEvent.setup() + const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) + const res1 = { + ...buildReservation({ id: 401, type: 'flight' }), + endpoints: [ + { role: 'from', sequence: 0, name: 'A', code: null, lat: 1, lng: 2, timezone: null, local_time: null, local_date: null }, + { role: 'to', sequence: 1, name: 'B', code: null, lat: 3, lng: 4, timezone: null, local_time: null, local_date: null }, + ], + } + const onToggleAllConnections = vi.fn() + render() + await user.click(screen.getByLabelText('Show all booking routes')) + expect(onToggleAllConnections).toHaveBeenCalledTimes(1) + }) + + it('FE-PLANNER-DAYPLAN-107: the toolbar button reads "hide" once allConnectionsShown is true, and calls onToggleAllConnections on click', async () => { + const user = userEvent.setup() + const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) + const res1 = { + ...buildReservation({ id: 401, type: 'flight' }), + endpoints: [ + { role: 'from', sequence: 0, name: 'A', code: null, lat: 1, lng: 2, timezone: null, local_time: null, local_date: null }, + { role: 'to', sequence: 1, name: 'B', code: null, lat: 3, lng: 4, timezone: null, local_time: null, local_date: null }, + ], + } + const onToggleAllConnections = vi.fn() + render() + await user.click(screen.getByLabelText('Hide all booking routes')) + expect(onToggleAllConnections).toHaveBeenCalledTimes(1) + }) + + it('FE-PLANNER-DAYPLAN-108: the toolbar button is absent when the trip has no routable reservation', () => { + const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) + render() + expect(screen.queryByLabelText('Show all booking routes')).not.toBeInTheDocument() + expect(screen.queryByLabelText('Hide all booking routes')).not.toBeInTheDocument() + }) + // ── Day info button ───────────────────────────────────────────────────── it('FE-PLANNER-DAYPLAN-018: clicking day header calls onDayDetail', async () => { diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 9e2d45bab..2b503f576 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -69,6 +69,8 @@ interface DayPlanSidebarProps { reservations?: Reservation[] visibleConnectionIds?: number[] onToggleConnection?: (reservationId: number) => void + allConnectionsShown?: boolean + onToggleAllConnections?: () => void externalTransportDetail?: Reservation | null onExternalTransportDetailHandled?: () => void onAddReservation: (dayId: number) => void @@ -122,6 +124,8 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { reservations = [], visibleConnectionIds = [], onToggleConnection, + allConnectionsShown = false, + onToggleAllConnections, externalTransportDetail, onExternalTransportDetailHandled, onAddReservation, @@ -1014,6 +1018,8 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { reservations, visibleConnectionIds, onToggleConnection, + allConnectionsShown, + onToggleAllConnections, externalTransportDetail, onExternalTransportDetailHandled, onAddReservation, @@ -1183,6 +1189,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP reservations, visibleConnectionIds, onToggleConnection, + allConnectionsShown, + onToggleAllConnections, externalTransportDetail, onExternalTransportDetailHandled, onAddReservation, @@ -1323,6 +1331,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP categories={categories} assignments={assignments} reservations={reservations} + allConnectionsShown={allConnectionsShown} + onToggleAllConnections={onToggleAllConnections} dayNotes={dayNotes} t={t} locale={locale} diff --git a/client/src/components/Planner/DayPlanSidebarToolbar.tsx b/client/src/components/Planner/DayPlanSidebarToolbar.tsx index ebe92dc29..e52ea50c8 100644 --- a/client/src/components/Planner/DayPlanSidebarToolbar.tsx +++ b/client/src/components/Planner/DayPlanSidebarToolbar.tsx @@ -1,10 +1,11 @@ import React, { useState, useRef } from 'react' -import { ChevronsDownUp, ChevronsUpDown, FileDown, Undo2, ArrowUpDown, CalendarPlus } from 'lucide-react' +import { ChevronsDownUp, ChevronsUpDown, FileDown, Undo2, ArrowUpDown, CalendarPlus, Route as RouteIcon } from 'lucide-react' import { downloadTripPDF } from '../PDF/TripPDF' import { DayReorderPopup } from './DayReorderPopup' import Tooltip from '../shared/Tooltip' import { useToast } from '../shared/Toast' import { IcsSubscribeModal } from './IcsSubscribeModal' +import { isRoutableReservation } from '../../utils/reservationRoutes' import type { Trip, Day, Place, Category, AssignmentsMap, Reservation, DayNote } from '../../types' interface DayPlanSidebarToolbarProps { @@ -15,6 +16,8 @@ interface DayPlanSidebarToolbarProps { categories: Category[] assignments: AssignmentsMap reservations: Reservation[] + allConnectionsShown?: boolean + onToggleAllConnections?: () => void dayNotes: Record t: (key: string, params?: Record) => string locale: string @@ -37,6 +40,7 @@ interface DayPlanSidebarToolbarProps { export function DayPlanSidebarToolbar({ tripId, trip, days, places, categories, assignments, reservations, dayNotes, + allConnectionsShown = false, onToggleAllConnections, t, locale, toast, pdfHover, setPdfHover, setIcsHover, expandedDays, setExpandedDays, onUndo, canUndo, undoHover, setUndoHover, lastActionLabel, canEditDays, onReorderDays, onAddDay, @@ -295,6 +299,32 @@ export function DayPlanSidebarToolbar({ />
)} + {onToggleAllConnections && reservations.some(isRoutableReservation) && (() => { + const label = t(allConnectionsShown ? 'map.hideAllConnections' : 'map.showAllConnections') + return ( + + + + ) + })()}
) diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index ada1934d5..6460b322a 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -180,7 +180,7 @@ export default function TripPlannerPage(): React.ReactElement | null { // Page = wiring container: the entire planner state machine (store, tabs, // selection, CRUD handlers with undo, map filters, splash) lives in the hook. const { - tripId, navigate, toast, t, language, settings, placesPhotosEnabled, + tripId, navigate, toast, t, language, placesPhotosEnabled, trip, days, places, assignments, packingItems, todoItems, categories, reservations, budgetItems, files, selectedDayId, isLoading, tripActions, can, canUploadFiles, pushUndo, undo, canUndo, lastActionLabel, handleUndo, @@ -204,7 +204,7 @@ export default function TripPlannerPage(): React.ReactElement | null { routeShown, setRouteShown, routeProfile, setRouteProfile, fitKey, setFitKey, mobileSidebarOpen, setMobileSidebarOpen, mobilePlanScrollTopRef, mobilePlacesScrollTopRef, deletePlaceId, setDeletePlaceId, deletePlaceIds, setDeletePlaceIds, - visibleConnections, setVisibleConnections, toggleConnection, mapTransportDetail, setMapTransportDetail, + visibleConnections, toggleConnection, allConnectionsShown, toggleAllConnections, mapTransportDetail, setMapTransportDetail, isMobile, isTouch, expandedDayIds, setExpandedDayIds, mapPlaces, route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay, @@ -419,6 +419,8 @@ export default function TripPlannerPage(): React.ReactElement | null { reservations={reservations} visibleConnectionIds={visibleConnections} onToggleConnection={toggleConnection} + allConnectionsShown={allConnectionsShown} + onToggleAllConnections={toggleAllConnections} externalTransportDetail={mapTransportDetail} onExternalTransportDetailHandled={() => setMapTransportDetail(null)} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true) }} @@ -646,7 +648,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
{mobileSidebarOpen === 'left' - ? { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId) }} onReorder={handleReorder} onReorderDays={handleReorderDays} onAddDay={handleAddDay} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute([r.coordinates]); setRouteInfo(r) } }} reservations={reservations} visibleConnectionIds={visibleConnections} onToggleConnection={toggleConnection} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onAddTransport={can('day_edit', trip) ? (dayId) => { setTransportModalDayId(dayId); setEditingTransport(null); setTransitPrefill(null); setTransportModalAutomated(false); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onOpenTransit={(r) => { setTransitJourney(r); setMobileSidebarOpen(null) }} onPlanTransit={can('day_edit', trip) && tripHasDates ? (dayId) => { setTransportModalDayId(dayId); setEditingTransport(null); setTransitPrefill(null); setTransportModalAutomated(true); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); selectAssignment(null) }} onRemoveAssignment={handleRemoveAssignment} onEditPlace={(place, assignmentId) => { setEditingPlace(place); setEditingAssignmentId(assignmentId || null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} accommodations={tripAccommodations} routeShown={routeShown} routeProfile={routeProfile} onToggleRoute={() => setRouteShown(v => !v)} onSetRouteProfile={setRouteProfile} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} onExpandedDaysChange={setExpandedDayIds} pushUndo={pushUndo} canUndo={canUndo} lastActionLabel={lastActionLabel} onUndo={handleUndo} onEditTransport={can('day_edit', trip) ? (reservation) => { setEditingTransport(reservation); setTransportModalDayId(reservation.day_id ?? null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onEditReservation={can('reservation_edit', trip) ? (r) => { setEditingReservation(r); setShowReservationModal(true); setMobileSidebarOpen(null) } : undefined} initialScrollTop={mobilePlanScrollTopRef.current} onScrollTopChange={(top) => { mobilePlanScrollTopRef.current = top }} showRouteToolsWhenExpanded isMobile /> + ? { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId) }} onReorder={handleReorder} onReorderDays={handleReorderDays} onAddDay={handleAddDay} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute([r.coordinates]); setRouteInfo(r) } }} reservations={reservations} visibleConnectionIds={visibleConnections} onToggleConnection={toggleConnection} allConnectionsShown={allConnectionsShown} onToggleAllConnections={toggleAllConnections} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onAddTransport={can('day_edit', trip) ? (dayId) => { setTransportModalDayId(dayId); setEditingTransport(null); setTransitPrefill(null); setTransportModalAutomated(false); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onOpenTransit={(r) => { setTransitJourney(r); setMobileSidebarOpen(null) }} onPlanTransit={can('day_edit', trip) && tripHasDates ? (dayId) => { setTransportModalDayId(dayId); setEditingTransport(null); setTransitPrefill(null); setTransportModalAutomated(true); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); selectAssignment(null) }} onRemoveAssignment={handleRemoveAssignment} onEditPlace={(place, assignmentId) => { setEditingPlace(place); setEditingAssignmentId(assignmentId || null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} accommodations={tripAccommodations} routeShown={routeShown} routeProfile={routeProfile} onToggleRoute={() => setRouteShown(v => !v)} onSetRouteProfile={setRouteProfile} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} onExpandedDaysChange={setExpandedDayIds} pushUndo={pushUndo} canUndo={canUndo} lastActionLabel={lastActionLabel} onUndo={handleUndo} onEditTransport={can('day_edit', trip) ? (reservation) => { setEditingTransport(reservation); setTransportModalDayId(reservation.day_id ?? null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onEditReservation={can('reservation_edit', trip) ? (r) => { setEditingReservation(r); setShowReservationModal(true); setMobileSidebarOpen(null) } : undefined} initialScrollTop={mobilePlanScrollTopRef.current} onScrollTopChange={(top) => { mobilePlanScrollTopRef.current = top }} showRouteToolsWhenExpanded isMobile /> : { handlePlaceClick(placeId); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} onEditPlace={(place) => { openPlaceEditor(place); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} onBulkDeletePlaces={(ids) => setDeletePlaceIds(ids)} onBulkDeleteConfirm={(ids) => confirmDeletePlaces(ids)} onBulkChangeCategory={(ids, catId) => confirmChangeCategory(ids, catId)} days={days} isMobile pushUndo={pushUndo} initialScrollTop={mobilePlacesScrollTopRef.current} onScrollTopChange={(top) => { mobilePlacesScrollTopRef.current = top }} /> }
diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index 965f2042e..7841472ea 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -27,6 +27,12 @@ import { usePluginStore } from '../../store/pluginStore' import type { Accommodation, TripMember, Day, Place, Reservation } from '../../types' import { DEFAULT_MAP_LAT, DEFAULT_MAP_LNG, DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults' import { resolvePoolAssignmentId } from './tripPlannerModel' +import { isRoutableReservation } from '../../utils/reservationRoutes' +import { + parseStoredConnections, resolveEffectiveConnections, resolveVisibleConnectionIds, + toggleConnectionId, toggleAllConnections as flipAllConnectionsMode, + type StoredConnections, +} from '../../utils/connectionsVisibility' /** * Trip planner page logic — the big one. Owns the trip store wiring, addon @@ -249,20 +255,39 @@ export function useTripPlanner() { }, []) const connectionsStorageKey = tripId ? `trek:visible-connections:${tripId}` : null - const [visibleConnections, setVisibleConnections] = useState(() => { - if (typeof window === 'undefined' || !connectionsStorageKey) return [] - try { - const stored = window.localStorage.getItem(connectionsStorageKey) - return stored ? JSON.parse(stored) as number[] : [] - } catch { return [] } + // Per-trip route-visibility preference — null means "never touched", which + // falls back to the account-wide map_always_show_routes default (see + // connectionsVisibility.ts). That fallback is purely computed, never + // written, so flipping the account setting later doesn't silently override + // a trip you've already made an explicit choice on. + const [storedConnections, setStoredConnections] = useState(() => { + if (typeof window === 'undefined' || !connectionsStorageKey) return null + return parseStoredConnections(window.localStorage.getItem(connectionsStorageKey)) }) useEffect(() => { - if (typeof window === 'undefined' || !connectionsStorageKey) return - window.localStorage.setItem(connectionsStorageKey, JSON.stringify(visibleConnections)) - }, [connectionsStorageKey, visibleConnections]) + if (typeof window === 'undefined' || !connectionsStorageKey || !storedConnections) return + window.localStorage.setItem(connectionsStorageKey, JSON.stringify(storedConnections)) + }, [connectionsStorageKey, storedConnections]) + const alwaysShowRoutesDefault = settings.map_always_show_routes === true + const routableReservationIds = useMemo( + () => reservations.filter(isRoutableReservation).map(r => r.id), + [reservations] + ) + const effectiveConnections = useMemo( + () => resolveEffectiveConnections(storedConnections, alwaysShowRoutesDefault), + [storedConnections, alwaysShowRoutesDefault] + ) + const visibleConnections = useMemo( + () => resolveVisibleConnectionIds(effectiveConnections, routableReservationIds), + [effectiveConnections, routableReservationIds] + ) + const allConnectionsShown = effectiveConnections.mode === 'all-except' const toggleConnection = useCallback((id: number) => { - setVisibleConnections(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]) - }, []) + setStoredConnections(prev => toggleConnectionId(prev, alwaysShowRoutesDefault, id)) + }, [alwaysShowRoutesDefault]) + const toggleAllConnections = useCallback(() => { + setStoredConnections(prev => flipAllConnectionsMode(prev, alwaysShowRoutesDefault)) + }, [alwaysShowRoutesDefault]) const [mapTransportDetail, setMapTransportDetail] = useState(null) const [isMobile, setIsMobile] = useState(() => window.innerWidth < 768) @@ -922,7 +947,7 @@ export function useTripPlanner() { routeShown, setRouteShown, routeProfile, setRouteProfile, fitKey, setFitKey, mobileSidebarOpen, setMobileSidebarOpen, mobilePlanScrollTopRef, mobilePlacesScrollTopRef, deletePlaceId, setDeletePlaceId, deletePlaceIds, setDeletePlaceIds, - visibleConnections, setVisibleConnections, toggleConnection, mapTransportDetail, setMapTransportDetail, + visibleConnections, toggleConnection, allConnectionsShown, toggleAllConnections, mapTransportDetail, setMapTransportDetail, isMobile, isTouch, expandedDayIds, setExpandedDayIds, mapPlaces, route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay, diff --git a/shared/src/i18n/ar/map.ts b/shared/src/i18n/ar/map.ts index 1bcadce5a..67800d935 100644 --- a/shared/src/i18n/ar/map.ts +++ b/shared/src/i18n/ar/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'متاحف وثقافة', 'poi.cat.nature': 'طبيعة وحدائق', 'poi.cat.activities': 'أنشطة', + 'map.showAllConnections': 'إظهار جميع مسارات الحجوزات', + 'map.hideAllConnections': 'إخفاء جميع مسارات الحجوزات', }; export default map; diff --git a/shared/src/i18n/br/map.ts b/shared/src/i18n/br/map.ts index 018759756..bc0341460 100644 --- a/shared/src/i18n/br/map.ts +++ b/shared/src/i18n/br/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Museus e cultura', 'poi.cat.nature': 'Natureza e parques', 'poi.cat.activities': 'Atividades', + 'map.showAllConnections': 'Mostrar todas as rotas de reservas', + 'map.hideAllConnections': 'Ocultar todas as rotas de reservas', }; export default map; diff --git a/shared/src/i18n/cs/map.ts b/shared/src/i18n/cs/map.ts index 85471b209..e76801428 100644 --- a/shared/src/i18n/cs/map.ts +++ b/shared/src/i18n/cs/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Muzea a kultura', 'poi.cat.nature': 'Příroda a parky', 'poi.cat.activities': 'Aktivity', + 'map.showAllConnections': 'Zobrazit všechny trasy rezervací', + 'map.hideAllConnections': 'Skrýt všechny trasy rezervací', }; export default map; diff --git a/shared/src/i18n/de/map.ts b/shared/src/i18n/de/map.ts index cd94e8be9..3029d6ed1 100644 --- a/shared/src/i18n/de/map.ts +++ b/shared/src/i18n/de/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Museen & Kultur', 'poi.cat.nature': 'Natur & Parks', 'poi.cat.activities': 'Aktivitäten', + 'map.showAllConnections': 'Alle Buchungsrouten anzeigen', + 'map.hideAllConnections': 'Alle Buchungsrouten ausblenden', }; export default map; diff --git a/shared/src/i18n/en/map.ts b/shared/src/i18n/en/map.ts index 74c348e21..34b859a3c 100644 --- a/shared/src/i18n/en/map.ts +++ b/shared/src/i18n/en/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Museums & culture', 'poi.cat.nature': 'Nature & parks', 'poi.cat.activities': 'Activities', + 'map.showAllConnections': 'Show all booking routes', + 'map.hideAllConnections': 'Hide all booking routes', }; export default map; diff --git a/shared/src/i18n/es/map.ts b/shared/src/i18n/es/map.ts index a4dbf85f2..ee98cf956 100644 --- a/shared/src/i18n/es/map.ts +++ b/shared/src/i18n/es/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Museos y cultura', 'poi.cat.nature': 'Naturaleza y parques', 'poi.cat.activities': 'Actividades', + 'map.showAllConnections': 'Mostrar todas las rutas de reservas', + 'map.hideAllConnections': 'Ocultar todas las rutas de reservas', }; export default map; diff --git a/shared/src/i18n/fr/map.ts b/shared/src/i18n/fr/map.ts index 3462064f3..4f465c7be 100644 --- a/shared/src/i18n/fr/map.ts +++ b/shared/src/i18n/fr/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Musées & culture', 'poi.cat.nature': 'Nature & parcs', 'poi.cat.activities': 'Activités', + 'map.showAllConnections': 'Afficher tous les itinéraires', + 'map.hideAllConnections': 'Masquer tous les itinéraires', }; export default map; diff --git a/shared/src/i18n/gr/map.ts b/shared/src/i18n/gr/map.ts index 8e973f04f..7f19b936e 100644 --- a/shared/src/i18n/gr/map.ts +++ b/shared/src/i18n/gr/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Μουσεία & πολιτισμός', 'poi.cat.nature': 'Φύση & πάρκα', 'poi.cat.activities': 'Δραστηριότητες', + 'map.showAllConnections': 'Εμφάνιση όλων των διαδρομών κρατήσεων', + 'map.hideAllConnections': 'Απόκρυψη όλων των διαδρομών κρατήσεων', }; export default map; diff --git a/shared/src/i18n/hu/map.ts b/shared/src/i18n/hu/map.ts index 8db116695..c66bd7bf5 100644 --- a/shared/src/i18n/hu/map.ts +++ b/shared/src/i18n/hu/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Múzeumok és kultúra', 'poi.cat.nature': 'Természet és parkok', 'poi.cat.activities': 'Programok', + 'map.showAllConnections': 'Összes foglalási útvonal megjelenítése', + 'map.hideAllConnections': 'Összes foglalási útvonal elrejtése', }; export default map; diff --git a/shared/src/i18n/id/map.ts b/shared/src/i18n/id/map.ts index 2bfbc188f..59710cd0a 100644 --- a/shared/src/i18n/id/map.ts +++ b/shared/src/i18n/id/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Museum & budaya', 'poi.cat.nature': 'Alam & taman', 'poi.cat.activities': 'Aktivitas', + 'map.showAllConnections': 'Tampilkan semua rute pemesanan', + 'map.hideAllConnections': 'Sembunyikan semua rute pemesanan', }; export default map; diff --git a/shared/src/i18n/it/map.ts b/shared/src/i18n/it/map.ts index e308c1024..cd4adbc12 100644 --- a/shared/src/i18n/it/map.ts +++ b/shared/src/i18n/it/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Musei e cultura', 'poi.cat.nature': 'Natura e parchi', 'poi.cat.activities': 'Attività', + 'map.showAllConnections': 'Mostra tutti i percorsi prenotati', + 'map.hideAllConnections': 'Nascondi tutti i percorsi prenotati', }; export default map; diff --git a/shared/src/i18n/ja/map.ts b/shared/src/i18n/ja/map.ts index b7eb503e3..2a7866d40 100644 --- a/shared/src/i18n/ja/map.ts +++ b/shared/src/i18n/ja/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': '美術館・文化施設', 'poi.cat.nature': '自然・公園', 'poi.cat.activities': 'アクティビティ', + 'map.showAllConnections': 'すべての予約ルートを表示', + 'map.hideAllConnections': 'すべての予約ルートを非表示', }; export default map; diff --git a/shared/src/i18n/ko/map.ts b/shared/src/i18n/ko/map.ts index 839f49045..bee566828 100644 --- a/shared/src/i18n/ko/map.ts +++ b/shared/src/i18n/ko/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': '박물관 & 문화', 'poi.cat.nature': '자연 & 공원', 'poi.cat.activities': '액티비티', + 'map.showAllConnections': '모든 예약 경로 표시', + 'map.hideAllConnections': '모든 예약 경로 숨기기', }; export default map; diff --git a/shared/src/i18n/nl/map.ts b/shared/src/i18n/nl/map.ts index 139f13659..d25105831 100644 --- a/shared/src/i18n/nl/map.ts +++ b/shared/src/i18n/nl/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Musea & cultuur', 'poi.cat.nature': 'Natuur & parken', 'poi.cat.activities': 'Activiteiten', + 'map.showAllConnections': 'Alle boekingsroutes tonen', + 'map.hideAllConnections': 'Alle boekingsroutes verbergen', }; export default map; diff --git a/shared/src/i18n/pl/map.ts b/shared/src/i18n/pl/map.ts index 4295a9b79..ae62a5627 100644 --- a/shared/src/i18n/pl/map.ts +++ b/shared/src/i18n/pl/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Muzea i kultura', 'poi.cat.nature': 'Przyroda i parki', 'poi.cat.activities': 'Aktywności', + 'map.showAllConnections': 'Pokaż wszystkie trasy rezerwacji', + 'map.hideAllConnections': 'Ukryj wszystkie trasy rezerwacji', }; export default map; diff --git a/shared/src/i18n/ru/map.ts b/shared/src/i18n/ru/map.ts index b6ad804ce..910eddd77 100644 --- a/shared/src/i18n/ru/map.ts +++ b/shared/src/i18n/ru/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Музеи и культура', 'poi.cat.nature': 'Природа и парки', 'poi.cat.activities': 'Развлечения', + 'map.showAllConnections': 'Показать все маршруты бронирований', + 'map.hideAllConnections': 'Скрыть все маршруты бронирований', }; export default map; diff --git a/shared/src/i18n/sv/map.ts b/shared/src/i18n/sv/map.ts index ace7aeeda..a244f4aec 100644 --- a/shared/src/i18n/sv/map.ts +++ b/shared/src/i18n/sv/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Museer och kultur', 'poi.cat.nature': 'Natur och parker', 'poi.cat.activities': 'Aktiviteter', + 'map.showAllConnections': 'Visa alla bokningsvägar', + 'map.hideAllConnections': 'Dölj alla bokningsvägar', }; export default map; diff --git a/shared/src/i18n/tr/map.ts b/shared/src/i18n/tr/map.ts index 4a0854d66..95e36b19a 100644 --- a/shared/src/i18n/tr/map.ts +++ b/shared/src/i18n/tr/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Müzeler ve kültür', 'poi.cat.nature': 'Doğa ve parklar', 'poi.cat.activities': 'Aktiviteler', + 'map.showAllConnections': 'Tüm rezervasyon rotalarını göster', + 'map.hideAllConnections': 'Tüm rezervasyon rotalarını gizle', }; export default map; diff --git a/shared/src/i18n/uk/map.ts b/shared/src/i18n/uk/map.ts index 894bebe93..9469d9b0c 100644 --- a/shared/src/i18n/uk/map.ts +++ b/shared/src/i18n/uk/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Музеї та культура', 'poi.cat.nature': 'Природа та парки', 'poi.cat.activities': 'Активності', + 'map.showAllConnections': 'Показати всі маршрути бронювань', + 'map.hideAllConnections': 'Приховати всі маршрути бронювань', }; export default map; diff --git a/shared/src/i18n/vi/map.ts b/shared/src/i18n/vi/map.ts index 63b69a1b1..52c4a8f54 100644 --- a/shared/src/i18n/vi/map.ts +++ b/shared/src/i18n/vi/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': 'Bảo tàng & văn hóa', 'poi.cat.nature': 'Thiên nhiên & công viên', 'poi.cat.activities': 'Các hoạt động', + 'map.showAllConnections': 'Hiển thị tất cả lộ trình đặt chỗ', + 'map.hideAllConnections': 'Ẩn tất cả lộ trình đặt chỗ', }; export default map; diff --git a/shared/src/i18n/zh-TW/map.ts b/shared/src/i18n/zh-TW/map.ts index 142312266..7afeab193 100644 --- a/shared/src/i18n/zh-TW/map.ts +++ b/shared/src/i18n/zh-TW/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': '博物館與文化', 'poi.cat.nature': '自然與公園', 'poi.cat.activities': '活動', + 'map.showAllConnections': '顯示所有預訂路線', + 'map.hideAllConnections': '隱藏所有預訂路線', }; export default map; diff --git a/shared/src/i18n/zh/map.ts b/shared/src/i18n/zh/map.ts index 002e32ae6..645bec625 100644 --- a/shared/src/i18n/zh/map.ts +++ b/shared/src/i18n/zh/map.ts @@ -13,5 +13,7 @@ const map: TranslationStrings = { 'poi.cat.museums': '博物馆与文化', 'poi.cat.nature': '自然与公园', 'poi.cat.activities': '活动', + 'map.showAllConnections': '显示所有预订路线', + 'map.hideAllConnections': '隐藏所有预订路线', }; export default map;