mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 10:41:49 +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', () => ({
|
||||
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(<BottomNav />);
|
||||
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')
|
||||
|
||||
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`) }
|
||||
|
||||
@@ -161,6 +161,20 @@ export function useTripPlanner() {
|
||||
const [showTransportModal, setShowTransportModal] = useState<boolean>(false)
|
||||
const [editingTransport, setEditingTransport] = useState<Reservation | 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
|
||||
// the user checks/fixes it, then saves. A ref drives the queue (no stale closures).
|
||||
const [reservationPrefill, setReservationPrefill] = useState<BookingReviewDraft | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user