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,72 @@
import type React from 'react';
import { type Dispatch, type SetStateAction, useState } from 'react';
import type { TripFile } from '../../types';
import { attachedBookingFiles } from './bookingFormModel';
export interface BookingFileAttachOptions {
/** The booking being edited; files picked before it is saved wait in `pendingFiles`. */
reservation: { id: number; title: string } | null | undefined;
/** The trip's files. */
files: TripFile[];
onFileUpload?: (fd: FormData) => Promise<unknown>;
setPendingFiles: Dispatch<SetStateAction<File[]>>;
/** The toast and translate the dialog already holds. */
toast: { success: (message: string) => void; error: (message: string) => void };
t: (key: string) => string;
}
/**
* The Files row of the desktop booking and transport dialogs: a file picked on a
* saved booking goes up at once, one picked on a new booking waits for the save,
* and the files linked or detached in the dialog show on the booking right away.
*/
export function useBookingFileAttach({
reservation,
files,
onFileUpload,
setPendingFiles,
toast,
t,
}: BookingFileAttachOptions) {
const [uploadingFile, setUploadingFile] = useState(false);
const [linkedFileIds, setLinkedFileIds] = useState<number[]>([]);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (reservation?.id) {
setUploadingFile(true);
try {
if (onFileUpload) {
const fd = new FormData();
fd.append('file', file);
fd.append('reservation_id', String(reservation.id));
fd.append('description', reservation.title);
await onFileUpload(fd);
toast.success(t('reservations.toast.fileUploaded'));
} else {
// A dialog without an upload reports a failed upload, as it always did.
toast.error(t('reservations.toast.uploadError'));
}
} catch {
toast.error(t('reservations.toast.uploadError'));
} finally {
setUploadingFile(false);
e.target.value = '';
}
} else {
setPendingFiles((prev) => [...prev, file]);
e.target.value = '';
}
};
return {
uploadingFile,
handleFileChange,
attachedFiles: attachedBookingFiles(files, reservation?.id, linkedFileIds),
removePending: (index: number) => setPendingFiles((prev) => prev.filter((_, j) => j !== index)),
linkFile: (fileId: number) => setLinkedFileIds((prev) => [...prev, fileId]),
detachFile: (fileId: number) => setLinkedFileIds((prev) => prev.filter((id) => id !== fileId)),
};
}