mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-07-18 19:36:02 +00:00
feat(mobile): make the bottom-nav "+" context-aware per trip tab (#1349)
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=<intent> 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
This commit is contained in:
@@ -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', () => ({
|
vi.mock('../../api/websocket', () => ({
|
||||||
connect: vi.fn(),
|
connect: vi.fn(),
|
||||||
@@ -30,6 +30,7 @@ const currentUser = buildUser({ id: 1, username: 'testuser', email: 'test@exampl
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetAllStores();
|
resetAllStores();
|
||||||
mockNavigate.mockClear();
|
mockNavigate.mockClear();
|
||||||
|
sessionStorage.clear();
|
||||||
seedStore(useAuthStore, { user: currentUser, isAuthenticated: true });
|
seedStore(useAuthStore, { user: currentUser, isAuthenticated: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,4 +80,37 @@ describe('BottomNav', () => {
|
|||||||
render(<BottomNav />);
|
render(<BottomNav />);
|
||||||
expect(screen.queryByText('Foo Addon')).not.toBeInTheDocument();
|
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(<BottomNav />, { 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(<BottomNav />, { 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(<BottomNav />, { 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(<BottomNav />, { initialEntries: ['/trips/42'] });
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Add expense' }));
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/trips/42?create=expense');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,12 +25,15 @@ function useCreateAction(): { label: string; run: () => void } {
|
|||||||
const onJourneyList = useMatch('/journey')
|
const onJourneyList = useMatch('/journey')
|
||||||
|
|
||||||
if (inTrip) {
|
if (inTrip) {
|
||||||
// On the Costs tab the "+" adds an expense; otherwise it adds a place.
|
// The "+" is context-aware per active tab: Bookings → reservation,
|
||||||
const tripTab = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(`trip-tab-${inTrip.params.id}`) : null
|
// Transports → transport, Costs → expense. Tabs without a create modal
|
||||||
if (tripTab === 'finanzplan') {
|
// (lists / files / collab) fall through to adding a place. #1349
|
||||||
return { label: t('costs.addExpense'), run: () => navigate(`/trips/${inTrip.params.id}?create=expense`) }
|
const id = inTrip.params.id
|
||||||
}
|
const tripTab = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(`trip-tab-${id}`) : null
|
||||||
return { label: t('places.addPlace'), run: () => navigate(`/trips/${inTrip.params.id}?create=place`) }
|
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) {
|
if (inJourney) {
|
||||||
return { label: t('journey.detail.addEntry'), run: () => navigate(`/journey/${inJourney.params.id}?create=entry`) }
|
return { label: t('journey.detail.addEntry'), run: () => navigate(`/journey/${inJourney.params.id}?create=entry`) }
|
||||||
|
|||||||
@@ -161,6 +161,20 @@ export function useTripPlanner() {
|
|||||||
const [showTransportModal, setShowTransportModal] = useState<boolean>(false)
|
const [showTransportModal, setShowTransportModal] = useState<boolean>(false)
|
||||||
const [editingTransport, setEditingTransport] = useState<Reservation | null>(null)
|
const [editingTransport, setEditingTransport] = useState<Reservation | null>(null)
|
||||||
const [transportModalDayId, setTransportModalDayId] = useState<number | null>(null)
|
const [transportModalDayId, setTransportModalDayId] = useState<number | null>(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
|
// 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).
|
// the user checks/fixes it, then saves. A ref drives the queue (no stale closures).
|
||||||
const [reservationPrefill, setReservationPrefill] = useState<BookingReviewDraft | null>(null)
|
const [reservationPrefill, setReservationPrefill] = useState<BookingReviewDraft | null>(null)
|
||||||
|
|||||||
Reference in New Issue
Block a user