From c7e8a5614d3abe73107de53974d5c61614343e15 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 28 Jun 2026 15:23:54 +0200 Subject: [PATCH] feat(mobile): make the bottom-nav "+" context-aware per trip tab (#1349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On mobile the bottom-nav "+" always created a new place (except on the Costs tab, where it added an expense). It now matches the active trip tab: Bookings adds a reservation, Transports adds a transport, Costs adds an expense, and everything else (Plan, plus tabs that have no create modal — Lists / Files / Collab) keeps adding a place. Follows the existing ?create= pattern: BottomNav.useCreateAction emits the per-tab intent, and useTripPlanner consumes create=reservation|transport to open the booking / transport modals (both already mounted at page level). Place and expense were already wired; this just extends the mapping. Tests: 4 new BottomNav cases (plan/bookings/transports/costs → correct intent + navigate target); client tsc clean, full client suite green (2855). Implements mauriceboe/TREK#1349 --- .../src/components/Layout/BottomNav.test.tsx | 36 ++++++++++++++++++- client/src/components/Layout/BottomNav.tsx | 15 ++++---- .../src/pages/tripPlanner/useTripPlanner.ts | 14 ++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/client/src/components/Layout/BottomNav.test.tsx b/client/src/components/Layout/BottomNav.test.tsx index 0647ec6f..fadcbed2 100644 --- a/client/src/components/Layout/BottomNav.test.tsx +++ b/client/src/components/Layout/BottomNav.test.tsx @@ -1,4 +1,4 @@ -// FE-COMP-BOTTOMNAV-001 to FE-COMP-BOTTOMNAV-006 +// FE-COMP-BOTTOMNAV-001 to FE-COMP-BOTTOMNAV-010 vi.mock('../../api/websocket', () => ({ connect: vi.fn(), @@ -30,6 +30,7 @@ const currentUser = buildUser({ id: 1, username: 'testuser', email: 'test@exampl beforeEach(() => { resetAllStores(); mockNavigate.mockClear(); + sessionStorage.clear(); seedStore(useAuthStore, { user: currentUser, isAuthenticated: true }); }); @@ -79,4 +80,37 @@ describe('BottomNav', () => { render(); expect(screen.queryByText('Foo Addon')).not.toBeInTheDocument(); }); + + // Context-aware "+" inside a trip — #1349 + it('FE-COMP-BOTTOMNAV-007: in a trip, the "+" adds a place by default (plan tab)', async () => { + const user = userEvent.setup(); + sessionStorage.setItem('trip-tab-42', 'plan'); + render(, { initialEntries: ['/trips/42'] }); + await user.click(screen.getByRole('button', { name: 'Add Place/Activity' })); + expect(mockNavigate).toHaveBeenCalledWith('/trips/42?create=place'); + }); + + it('FE-COMP-BOTTOMNAV-008: Bookings tab → "+" creates a reservation', async () => { + const user = userEvent.setup(); + sessionStorage.setItem('trip-tab-42', 'buchungen'); + render(, { initialEntries: ['/trips/42'] }); + await user.click(screen.getByRole('button', { name: 'Manual Booking' })); + expect(mockNavigate).toHaveBeenCalledWith('/trips/42?create=reservation'); + }); + + it('FE-COMP-BOTTOMNAV-009: Transports tab → "+" creates a transport', async () => { + const user = userEvent.setup(); + sessionStorage.setItem('trip-tab-42', 'transports'); + render(, { initialEntries: ['/trips/42'] }); + await user.click(screen.getByRole('button', { name: 'Manual Transport' })); + expect(mockNavigate).toHaveBeenCalledWith('/trips/42?create=transport'); + }); + + it('FE-COMP-BOTTOMNAV-010: Costs tab → "+" creates an expense', async () => { + const user = userEvent.setup(); + sessionStorage.setItem('trip-tab-42', 'finanzplan'); + render(, { initialEntries: ['/trips/42'] }); + await user.click(screen.getByRole('button', { name: 'Add expense' })); + expect(mockNavigate).toHaveBeenCalledWith('/trips/42?create=expense'); + }); }); diff --git a/client/src/components/Layout/BottomNav.tsx b/client/src/components/Layout/BottomNav.tsx index 04b74c6a..5eb75cfc 100644 --- a/client/src/components/Layout/BottomNav.tsx +++ b/client/src/components/Layout/BottomNav.tsx @@ -25,12 +25,15 @@ function useCreateAction(): { label: string; run: () => void } { const onJourneyList = useMatch('/journey') if (inTrip) { - // On the Costs tab the "+" adds an expense; otherwise it adds a place. - const tripTab = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(`trip-tab-${inTrip.params.id}`) : null - if (tripTab === 'finanzplan') { - return { label: t('costs.addExpense'), run: () => navigate(`/trips/${inTrip.params.id}?create=expense`) } - } - return { label: t('places.addPlace'), run: () => navigate(`/trips/${inTrip.params.id}?create=place`) } + // The "+" is context-aware per active tab: Bookings → reservation, + // Transports → transport, Costs → expense. Tabs without a create modal + // (lists / files / collab) fall through to adding a place. #1349 + const id = inTrip.params.id + const tripTab = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(`trip-tab-${id}`) : null + if (tripTab === 'finanzplan') return { label: t('costs.addExpense'), run: () => navigate(`/trips/${id}?create=expense`) } + if (tripTab === 'buchungen') return { label: t('reservations.addManual'), run: () => navigate(`/trips/${id}?create=reservation`) } + if (tripTab === 'transports') return { label: t('transport.addManual'), run: () => navigate(`/trips/${id}?create=transport`) } + return { label: t('places.addPlace'), run: () => navigate(`/trips/${id}?create=place`) } } if (inJourney) { return { label: t('journey.detail.addEntry'), run: () => navigate(`/journey/${inJourney.params.id}?create=entry`) } diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index 3017628e..7a354909 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -161,6 +161,20 @@ export function useTripPlanner() { const [showTransportModal, setShowTransportModal] = useState(false) const [editingTransport, setEditingTransport] = useState(null) const [transportModalDayId, setTransportModalDayId] = useState(null) + + // The bottom-nav "+" is context-aware per tab: on the Bookings / Transports tabs + // it opens the booking / transport modal via ?create=reservation|transport + // (place is handled above, expense in CostsPanel). #1349 + useEffect(() => { + const intent = searchParams.get('create') + if (intent === 'reservation') { + setEditingReservation(null); setBookingForAssignmentId(null); setShowReservationModal(true) + setSearchParams(p => { p.delete('create'); return p }, { replace: true }) + } else if (intent === 'transport') { + setEditingTransport(null); setTransportModalDayId(null); setShowTransportModal(true) + setSearchParams(p => { p.delete('create'); return p }, { replace: true }) + } + }, [searchParams]) // Review-before-save import: each parsed item pre-fills the normal edit modal so // the user checks/fixes it, then saves. A ref drives the queue (no stale closures). const [reservationPrefill, setReservationPrefill] = useState(null)