import { getMergedItems, getTransportForDay, hidesOnMiddleDay } from '../../utils/dayMerge' import { isDayInAccommodationRange } from '../../utils/dayOrder' import { SharedDayCard } from './SharedDayCard' import { SharedMap, type MapPlace } from './SharedMap' import { stopNumbers } from './sharedTripModel' interface Assignment { id: number order_index?: number | null accommodation_id?: number | null notes?: string | null place?: (MapPlace & { category_id?: number | null }) | null } interface SharedPlanViewProps { // The share payload is an open-ended snapshot, typed loosely at the hook. data: { days?: { id: number; day_number: number; title?: string | null; date?: string | null }[] assignments?: Record dayNotes?: Record places?: MapPlace[] reservations?: { id: number; type: string; title: string; day_id?: number | null; end_day_id?: number | null }[] accommodations?: { id: number; place_name?: string | null; start_day_id?: number | null; end_day_id?: number | null }[] categories?: { id: number; color?: string | null; icon?: string | null }[] cartoApiKey?: string | null } selectedDay: number | null onSelectDay: (id: number | null) => void onPickDayOnMap: (id: number | null) => void collapsedDays: ReadonlySet onToggleDay: (id: number) => void } /** * The Plan tab: the days as the planner's cards on the left, the map beside * them and staying in view while they scroll. On a phone the map comes first * and the days follow under it. */ export function SharedPlanView({ data, selectedDay, onSelectDay, onPickDayOnMap, collapsedDays, onToggleDay }: SharedPlanViewProps) { const days = [...(data.days || [])].sort((a, b) => a.day_number - b.day_number) const assignments = data.assignments || {} // The stop a booked night wrote onto its check-in day heads that day since the // reseat, and the planner's numbers leave it out (the day list does too). So the // numbers and the day line are counted over the traveller's own stops, or the // hotel wore badge 1, every real stop read one higher than in the app, and the // line set off from where the day ends. Its pin stays on the day, unnumbered, // the way the planner keeps the hotel on the map. const stopsOf = (dayId: number) => (assignments[String(dayId)] || []).filter(a => a.accommodation_id == null) const dayAssignments = selectedDay ? [...(assignments[String(selectedDay)] || [])].sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0)) : [] const selectedNumbers = stopNumbers(selectedDay ? stopsOf(selectedDay) : []) // The places these assignments sit on, in their order, each drawn once. const located = (list: Assignment[]) => { const seen = new Set() return list.map(a => a.place).filter((p): p is MapPlace => { if (!p?.lat || !p?.lng || seen.has(p.id)) return false seen.add(p.id) return true }) } const mapPlaces = selectedDay ? located(dayAssignments) : (data.places || []).filter(p => p?.lat && p?.lng) const dayLine = located(dayAssignments.filter(a => a.accommodation_id == null)) return (
{days.map((day, index) => { // Without the booked nights: the day already shows each of them as its // own pill, and the stop a booking writes for the drive would be that // same hotel a second time. There is no road trip view on a shared link, // so the stop has nothing else to do here. const stops = stopsOf(day.id) const transports = getTransportForDay({ reservations: data.reservations || [], dayId: day.id, dayAssignmentIds: stops.map(a => a.id), days, }) // The shared link has to say what the app says: a multi-day parking only // shows up on its drop-off and pickup day (#1937). const items = getMergedItems({ dayAssignments: stops, dayNotes: data.dayNotes?.[String(day.id)] || [], dayTransports: transports, dayId: day.id, }).filter(item => !(item.type === 'transport' && hidesOnMiddleDay(item.data, day.id))) return ( onSelectDay(selectedDay === day.id ? null : day.id)} onToggleCollapse={() => onToggleDay(day.id)} items={items} stays={(data.accommodations || []).filter(a => isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days))} // A share can still carry an assignment for a deleted place. The list // skips those rows, so the count must not count them either. placeCount={stops.filter(a => a.place).length} stopNumber={stopNumbers(stops).byAssignment} categories={data.categories || []} /> ) })}
) }