mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { applyStayStops } from '../../../../store/stayStops'
|
||||
import { useTranslation } from '../../../../i18n'
|
||||
import { Eyebrow } from './MTripSheetUi'
|
||||
import type { MTripSheetsProps } from '../MTripShell'
|
||||
import { stayPlaces } from '../../../../utils/stayPlaces'
|
||||
|
||||
interface AccommodationPayload {
|
||||
dayId?: number
|
||||
@@ -97,7 +98,8 @@ export default function MAccommodationSheet({ planner, shell }: MTripSheetsProps
|
||||
: (d.title ? t('planner.dayN', { n: i + 1 }) : undefined),
|
||||
}))
|
||||
|
||||
const filteredPlaces = categoryFilter != null ? places.filter(p => p.category_id === categoryFilter) : places
|
||||
const offeredPlaces = stayPlaces(places, form.place_id)
|
||||
const filteredPlaces = categoryFilter != null ? offeredPlaces.filter(p => p.category_id === categoryFilter) : offeredPlaces
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true)
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useTripStore } from '../../../../store/tripStore'
|
||||
import type { TripMember } from '../../../../types'
|
||||
import type { BookingExpenseRequest } from '../../../../components/Planner/BookingCostsSection.types'
|
||||
import type { TripPlanner } from '../MTripShell'
|
||||
import { stayPlaces } from '../../../../utils/stayPlaces'
|
||||
|
||||
export interface MReservationSheetProps {
|
||||
planner: TripPlanner
|
||||
@@ -212,6 +213,7 @@ export default function MReservationSheet({ planner, onOpenExpense }: MReservati
|
||||
d ? new Date(`${d.slice(0, 10)}T00:00:00Z`).toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) : undefined
|
||||
|
||||
const placeOptions = [{ value: '', label: '—' }, ...places.map(p => ({ value: p.id, label: p.name }))]
|
||||
const hotelPlaceOptions = [{ value: '', label: '—' }, ...stayPlaces(places, form.hotel_place_id).map(p => ({ value: p.id, label: p.name }))]
|
||||
const dayOptions = days.map(d => ({
|
||||
value: d.id,
|
||||
label: d.title || t('dayplan.dayN', { n: d.day_number }),
|
||||
@@ -449,7 +451,7 @@ export default function MReservationSheet({ planner, onOpenExpense }: MReservati
|
||||
return next
|
||||
})
|
||||
}}
|
||||
options={placeOptions}
|
||||
options={hotelPlaceOptions}
|
||||
placeholder={t('reservations.meta.pickHotel')}
|
||||
searchable
|
||||
size="sm"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { resolvePoolAssignmentId } from './tripPlannerModel'
|
||||
import { placesForDays, resolvePoolAssignmentId } from './tripPlannerModel'
|
||||
import { buildAssignment, buildPlace } from '../../../tests/helpers/factories'
|
||||
|
||||
describe('resolvePoolAssignmentId', () => {
|
||||
@@ -23,3 +23,35 @@ describe('resolvePoolAssignmentId', () => {
|
||||
expect(resolvePoolAssignmentId(assignments, 7)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('placesForDays', () => {
|
||||
const town = buildPlace({ id: 1, stop_type: null })
|
||||
const hotel = buildPlace({ id: 2, stop_type: 'hotel' })
|
||||
const pump = buildPlace({ id: 3, stop_type: 'fuel' })
|
||||
const brauhaus = buildPlace({ id: 4, stop_type: 'restaurant' })
|
||||
const campsite = buildPlace({ id: 5, stop_type: 'campsite' })
|
||||
const all = [town, hotel, pump, brauhaus, campsite]
|
||||
const none = { accommodations: [], reservations: [] }
|
||||
|
||||
it('FE-TP-MODEL-001: hands the list back untouched while road trip stops show in Days', () => {
|
||||
expect(placesForDays(all, false, none)).toBe(all)
|
||||
})
|
||||
|
||||
it('FE-TP-MODEL-002: leaves out every road trip stop nobody booked, but never a lodging', () => {
|
||||
// The hotel's booking may be gone; the 'hotel' type it stamped on the place is not.
|
||||
expect(placesForDays(all, true, none).map(p => p.id)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('FE-TP-MODEL-003: keeps the place a stay is at, whatever type it carries', () => {
|
||||
const kept = placesForDays(all, true, { accommodations: [{ place_id: 5 }, { place_id: null }], reservations: [] })
|
||||
expect(kept.map(p => p.id)).toEqual([1, 2, 5])
|
||||
})
|
||||
|
||||
it('FE-TP-MODEL-004: keeps the place a booking names, directly or through its stay', () => {
|
||||
const kept = placesForDays(all, true, {
|
||||
accommodations: [],
|
||||
reservations: [{ place_id: 4 }, { accommodation_place_id: 5 }, { place_id: null, accommodation_place_id: null }],
|
||||
})
|
||||
expect(kept.map(p => p.id)).toEqual([1, 2, 4, 5])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,8 +4,42 @@
|
||||
* the FE "page = wiring container + data hook" convention (see PATTERN.md).
|
||||
*/
|
||||
|
||||
import { isServiceStopType } from '../../components/Roadtrip/roadtripModel'
|
||||
import type { Assignment } from '../../types'
|
||||
|
||||
/** What a stay or a booking says about the place it is for. */
|
||||
export interface BookedPlaceSources {
|
||||
accommodations: { place_id?: number | null }[]
|
||||
reservations: { place_id?: number | null; accommodation_place_id?: number | null }[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The trip's places as Days shows them.
|
||||
*
|
||||
* With "Show in Days too" switched off, a place typed as a road trip stop (a pump, a
|
||||
* rest area, a campsite) belongs to the drive and stays out of Days, list and map alike.
|
||||
* A place a stay or a booking points at is not such a stop, whatever its type says, and
|
||||
* neither is lodging: every lodging booking types its place as 'hotel', the type stays
|
||||
* after the booking is gone, and dropping it took the booked hotel out of the place
|
||||
* list, the booking's own place picker and the Days map the moment it was booked.
|
||||
*/
|
||||
export function placesForDays<P extends { id: number; stop_type?: string | null }>(
|
||||
places: P[],
|
||||
hideRoadtripStops: boolean,
|
||||
{ accommodations, reservations }: BookedPlaceSources,
|
||||
): P[] {
|
||||
if (!hideRoadtripStops) return places
|
||||
const booked = new Set<number>()
|
||||
for (const stay of accommodations) {
|
||||
if (stay.place_id != null) booked.add(stay.place_id)
|
||||
}
|
||||
for (const reservation of reservations) {
|
||||
if (reservation.place_id != null) booked.add(reservation.place_id)
|
||||
if (reservation.accommodation_place_id != null) booked.add(reservation.accommodation_place_id)
|
||||
}
|
||||
return places.filter(place => !isServiceStopType(place.stop_type) || place.stop_type === 'hotel' || booked.has(place.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the day-assignment to use when a place is edited from the Places pool,
|
||||
* where no day is in context. Times live per day-assignment (#1247), so we can
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { roadtripPreferencesRepo } from '../../repo/roadtripPreferencesRepo'
|
||||
// FE-TP-ROAD-001 to FE-TP-ROAD-153
|
||||
// FE-TP-ROAD-001 to FE-TP-ROAD-154
|
||||
import React from 'react'
|
||||
import { renderHook, act, waitFor } from '@testing-library/react'
|
||||
import { TranslationProvider } from '../../i18n/TranslationContext'
|
||||
@@ -2293,6 +2293,38 @@ it('preserves exclusive service stops when reordering the visible Days stops', a
|
||||
expect(actions.reorderAssignments).toHaveBeenCalledWith(42, 5, [13, 12, 11])
|
||||
})
|
||||
|
||||
it('FE-TP-ROAD-154: a place a booking points at stays in Days while road trip stops are hidden there', async () => {
|
||||
// Every lodging booking types its place as 'hotel', and 'hotel' is a road trip stop
|
||||
// type. With "Show in Days too" off, Days dropped the booked hotel from the place list,
|
||||
// from the booking's own place picker and from the map the moment it was booked.
|
||||
const town = buildPlace({ id: 101, name: 'Cologne' })
|
||||
const hotel = buildPlace({ id: 102, name: 'Cologne Cathedral', stop_type: 'hotel' })
|
||||
const pump = buildPlace({ id: 103, name: 'Pump', stop_type: 'fuel' })
|
||||
const brauhaus = buildPlace({ id: 104, name: 'Brauhaus', stop_type: 'restaurant' })
|
||||
const campsite = buildPlace({ id: 105, name: 'Camping Rhein', stop_type: 'campsite' })
|
||||
seedTrip({
|
||||
places: [town, hotel, pump, brauhaus, campsite],
|
||||
days: [buildDay({ id: 5 }), buildDay({ id: 6 })],
|
||||
reservations: [
|
||||
buildReservation({ id: 9, type: 'restaurant', place_id: 104, day_id: 5 }),
|
||||
buildReservation({ id: 10, type: 'hotel', accommodation_id: 8, accommodation_place_id: 105 }),
|
||||
],
|
||||
})
|
||||
vi.mocked(accommodationRepo.list).mockResolvedValue({
|
||||
accommodations: [{ id: 7, trip_id: 42, place_id: 102, start_day_id: 5, end_day_id: 6, reservation_title: null }],
|
||||
} as never)
|
||||
useSettingsStore.setState(s => ({ settings: { ...s.settings, roadtrip_service_stops_in_days: false } }))
|
||||
const { result } = await renderRoadtrip()
|
||||
|
||||
act(() => result.current.toggleRoadtripMode())
|
||||
|
||||
// The pump nobody booked stays a road trip stop, out of Days.
|
||||
await waitFor(() => expect(result.current.places.map(p => p.id)).toEqual([101, 102, 104, 105]))
|
||||
// Road trip mode keeps every place, as before.
|
||||
act(() => result.current.toggleRoadtripMode())
|
||||
expect(result.current.places.map(p => p.id)).toEqual([101, 102, 103, 104, 105])
|
||||
})
|
||||
|
||||
/**
|
||||
* The phone's own way into the drive.
|
||||
*
|
||||
|
||||
@@ -67,7 +67,7 @@ import type { Accommodation, Assignment, TripMember, Day, Place, Reservation } f
|
||||
import { OFM_POSITRON, DEFAULT_MAP_LAT, DEFAULT_MAP_LNG, DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults'
|
||||
import { useTileUrl } from '../../hooks/useTileUrl'
|
||||
import { applyStayStops } from '../../store/stayStops'
|
||||
import { resolvePoolAssignmentId } from './tripPlannerModel'
|
||||
import { placesForDays, resolvePoolAssignmentId } from './tripPlannerModel'
|
||||
import { isDeepLinkableTripTab, TRIP_TAB_LABEL_KEYS } from '../../constants/tripTabs'
|
||||
import { isRoutableReservation } from '../../utils/reservationRoutes'
|
||||
import {
|
||||
@@ -175,7 +175,6 @@ export function useTripPlanner() {
|
||||
Object.entries(storedAssignments).map(([dayId, visits]) => [dayId, visits.filter(v => !hidden(v))]),
|
||||
)
|
||||
}, [roadtripMode, roadtripSettings.roadtrip_service_stops_in_days, storedAssignments])
|
||||
const places = useMemo(() => roadtripMode || roadtripSettings.roadtrip_service_stops_in_days !== false ? allPlaces : allPlaces.filter(place => !isServiceStopType(place.stop_type)), [roadtripMode, roadtripSettings.roadtrip_service_stops_in_days, allPlaces])
|
||||
const toggleRoadtripMode = useCallback(() => {
|
||||
setRoadtripMode(prev => {
|
||||
const next = !prev
|
||||
@@ -185,6 +184,12 @@ export function useTripPlanner() {
|
||||
}, [tripId])
|
||||
const [collabFeatures, setCollabFeatures] = useState<{ chat: boolean; notes: boolean; links: boolean; polls: boolean; whatsnext: boolean }>({ chat: true, notes: true, links: true, polls: true, whatsnext: true })
|
||||
const [tripAccommodations, setTripAccommodations] = useState<Accommodation[]>([])
|
||||
const places = useMemo(
|
||||
() => (roadtripMode
|
||||
? allPlaces
|
||||
: placesForDays(allPlaces, roadtripSettings.roadtrip_service_stops_in_days === false, { accommodations: tripAccommodations, reservations })),
|
||||
[roadtripMode, roadtripSettings.roadtrip_service_stops_in_days, allPlaces, tripAccommodations, reservations],
|
||||
)
|
||||
const [allowedFileTypes, setAllowedFileTypes] = useState<string | null>(null)
|
||||
const [tripMembers, setTripMembers] = useState<TripMember[]>([])
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { stayPlaces } from './stayPlaces'
|
||||
|
||||
// FE-UTIL-STAYPLACES-001 to FE-UTIL-STAYPLACES-003
|
||||
|
||||
describe('stayPlaces', () => {
|
||||
const hotel = { id: 1, route_geometry: null }
|
||||
const hut = { id: 2 }
|
||||
const track = { id: 3, route_geometry: '[[50.1,7.6],[50.2,7.7]]' }
|
||||
|
||||
it('FE-UTIL-STAYPLACES-001: offers every place but an imported track', () => {
|
||||
expect(stayPlaces([hotel, hut, track]).map(p => p.id)).toEqual([1, 2])
|
||||
expect(stayPlaces([hotel, hut, track], '').map(p => p.id)).toEqual([1, 2])
|
||||
expect(stayPlaces([hotel, hut, track], null).map(p => p.id)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('FE-UTIL-STAYPLACES-002: keeps the track a stay already names, by number or by text', () => {
|
||||
expect(stayPlaces([hotel, track], 3).map(p => p.id)).toEqual([1, 3])
|
||||
expect(stayPlaces([hotel, track], '3').map(p => p.id)).toEqual([1, 3])
|
||||
})
|
||||
|
||||
it('FE-UTIL-STAYPLACES-003: a stay at an ordinary place lets no track in', () => {
|
||||
expect(stayPlaces([hotel, track], 1).map(p => p.id)).toEqual([1])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* The places a stay can be booked at: every place but an imported GPX or KML track,
|
||||
* which is a route to follow, not somewhere to sleep.
|
||||
*
|
||||
* The place a stay already names stays in the list, track or not, so a booking made
|
||||
* before tracks were left out still shows what it points at and saves unchanged.
|
||||
*/
|
||||
export function stayPlaces<P extends { id: number; route_geometry?: string | null }>(
|
||||
places: P[],
|
||||
currentPlaceId?: number | string | null,
|
||||
): P[] {
|
||||
return places.filter(place => !place.route_geometry
|
||||
|| (currentPlaceId != null && currentPlaceId !== '' && String(place.id) === String(currentPlaceId)))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { useSettingsStore } from '../../../../src/store/settingsStore'
|
||||
import { isBlurred } from '../../../helpers/bookingCodeBlur'
|
||||
import { act, fireEvent, render, screen, waitFor } from '../../../helpers/render'
|
||||
|
||||
// FE-MOB-ACCSH-001 to FE-MOB-ACCSH-029
|
||||
// FE-MOB-ACCSH-001 to FE-MOB-ACCSH-031
|
||||
|
||||
// The pickers have their own suites; here they only need to be addressable, so
|
||||
// they render as plain controls. data-value keeps the requested value readable
|
||||
@@ -395,4 +395,17 @@ describe('MAccommodationSheet', () => {
|
||||
expect(update.mock.calls[0][2]).toMatchObject({ confirmation: 'ABC-1' })
|
||||
})
|
||||
})
|
||||
|
||||
it('FE-MOB-ACCSH-030: an imported track is not offered as the place of a stay', () => {
|
||||
const track = { id: 103, name: 'Donauradweg', address: null, category_id: null, image_url: null, route_geometry: '[[48.2,16.3],[48.3,16.4]]' }
|
||||
setup(makePlanner({ places: [...PLACES, track] }))
|
||||
expect(screen.getByText('Hotel Sacher')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Donauradweg')).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-MOB-ACCSH-031: a stay already at a track still lists that track', () => {
|
||||
const track = { id: 103, name: 'Donauradweg', address: null, category_id: null, image_url: null, route_geometry: '[[48.2,16.3],[48.3,16.4]]' }
|
||||
setup(makePlanner({ places: [...PLACES, track], tripAccommodations: [{ ...EXISTING, place_id: 103 }] }), makeShell({ dayId: 12, accId: 77 }))
|
||||
expect(placeRow('Donauradweg')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import { buildPlanner } from '../../../helpers/mobileTrip'
|
||||
import { resetAllStores } from '../../../helpers/store'
|
||||
import { act, fireEvent, render, screen, waitFor } from '../../../helpers/render'
|
||||
|
||||
// FE-MOB-RESSH-001 to FE-MOB-RESSH-061
|
||||
// FE-MOB-RESSH-001 to FE-MOB-RESSH-063
|
||||
|
||||
// Date/time/select pickers have their own suites — here they only have to be
|
||||
// addressable, so they render as plain controls.
|
||||
@@ -469,6 +469,20 @@ describe('MReservationSheet', () => {
|
||||
expect(screen.getByPlaceholderText('reservations.locationPlaceholder')).toHaveValue('Philharmonikerstrasse 4')
|
||||
})
|
||||
|
||||
it('FE-MOB-RESSH-062: an imported track is not offered as the place of a stay', () => {
|
||||
const track = { id: 103, name: 'Donauradweg', address: null, route_geometry: '[[48.2,16.3],[48.3,16.4]]' }
|
||||
setup(makePlanner({ places: [...PLACES, track], editingReservation: HOTEL }))
|
||||
const hotelOptions = [...screen.getByLabelText('reservations.meta.pickHotel').querySelectorAll('option')].map(o => o.textContent)
|
||||
expect(hotelOptions).toContain('Hotel Sacher')
|
||||
expect(hotelOptions).not.toContain('Donauradweg')
|
||||
})
|
||||
|
||||
it('FE-MOB-RESSH-063: a stay already at a track keeps that track selected', () => {
|
||||
const track = { id: 103, name: 'Donauradweg', address: null, route_geometry: '[[48.2,16.3],[48.3,16.4]]' }
|
||||
setup(makePlanner({ places: [...PLACES, track], tripAccommodations: [{ ...ACCOMMODATIONS[0], place_id: 103 }], editingReservation: HOTEL }))
|
||||
expect(screen.getByLabelText('reservations.meta.pickHotel')).toHaveAttribute('data-value', '103')
|
||||
})
|
||||
|
||||
it('FE-MOB-RESSH-023: a hotel without an accommodation falls back to the booking location', () => {
|
||||
setup(makePlanner({ editingReservation: { ...HOTEL, accommodation_id: null } }))
|
||||
expect(screen.getByPlaceholderText('reservations.locationPlaceholder')).toHaveValue('fallback address')
|
||||
|
||||
@@ -77,8 +77,10 @@ A few details worth knowing:
|
||||
placed, and the booking simply rides along with it.
|
||||
- Moving the booking to a different check-in day moves its stop with it. Deleting the
|
||||
booking removes the stop it created, and leaves a stop you placed yourself standing.
|
||||
- Hotels are service stops, so **Show in Days too** under **Service stops** in the Road
|
||||
Trip settings also decides whether they appear in the places list.
|
||||
- Hotels count as service stops on the drive, so **Show in Days too** under **Service
|
||||
stops** in the Road Trip settings decides whether the hotel's stop shows in the day list.
|
||||
The hotel itself always stays in the places list, on the Days map and in the booking
|
||||
forms, whatever the switch says.
|
||||
|
||||
Nights booked before this existed are given their stop when the server upgrades, so trips
|
||||
you already have show their hotels on the drive without anyone re-saving anything. Trips
|
||||
|
||||
@@ -108,7 +108,7 @@ Driving settings includes **Show hazard areas**, off by default and shared by th
|
||||
- [Reservations-and-Bookings](Reservations-and-Bookings)
|
||||
- [Admin-Addons](Admin-Addons)
|
||||
|
||||
Driving settings also controls **Show in Days too** under **Service stops**. It is on by default and applies to every service stop in the trip, including existing stops. Turn it off to keep service stops exclusively in the Roadtrip view and omit them from the normal Days view and its route. Turning it on restores their visibility without creating duplicates. The setting is shared with fellow travellers and can also be changed through `update_roadtrip_settings` using `roadtrip_service_stops_in_days`.
|
||||
Driving settings also controls **Show in Days too** under **Service stops**. It is on by default and applies to every service stop in the trip, including existing stops. Turn it off to keep service stops exclusively in the Roadtrip view and omit them from the normal Days view and its route. Hotels, and any place a booking points at, stay in the places list and on the Days map either way. Turning it on restores their visibility without creating duplicates. The setting is shared with fellow travellers and can also be changed through `update_roadtrip_settings` using `roadtrip_service_stops_in_days`.
|
||||
|
||||
**Start and end each day at your stay**, under **Route line** in Driving settings, starts each Roadtrip day after a booked night at that hotel and ends each day before one there. It is off by default and shared by the trip. The hotel rows are drawn from the booking, not stored as stops, so the Days view stays as it is and switching it off gives the drive exactly as before. On the phone it is the one switch in **Driving figures**. See [Road-Trip](Road-Trip#starting-and-ending-the-day-at-the-stay).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user