fix(planner): a booked hotel stays in the place list when road trip stops are hidden in Days

Since 4.3.0 every lodging booking types its place as 'hotel', and 'hotel'
is one of the road trip stop types. With "Show in Days too" switched off,
Days dropped every such place from the trip's place list, so a hotel
vanished from the sidebar, from the booking's own place picker and from
the Days map the moment it was booked, and a click on it in the day list
found nothing. Nothing was deleted: road trip mode kept showing it.

Days now leaves out only road trip stops nobody booked. A lodging place
stays, and so does any place a stay or a booking points at, on the
desktop and on the phone. Trips planned on 4.3.1 show their hotels again
without any change to their data.

An imported GPX or KML track is no longer offered as the place of a stay,
in the booking form, the day's hotel picker and the phone's sheets. A
stay that already names a track keeps showing it.
This commit is contained in:
Maurice
2026-09-23 21:28:34 +02:00
parent a1d6348c5f
commit b5457d9e6a
16 changed files with 225 additions and 15 deletions
@@ -1,4 +1,4 @@
// FE-PLANNER-DAYDETAIL-001 to FE-PLANNER-DAYDETAIL-084
// FE-PLANNER-DAYDETAIL-001 to FE-PLANNER-DAYDETAIL-085
import React from 'react';
import { fireEvent, render, screen, waitFor, within, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
@@ -520,6 +520,15 @@ describe('DayDetailPanel', () => {
expect(await screen.findByText('102 Quai de Jemmapes')).toBeInTheDocument();
});
it('FE-PLANNER-DAYDETAIL-085: the hotel picker leaves out imported tracks', async () => {
const hotel = buildPlace({ id: 10, name: 'Hotel du Nord' });
const track = buildPlace({ id: 12, name: 'Canal Saint-Martin walk', route_geometry: '[[48.87,2.36],[48.88,2.37]]' });
render(<DayDetailPanel {...defaultProps} places={[hotel, track]} />);
await userEvent.click(await screen.findByText(/Add accommodation/i));
await screen.findByText('Hotel du Nord');
expect(screen.queryByText('Canal Saint-Martin walk')).not.toBeInTheDocument();
});
it('FE-PLANNER-DAYDETAIL-030: selecting a place in hotel picker enables save button', async () => {
const place = buildPlace({ id: 10, name: 'Maison Blanche' });
server.use(
@@ -21,6 +21,7 @@ import type { Day, Place, Category, Reservation, AssignmentsMap } from '../../ty
import { isDayInAccommodationRange } from '../../utils/dayOrder'
import { formatClockTime, splitReservationDateTime } from '../../utils/formatters'
import { useDayDetail } from './useDayDetail'
import { stayPlaces } from '../../utils/stayPlaces'
const WEATHER_ICON_MAP = {
Clear: Sun, Clouds: Cloud, Rain: CloudRain, Drizzle: CloudDrizzle,
@@ -660,7 +661,8 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
{/* Place List */}
<div style={{ maxHeight: 250, overflowY: 'auto' }}>
{(() => {
const filtered = hotelCategoryFilter ? places.filter(p => p.category_id === hotelCategoryFilter) : places
const offered = stayPlaces<Place>(places, hotelForm.place_id)
const filtered = hotelCategoryFilter ? offered.filter(p => p.category_id === hotelCategoryFilter) : offered
return filtered.length === 0 ? (
<div style={{ padding: 20, textAlign: 'center', fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: 'var(--text-faint)' }}>{t('day.noPlacesForHotel')}</div>
) : filtered.map(p => (
@@ -1,4 +1,4 @@
// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-100
// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-102
import { render, screen, waitFor, fireEvent, within, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@@ -627,6 +627,29 @@ describe('ReservationModal', () => {
);
});
it('FE-PLANNER-RESMODAL-101: an imported track is not offered as the place of a stay', async () => {
const hotel = buildPlace({ id: 21, name: 'Hotel Adler' });
const track = buildPlace({ id: 22, name: 'Rheinsteig', route_geometry: '[[50.1,7.6],[50.2,7.7]]' });
render(<ReservationModal {...defaultProps} places={[hotel, track]} />);
await userEvent.click(screen.getByRole('button', { name: /Accommodation/i }));
const field = screen.getAllByText('Accommodation').find(el => el.tagName === 'LABEL')!.parentElement!;
await userEvent.click(within(field).getByRole('button'));
expect(screen.getByText('Hotel Adler')).toBeInTheDocument();
expect(screen.queryByText('Rheinsteig')).not.toBeInTheDocument();
});
it('FE-PLANNER-RESMODAL-102: a stay already booked at a track still shows that track', () => {
const track = buildPlace({ id: 22, name: 'Rheinsteig', route_geometry: '[[50.1,7.6],[50.2,7.7]]' });
const days = [buildDay({ id: 1 }), buildDay({ id: 2 })];
const accommodations = [{ id: 7, trip_id: 1, place_id: 22, start_day_id: 1, end_day_id: 2 }];
const res = buildReservation({ id: 9, type: 'hotel', title: 'Hut', accommodation_id: 7 });
render(<ReservationModal {...defaultProps} days={days} places={[track]} accommodations={accommodations as never} reservation={res} />);
expect(screen.getByText('Rheinsteig')).toBeInTheDocument();
});
it('FE-PLANNER-RESMODAL-043: hover styles applied to file picker items', async () => {
const res = buildReservation({ id: 5 });
const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });
@@ -24,6 +24,7 @@ import type { TripMember } from '../Budget/BudgetPanelMemberChips'
import type { BookingExpenseRequest } from './BookingCostsSection.types'
import type { BookingReviewDraft } from './parsedItemToDraft'
import { typeToCostCategory } from '@trek/shared'
import { stayPlaces } from '../../utils/stayPlaces'
const TYPE_OPTIONS = [
{ value: 'hotel', labelKey: 'reservations.type.hotel', Icon: Hotel },
@@ -601,7 +602,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
placeholder={t('reservations.meta.pickHotel')}
options={[
{ value: '', label: '—' },
...places.map(p => ({ value: p.id, label: p.name })),
...stayPlaces(places, form.hotel_place_id).map(p => ({ value: p.id, label: p.name })),
]}
searchable
size="sm"