refactor(planner): share the booking form and its hotel stay between desktop and phone

The booking dialog and the phone booking sheet keep their markup and run on useReservationForm: how a saved booking or an import fills the form, the end before start check, the start date and time, the place and hotel picks, the stay range and the request body with the stay a hotel booking creates come from reservationFormModel. The desktop's check-in window, its link to a stop inside the form and its imported price stay its own, behind options where the code is shared.
This commit is contained in:
Maurice
2026-10-09 10:43:50 +02:00
parent 203e425e7e
commit fc3cc1bbbe
14 changed files with 1360 additions and 571 deletions
@@ -0,0 +1,74 @@
import { useMemo, useState } from 'react';
import type { Day, Place } from '../../types';
import { toggledTraveler } from './bookingFormModel';
import {
type CheckInEndField,
isEndBeforeStart,
type ReservationFields,
reservationSaveData,
type ReservationSaveOptions,
startParts,
tripDateBounds,
withHotelEnd,
withHotelPlace,
withHotelStart,
withLinkedPlace,
withLinkedStopDay,
withStartDate,
withStartTime,
} from './reservationFormModel';
/**
* The booking form behind the desktop ReservationModal and the phone's booking
* sheet: the fields, the travellers and the files picked before saving, the date
* rules and the place and hotel picks. Each shell decides when to fill it and what
* to do around a save.
*/
export function useReservationForm<F extends ReservationFields & CheckInEndField>(
emptyForm: F,
days: Day[],
places: Pick<Place, 'id' | 'name' | 'address'>[]
) {
const [form, setForm] = useState<F>(emptyForm);
const [isSaving, setIsSaving] = useState(false);
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
// Travelers assigned to this booking (#1517), seeded on open and persisted after the save resolves.
const [travelerIds, setTravelerIds] = useState<Set<number>>(new Set());
const set = (field: keyof F, value: string | number) => setForm((prev) => ({ ...prev, [field]: value }));
const toggleTraveler = (id: number) => setTravelerIds((prev) => toggledTraveler(prev, id));
// Restrict non-hotel booking dates to the trip's span (#1662). Hotels already
// constrain to trip days via their day dropdowns.
const dateBounds = useMemo(() => tripDateBounds(days || []), [days]);
const start = startParts(form.reservation_time);
return {
form,
setForm,
set,
isSaving,
setIsSaving,
pendingFiles,
setPendingFiles,
travelerIds,
setTravelerIds,
toggleTraveler,
isEndBeforeStart: isEndBeforeStart(form),
dateBounds,
startDate: start.date,
startTime: start.time,
setStartDate: (date: string | null | undefined) =>
set('reservation_time', withStartDate(form.reservation_time, date)),
setStartTime: (time: string, fallbackDate: string) =>
set('reservation_time', withStartTime(form.reservation_time, time, fallbackDate)),
/** An undated booking takes the day of the stop it was just linked to. */
takeStopDay: (dayDate: string) => setForm((prev) => withLinkedStopDay(prev, dayDate)),
pickPlace: (value: string | number) => setForm((prev) => withLinkedPlace(prev, value, places)),
pickHotelPlace: (value: string | number) => setForm((prev) => withHotelPlace(prev, value, places)),
pickHotelStart: (value: string | number) => setForm((prev) => withHotelStart(prev, value, days)),
pickHotelEnd: (value: string | number) => setForm((prev) => withHotelEnd(prev, value, days)),
saveData: (opts: ReservationSaveOptions) => reservationSaveData(form, opts),
};
}