Files
TREK/client/src/components/Planner/useReservationForm.ts
T
Maurice b80fdf27f2 fix(bookings): show where an imported stay lands on the map before it is saved
An imported hotel was looked up once, by its name and its address together.
A booking site's romanized address (Agoda for a stay in Japan) often matches
nothing and sank the hotel's name with it, so the place was created without
coordinates and silently never showed on the map.

The review now finds the stay right away (name and address, then the name,
then the address) and says under the address whether it is on the map and
where, or that it is not yet, with a way to search again. Picking an address
suggestion sets the spot by hand. The save uses that spot, falls back the same
way when there is none, and a stay saved without a spot says so in a toast.
Desktop dialog and phone sheet share the lookup through useReservationForm.
2026-10-25 18:59:55 +01:00

81 lines
3.2 KiB
TypeScript

import { useMemo, useState } from 'react';
import type { Day, Place } from '../../types';
import { toggledTraveler } from './bookingFormModel';
import {
type CheckInEndField,
endTimeBeforeStartTime,
isEndBeforeStart,
type ReservationFields,
reservationSaveData,
type ReservationSaveOptions,
startParts,
tripDateBounds,
withHotelEnd,
withHotelPlace,
withHotelStart,
withLinkedPlace,
withLinkedStopDay,
withStartDate,
withStartTime,
} from './reservationFormModel';
import { useStayPin } from './useStayPin';
/**
* 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));
// Where an imported stay will land on the map, looked up while it is reviewed.
const stayPin = useStayPin();
// 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),
endTimeBeforeStartTime: endTimeBeforeStartTime(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)),
stayPin,
saveData: (opts: ReservationSaveOptions) => reservationSaveData(form, { ...opts, venuePin: stayPin.match }),
};
}