Files
TREK/client/src/components/Planner/parsedItemToDraft.ts
T
Maurice 130272cfb0 fix(booking-import): stop writing an unreadable type down as a hotel (#2076)
When neither the keyword table nor the model produced a type, the router wrote
"hotel" and cast whatever string it got to FlatType without checking it. A hotel
is a booking type, so a ferry voucher imported from the Transport tab opened the
booking form, which offers six chips and not one transport among them, and
"other" was the only honest pick left. An unrecognised string got through the
cast too and then vanished silently in nuExtractToKiReservations.

The type is validated against FLAT_TYPES now. When it cannot be read the item is
still extracted with the lodging shape so it survives mapping, but it says so,
and the tie-breaker #2080 added finally has something to fire on: isUnplaceableItem
only ever matched a type outside both form's vocabularies, and the server can
only emit its own eight, so on a real document it never matched at all.

The tab travels with the job instead of sitting in component state. The parse
deliberately outlives navigation and reload and the review is triggered by the
global widget on any page, so by the time it ran the state had remounted back to
its default and the context was gone.
2026-08-28 14:25:08 +02:00

68 lines
3.2 KiB
TypeScript

import type { BookingImportPreviewItem, Reservation, ReservationEndpoint } from '@trek/shared'
/**
* A pre-fill draft for the reservation/transport edit modals built from a parsed
* booking-import item. Carries the normal reservation fields the modals read for
* their form, plus the import-only `_venue`/`_accommodation` the hotel path needs
* to suggest a place and a day range. It has no `id` — the modal stays in
* "create" mode and the user reviews/edits before it is ever persisted.
*/
export interface BookingReviewDraft extends Omit<Partial<Reservation>, 'metadata' | 'endpoints'> {
/** Type-specific extras (airline, flight_number, check_in_time, price, …) as an object. */
metadata?: Record<string, unknown> | null
endpoints?: ReservationEndpoint[]
/** Parsed venue (auto-created place candidate) — hotel/restaurant/event. */
_venue?: BookingImportPreviewItem['_venue']
/** Parsed check-in/out + confirmation — hotels only. */
_accommodation?: BookingImportPreviewItem['_accommodation']
/** The uploaded source file(s) the item was parsed from — attached to the booking on save. */
_sourceFiles?: File[]
}
/**
* Map a parsed booking item onto the shape the edit modals pre-fill from. Pure
* (no I/O). Transport items keep their geocoded endpoints; venue/accommodation
* ride along untouched so the hotel modal can match a place by name (or create
* one from the reviewed address on save).
*/
export function parsedItemToDraft(item: BookingImportPreviewItem): BookingReviewDraft {
return {
type: item.type,
title: item.title,
status: 'pending',
reservation_time: item.reservation_time ?? null,
reservation_end_time: item.reservation_end_time ?? null,
location: item.location ?? item._venue?.address ?? item._venue?.name ?? null,
confirmation_number: item.confirmation_number ?? null,
notes: null,
metadata: (item.metadata as Record<string, unknown> | undefined) ?? null,
endpoints: (item.endpoints ?? []) as ReservationEndpoint[],
_venue: item._venue,
_accommodation: item._accommodation,
}
}
/** Transport types route to the TransportModal; everything else to the ReservationModal. */
const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other'])
/** The types the booking form can express — its own chip list. */
const BOOKING_TYPES = new Set(['hotel', 'restaurant', 'event', 'tour', 'activity', 'parking', 'other'])
export function isTransportItem(item: BookingImportPreviewItem): boolean {
return TRANSPORT_TYPES.has(item.type)
}
/**
* Neither form is obviously right, so the tab the user started the import from
* breaks the tie (#2076).
*
* Two cases. A type neither form can express, and a type the extractor filled in
* rather than read. The second is the one that actually occurs: the server can
* only ever emit its eight known types, so the first branch never fires on a real
* document, and a ferry voucher the model could not classify arrived as a
* placeholder 'hotel' with no way to tell it from a real one.
*/
export function isUnplaceableItem(item: BookingImportPreviewItem): boolean {
if (item.type_guessed === true) return true
return !TRANSPORT_TYPES.has(item.type) && !BOOKING_TYPES.has(item.type)
}