fix: refine itinerary lodging placement and stay metadata

Align lodging cards with itinerary flow by rendering checkout stays before the timeline and check-in stays after it, while collapsing duplicate no-location stays. Tighten compact card metadata into a concise IN/OUT panel so stay details read cleanly without visual noise.
This commit is contained in:
2026-03-07 20:18:26 +00:00
parent 68a6aea023
commit 288f81f631
5 changed files with 208 additions and 72 deletions

View File

@@ -98,8 +98,8 @@ Voyage aims to be simple, beautiful, and open to everyone — inheriting Adventu
- Upload trails and activities to your locations to remember your experiences with detailed maps and stats. - Upload trails and activities to your locations to remember your experiences with detailed maps and stats.
- **Plan Your Next Trip** 📃: Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner. - **Plan Your Next Trip** 📃: Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner.
- Itineraries can be created for any number of days and can include multiple destinations. - Itineraries can be created for any number of days and can include multiple destinations.
- A timeline-style day view shows ordered stops with numbered markers and compact location cards (no image banners) for a dense overview. Lodging appears as boundary anchors — before the first stop and after the last stop of each day — giving a clear at-a-glance view of where you're staying. Lodging cards use the same compact style (no image header) as location cards within the itinerary. - A timeline-style day view shows ordered stops with numbered markers and compact location cards (no image banners) for a dense overview. Lodging placement follows directional rules: on check-in day lodging appears after the last stop, on check-out day it appears before the first stop, and on days with no locations a single lodging card is shown (or two cards when a checkout and checkin are different lodgings). Lodging cards use the same compact style (no image header) as location cards within the itinerary.
- Connector rows between consecutive locations display distance and travel time powered by [OSRM](https://project-osrm.org/) routing (walking if ≤ 20 min, driving otherwise), with automatic haversine fallback when OSRM is unavailable. Self-hosted OSRM instances are supported via the `OSRM_BASE_URL` environment variable. Transportation items appear as separate compact connector rows showing mode, duration, and distance. Boundary transitions between lodging and the first/last stop of the day are also shown as connector rows. - Connector rows between consecutive locations display distance and travel time powered by [OSRM](https://project-osrm.org/) routing (walking if ≤ 20 min, driving otherwise), with automatic haversine fallback when OSRM is unavailable. Self-hosted OSRM instances are supported via the `OSRM_BASE_URL` environment variable. Transportation items appear as separate compact connector rows showing mode, duration, and distance. Boundary transitions between lodging and adjacent stops are also shown as connector rows.
- Each day has a single `+ Add` control to insert new places, and day-level quick actions include Auto-fill and an Optimize placeholder. Lodging added from within a day is automatically scheduled to that day. - Each day has a single `+ Add` control to insert new places, and day-level quick actions include Auto-fill and an Optimize placeholder. Lodging added from within a day is automatically scheduled to that day.
- Itineraries include many planning features like flight information, notes, checklists, and links to external resources. - Itineraries include many planning features like flight information, notes, checklists, and links to external resources.
- Itineraries can be shared with friends and family for collaborative planning. - Itineraries can be shared with friends and family for collaborative planning.

View File

@@ -14,8 +14,8 @@ Voyage is a full-fledged travel companion. With Voyage, you can log your adventu
- Upload trails and activities to your locations to remember your experiences with detailed maps and stats. - Upload trails and activities to your locations to remember your experiences with detailed maps and stats.
- **Plan Your Next Trip** 📃: Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner. - **Plan Your Next Trip** 📃: Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner.
- Itineraries can be created for any number of days and can include multiple destinations. - Itineraries can be created for any number of days and can include multiple destinations.
- A timeline-style day view shows ordered stops with numbered markers and compact location cards (no image banners) for a dense overview. Lodging appears as boundary anchors — before the first stop and after the last stop of each day — giving a clear at-a-glance view of where you're staying. Lodging cards use the same compact style (no image header) as location cards within the itinerary. - A timeline-style day view shows ordered stops with numbered markers and compact location cards (no image banners) for a dense overview. Lodging placement follows directional rules: on check-in day lodging appears after the last stop, on check-out day it appears before the first stop, and on days with no locations a single lodging card is shown (or two cards when a checkout and checkin are different lodgings). Lodging cards use the same compact style (no image header) as location cards within the itinerary.
- Connector rows between consecutive locations display distance and travel time powered by [OSRM](https://project-osrm.org/) routing (walking if ≤ 20 min, driving otherwise), with automatic haversine fallback when OSRM is unavailable. Self-hosted OSRM instances are supported via the `OSRM_BASE_URL` environment variable. Transportation items appear as separate compact connector rows showing mode, duration, and distance. Boundary transitions between lodging and the first/last stop of the day are also shown as connector rows. - Connector rows between consecutive locations display distance and travel time powered by [OSRM](https://project-osrm.org/) routing (walking if ≤ 20 min, driving otherwise), with automatic haversine fallback when OSRM is unavailable. Self-hosted OSRM instances are supported via the `OSRM_BASE_URL` environment variable. Transportation items appear as separate compact connector rows showing mode, duration, and distance. Boundary transitions between lodging and adjacent stops are also shown as connector rows.
- Each day has a single `+ Add` control to insert new places, and day-level quick actions include Auto-fill and an Optimize placeholder. Lodging added from within a day is automatically scheduled to that day. - Each day has a single `+ Add` control to insert new places, and day-level quick actions include Auto-fill and an Optimize placeholder. Lodging added from within a day is automatically scheduled to that day.
- Itineraries include many planning features like flight information, notes, checklists, and links to external resources. - Itineraries include many planning features like flight information, notes, checklists, and links to external resources.
- Itineraries can be shared with friends and family for collaborative planning. - Itineraries can be shared with friends and family for collaborative planning.

View File

@@ -22,9 +22,9 @@ The term "Location" is now used instead of "Adventure" - the usage remains the s
#### Collections #### Collections
- **Collection**: a collection is a way to group locations together. Collections are flexible and can be used in many ways. When no start or end date is added to a collection, it acts like a folder to group locations together. When a start and end date is added to a collection, it acts like a trip to group locations together that were visited during that time period. With start and end dates, the collection is transformed into a full itinerary with a timeline-style day view — each day displays numbered stops as compact cards (without image banners), connector rows between consecutive locations showing distance and travel time via OSRM routing (walking if ≤ 20 min, driving otherwise) with automatic haversine fallback when OSRM is unavailable, and a single `+ Add` control for inserting new places. Lodging appears as boundary anchors at the start and end of each day (before the first stop and after the last stop), with connector rows linking them to adjacent locations. Day-level quick actions include Auto-fill (populates an empty itinerary from dated records) and an Optimize placeholder for future route optimization. The itinerary also includes a map showing the route taken between locations. Your most recently updated collections also appear on the dashboard. For example, you could have a collection for a trip to Europe with dates so you can plan where you want to visit, a collection of local hiking trails, or a collection for a list of restaurants you want to try. - **Collection**: a collection is a way to group locations together. Collections are flexible and can be used in many ways. When no start or end date is added to a collection, it acts like a folder to group locations together. When a start and end date is added to a collection, it acts like a trip to group locations together that were visited during that time period. With start and end dates, the collection is transformed into a full itinerary with a timeline-style day view — each day displays numbered stops as compact cards (without image banners), connector rows between consecutive locations showing distance and travel time via OSRM routing (walking if ≤ 20 min, driving otherwise) with automatic haversine fallback when OSRM is unavailable, and a single `+ Add` control for inserting new places. Lodging placement follows directional rules: on check-in day it appears after the last stop, on check-out day it appears before the first stop, and on days with no locations a single lodging card is shown (or two cards when a checkout and checkin are different lodgings). Connector rows link lodging to adjacent locations. Day-level quick actions include Auto-fill (populates an empty itinerary from dated records) and an Optimize placeholder for future route optimization. The itinerary also includes a map showing the route taken between locations. Your most recently updated collections also appear on the dashboard. For example, you could have a collection for a trip to Europe with dates so you can plan where you want to visit, a collection of local hiking trails, or a collection for a list of restaurants you want to try.
- **Transportation**: a transportation is a collection exclusive feature that allows you to add transportation information to your trip. In the itinerary timeline view, transportation items appear as compact connector rows between stops — showing the travel mode, duration, and distance. This can be used to show the route taken between locations and the mode of transportation used. It can also be used to track flight information, such as flight number and departure time. - **Transportation**: a transportation is a collection exclusive feature that allows you to add transportation information to your trip. In the itinerary timeline view, transportation items appear as compact connector rows between stops — showing the travel mode, duration, and distance. This can be used to show the route taken between locations and the mode of transportation used. It can also be used to track flight information, such as flight number and departure time.
- **Lodging**: a lodging is a collection exclusive feature that allows you to add lodging information to your trip. This can be used to plan where you will stay during your trip and add notes about the lodging location. It can also be used to track reservation information, such as reservation number and check-in time. In the itinerary timeline view, lodging is displayed as compact cards (without image headers) positioned as boundary anchors — before the first stop and after the last stop of each day. Lodging added from within a specific day is automatically scheduled to that day. Connector rows show boundary transitions between lodging and adjacent locations. - **Lodging**: a lodging is a collection exclusive feature that allows you to add lodging information to your trip. This can be used to plan where you will stay during your trip and add notes about the lodging location. It can also be used to track reservation information, such as reservation number and check-in time. In the itinerary timeline view, lodging is displayed as compact cards (without image headers) using directional placement: on check-in day the lodging card appears after the last stop, on check-out day it appears before the first stop, and on days with no locations a single card is shown unless the checkout and checkin are different lodgings (in which case both appear). Lodging added from within a specific day is automatically scheduled to that day. Connector rows show boundary transitions between lodging and adjacent locations.
- **Note**: a note is a collection exclusive feature that allows you to add notes to your trip. This can be used to add additional information about your trip, such as a summary of the trip or a list of things to do. Notes can be assigned to a specific day of the trip to help organize the information. - **Note**: a note is a collection exclusive feature that allows you to add notes to your trip. This can be used to add additional information about your trip, such as a summary of the trip or a list of things to do. Notes can be assigned to a specific day of the trip to help organize the information.
- **Checklist**: a checklist is a collection exclusive feature that allows you to add a checklist to your trip. This can be used to create a list of things to do during your trip or for planning purposes like packing lists. Checklists can be assigned to a specific day of the trip to help organize the information. - **Checklist**: a checklist is a collection exclusive feature that allows you to add a checklist to your trip. This can be used to create a list of things to do during your trip or for planning purposes like packing lists. Checklists can be assigned to a specific day of the trip to help organize the information.

View File

@@ -69,6 +69,14 @@
return stars; return stars;
} }
export let lodging: Lodging;
export let user: User | null = null;
export let collection: Collection | null = null;
export let readOnly: boolean = false;
export let itineraryItem: CollectionItineraryItem | null = null;
export let showImage: boolean = true;
export let compact: boolean = false;
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC'; const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC';
const getTimezoneLabel = (zone?: string | null) => zone ?? localTimeZone; const getTimezoneLabel = (zone?: string | null) => zone ?? localTimeZone;
const getTimezoneTip = (zone?: string | null) => { const getTimezoneTip = (zone?: string | null) => {
@@ -85,9 +93,27 @@
}; };
const hasTimePortion = (date: string | null) => !!date && !isAllDay(date); const hasTimePortion = (date: string | null) => !!date && !isAllDay(date);
const isTimedStay = (date: string | null) => hasTimePortion(date); const isTimedStay = (date: string | null) => hasTimePortion(date);
const formatStayDateTime = (date: string | null) => {
if (!date) return null;
return isAllDay(date) ? formatAllDayDate(date) : formatDateInTimezone(date, lodging.timezone);
};
$: lodgingPriceLabel = formatMoney( $: lodgingPriceLabel = formatMoney(
toMoneyValue(lodging.price, lodging.price_currency, DEFAULT_CURRENCY) toMoneyValue(lodging.price, lodging.price_currency, DEFAULT_CURRENCY)
); );
$: compactStayMeta = [
lodging.check_in
? {
label: 'IN',
value: formatStayDateTime(lodging.check_in)
}
: null,
lodging.check_out
? {
label: 'OUT',
value: formatStayDateTime(lodging.check_out)
}
: null
].filter((entry): entry is { label: string; value: string | null } => Boolean(entry));
let showMoreDetails = false; let showMoreDetails = false;
$: hasExpandableDetails = Boolean( $: hasExpandableDetails = Boolean(
@@ -95,14 +121,6 @@
); );
$: if (!hasExpandableDetails) showMoreDetails = false; $: if (!hasExpandableDetails) showMoreDetails = false;
export let lodging: Lodging;
export let user: User | null = null;
export let collection: Collection | null = null;
export let readOnly: boolean = false;
export let itineraryItem: CollectionItineraryItem | null = null;
export let showImage: boolean = true;
export let compact: boolean = false;
let isWarningModalOpen: boolean = false; let isWarningModalOpen: boolean = false;
function editTransportation() { function editTransportation() {
@@ -328,6 +346,39 @@
</div> </div>
</div> </div>
{#if compact}
<div class="flex items-start justify-between gap-2.5">
<div class="min-w-0 flex-1">
{#if lodging.location}
<div class="flex items-center gap-1.5 text-xs text-base-content/70 min-w-0">
<MapMarker class="w-4 h-4 text-primary flex-shrink-0" />
<span class="truncate">{lodging.location}</span>
</div>
{/if}
</div>
{#if compactStayMeta.length > 0}
<div
class="shrink-0 min-w-[8rem] rounded-md border border-base-200/70 bg-base-200/55 px-2 py-1.5"
>
<div class="space-y-1">
{#each compactStayMeta as stayMeta}
<div class="grid grid-cols-[2.25rem_minmax(0,1fr)] items-baseline gap-1 leading-tight">
<div class="text-[9px] font-medium uppercase tracking-[0.14em] text-base-content/50">
{stayMeta.label}
</div>
<div
class="overflow-hidden text-ellipsis whitespace-nowrap text-right text-[11px] font-medium font-mono tabular-nums text-base-content/80"
>
{stayMeta.value}
</div>
</div>
{/each}
</div>
</div>
{/if}
</div>
{:else}
<!-- Location --> <!-- Location -->
{#if lodging.location} {#if lodging.location}
<div class="flex items-center gap-2 text-sm text-base-content/70 min-w-0"> <div class="flex items-center gap-2 text-sm text-base-content/70 min-w-0">
@@ -335,9 +386,10 @@
<span class="truncate">{lodging.location}</span> <span class="truncate">{lodging.location}</span>
</div> </div>
{/if} {/if}
{/if}
<!-- Check-in & Check-out Section --> <!-- Check-in & Check-out Section -->
{#if lodging.check_in || lodging.check_out} {#if !compact && (lodging.check_in || lodging.check_out)}
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
{#if lodging.check_in && lodging.check_out} {#if lodging.check_in && lodging.check_out}
<!-- Both dates present --> <!-- Both dates present -->

View File

@@ -55,7 +55,8 @@
date: string; date: string;
displayDate: string; displayDate: string;
items: ResolvedItineraryItem[]; items: ResolvedItineraryItem[];
boundaryLodgingItem: ResolvedItineraryItem | null; // Displayed as day start + end anchors preTimelineLodging: ResolvedItineraryItem | null; // Checkout-side lodging shown before timeline
postTimelineLodging: ResolvedItineraryItem | null; // Checkin-side lodging shown after timeline
overnightLodging: Lodging[]; // Lodging where guest is staying overnight (not check-in day) overnightLodging: Lodging[]; // Lodging where guest is staying overnight (not check-in day)
globalDatedItems: ResolvedItineraryItem[]; // Trip-wide items that still carry a date globalDatedItems: ResolvedItineraryItem[]; // Trip-wide items that still carry a date
dayMetadata: CollectionItineraryDay | null; // Day name and description dayMetadata: CollectionItineraryDay | null; // Day name and description
@@ -562,46 +563,115 @@
return null; return null;
} }
function getBoundaryLodgingItem(items: ResolvedItineraryItem[]): ResolvedItineraryItem | null { function getResolvedScheduledLodgingItem(
for (const item of items) { collection: Collection,
if (item?.[SHADOW_ITEM_MARKER_PROPERTY_NAME]) continue; lodging: Lodging
if ((item.item?.type || '') === 'lodging') return item; ): ResolvedItineraryItem | null {
const sourceItineraryItem = collection.itinerary?.find((item) => {
const objectType = item.item?.type || '';
return objectType === 'lodging' && item.object_id === lodging.id;
});
if (!sourceItineraryItem) return null;
return {
...sourceItineraryItem,
resolvedObject: lodging
};
} }
return null; function getDirectionalBoundaryLodging(
collection: Collection,
dateISO: string
): {
preTimelineLodging: ResolvedItineraryItem | null;
postTimelineLodging: ResolvedItineraryItem | null;
} {
const targetDate = DateTime.fromISO(dateISO).startOf('day');
let preTimelineLodging: ResolvedItineraryItem | null = null;
let postTimelineLodging: ResolvedItineraryItem | null = null;
for (const lodging of collection.lodging || []) {
if (!lodging.check_in || !lodging.check_out) continue;
const checkIn = DateTime.fromISO(lodging.check_in.split('T')[0]).startOf('day');
const checkOut = DateTime.fromISO(lodging.check_out.split('T')[0]).startOf('day');
const isPreTimelineContext = targetDate > checkIn && targetDate <= checkOut;
const isPostTimelineContext = targetDate >= checkIn && targetDate < checkOut;
if (!isPreTimelineContext && !isPostTimelineContext) continue;
const resolvedLodgingItem = getResolvedScheduledLodgingItem(collection, lodging);
if (!resolvedLodgingItem) continue;
if (!preTimelineLodging && isPreTimelineContext) {
preTimelineLodging = resolvedLodgingItem;
}
if (!postTimelineLodging && isPostTimelineContext) {
postTimelineLodging = resolvedLodgingItem;
}
if (preTimelineLodging && postTimelineLodging) break;
}
return { preTimelineLodging, postTimelineLodging };
} }
function getDayTimelineItems(day: DayGroup): ResolvedItineraryItem[] { function getDayTimelineItems(day: DayGroup): ResolvedItineraryItem[] {
if (!day.boundaryLodgingItem) return day.items; const boundaryIds = new Set(
return day.items.filter((item) => item.id !== day.boundaryLodgingItem?.id); [day.preTimelineLodging?.id, day.postTimelineLodging?.id].filter(
(id): id is string => !!id
)
);
if (boundaryIds.size === 0) return day.items;
return day.items.filter((item) => !boundaryIds.has(item.id));
} }
function shouldShowOvernightSummary(day: DayGroup): boolean { function shouldShowOvernightSummary(day: DayGroup): boolean {
return day.overnightLodging.length > 0 && !day.boundaryLodgingItem?.resolvedObject; return (
day.overnightLodging.length > 0 &&
!day.preTimelineLodging?.resolvedObject &&
!day.postTimelineLodging?.resolvedObject
);
} }
function reinsertBoundaryLodgingItem( function reinsertBoundaryLodgingItems(
day: DayGroup, day: DayGroup,
timelineItems: ResolvedItineraryItem[] timelineItems: ResolvedItineraryItem[]
): ResolvedItineraryItem[] { ): ResolvedItineraryItem[] {
if (!day.boundaryLodgingItem) return timelineItems; const boundaryCandidates = [day.preTimelineLodging, day.postTimelineLodging].filter(
(item, index, list): item is ResolvedItineraryItem =>
!!item && list.findIndex((candidate) => candidate?.id === item.id) === index
);
const boundaryItem = day.boundaryLodgingItem; if (boundaryCandidates.length === 0) return timelineItems;
if (timelineItems.some((item) => item.id === boundaryItem.id)) return timelineItems;
let restoredItems = [...timelineItems];
for (const boundaryItem of boundaryCandidates) {
const existedOnThisDay = day.items.some((item) => item.id === boundaryItem.id);
if (!existedOnThisDay) continue;
if (restoredItems.some((item) => item.id === boundaryItem.id)) continue;
const previousBoundaryIndex = day.items.findIndex((item) => item.id === boundaryItem.id); const previousBoundaryIndex = day.items.findIndex((item) => item.id === boundaryItem.id);
const insertIndex = const insertIndex =
previousBoundaryIndex >= 0 previousBoundaryIndex >= 0
? Math.min(previousBoundaryIndex, timelineItems.length) ? Math.min(previousBoundaryIndex, restoredItems.length)
: Math.min(0, timelineItems.length); : restoredItems.length;
return [ restoredItems = [
...timelineItems.slice(0, insertIndex), ...restoredItems.slice(0, insertIndex),
boundaryItem, boundaryItem,
...timelineItems.slice(insertIndex) ...restoredItems.slice(insertIndex)
]; ];
} }
return restoredItems;
}
function getLocationConnectorKey( function getLocationConnectorKey(
currentItem: ResolvedItineraryItem, currentItem: ResolvedItineraryItem,
nextItem: ResolvedItineraryItem | null nextItem: ResolvedItineraryItem | null
@@ -667,8 +737,8 @@
const firstLocationItem = getFirstLocationItem(dayGroup.items); const firstLocationItem = getFirstLocationItem(dayGroup.items);
const lastLocationItem = getLastLocationItem(dayGroup.items); const lastLocationItem = getLastLocationItem(dayGroup.items);
if (dayGroup.boundaryLodgingItem && firstLocationItem) { if (dayGroup.preTimelineLodging && firstLocationItem) {
pushPair(getConnectorPair(dayGroup.boundaryLodgingItem, firstLocationItem)); pushPair(getConnectorPair(dayGroup.preTimelineLodging, firstLocationItem));
} }
for (let index = 0; index < dayTimelineItems.length - 1; index += 1) { for (let index = 0; index < dayTimelineItems.length - 1; index += 1) {
@@ -680,8 +750,8 @@
pushPair(getConnectorPair(currentItem, nextLocationItem)); pushPair(getConnectorPair(currentItem, nextLocationItem));
} }
if (dayGroup.boundaryLodgingItem && lastLocationItem) { if (dayGroup.postTimelineLodging && lastLocationItem) {
pushPair(getConnectorPair(lastLocationItem, dayGroup.boundaryLodgingItem)); pushPair(getConnectorPair(lastLocationItem, dayGroup.postTimelineLodging));
} }
} }
@@ -1289,7 +1359,9 @@
}); });
// Sort items within each date group by order // Sort items within each date group by order
grouped.forEach((items) => items.sort((a, b) => a.order - b.order)); grouped.forEach((items) => {
items.sort((a, b) => a.order - b.order);
});
return grouped; return grouped;
} }
@@ -1362,8 +1434,11 @@
for (let dt = start; dt <= end; dt = dt.plus({ days: 1 })) { for (let dt = start; dt <= end; dt = dt.plus({ days: 1 })) {
const iso = dt.toISODate(); const iso = dt.toISODate();
const items = (grouped.get(iso) || []).sort((a, b) => a.order - b.order); const items = (grouped.get(iso) || []).sort((a, b) => a.order - b.order);
const boundaryLodgingItem = getBoundaryLodgingItem(items);
const overnightLodging = getOvernightLodgingForDate(collection, iso); const overnightLodging = getOvernightLodgingForDate(collection, iso);
const { preTimelineLodging, postTimelineLodging } = getDirectionalBoundaryLodging(
collection,
iso
);
const globalDatedItems = globalByDate.get(iso) || []; const globalDatedItems = globalByDate.get(iso) || [];
// Find day metadata for this date // Find day metadata for this date
@@ -1373,7 +1448,8 @@
date: iso, date: iso,
displayDate: dt.toFormat('cccc, LLLL d, yyyy'), displayDate: dt.toFormat('cccc, LLLL d, yyyy'),
items, items,
boundaryLodgingItem, preTimelineLodging,
postTimelineLodging,
overnightLodging, overnightLodging,
globalDatedItems, globalDatedItems,
dayMetadata dayMetadata
@@ -1441,7 +1517,7 @@
const day = days[dayIndex]; const day = days[dayIndex];
if (!day) return; if (!day) return;
// Update the local state immediately for smooth drag feedback // Update the local state immediately for smooth drag feedback
days[dayIndex].items = reinsertBoundaryLodgingItem(day, newItems); days[dayIndex].items = reinsertBoundaryLodgingItems(day, newItems);
days = [...days]; days = [...days];
} }
@@ -1474,7 +1550,7 @@
if (!day) return; if (!day) return;
// Update local state // Update local state
days[dayIndex].items = reinsertBoundaryLodgingItem(day, newItems); days[dayIndex].items = reinsertBoundaryLodgingItems(day, newItems);
days = [...days]; days = [...days];
// Save to backend if item was actually moved (not just considered) // Save to backend if item was actually moved (not just considered)
@@ -2384,30 +2460,38 @@
{@const weekday = DateTime.fromISO(day.date).toFormat('ccc')} {@const weekday = DateTime.fromISO(day.date).toFormat('ccc')}
{@const dayOfMonth = DateTime.fromISO(day.date).toFormat('d')} {@const dayOfMonth = DateTime.fromISO(day.date).toFormat('d')}
{@const monthAbbrev = DateTime.fromISO(day.date).toFormat('LLL')} {@const monthAbbrev = DateTime.fromISO(day.date).toFormat('LLL')}
{@const boundaryLodgingItem = day.boundaryLodgingItem} {@const preTimelineLodging = day.preTimelineLodging}
{@const postTimelineLodging = day.postTimelineLodging}
{@const dayTimelineItems = getDayTimelineItems(day)}
{@const firstLocationItem = getFirstLocationItem(day.items)} {@const firstLocationItem = getFirstLocationItem(day.items)}
{@const lastLocationItem = getLastLocationItem(day.items)} {@const lastLocationItem = getLastLocationItem(day.items)}
{@const noLocationsInDay = !firstLocationItem && !lastLocationItem}
{@const shouldCollapseBoundaryLodging =
noLocationsInDay &&
preTimelineLodging?.id &&
postTimelineLodging?.id &&
preTimelineLodging.id === postTimelineLodging.id}
{@const startBoundaryConnector = {@const startBoundaryConnector =
boundaryLodgingItem && firstLocationItem preTimelineLodging && firstLocationItem
? getLocationConnector(boundaryLodgingItem, firstLocationItem) ? getLocationConnector(preTimelineLodging, firstLocationItem)
: null} : null}
{@const startBoundaryDirectionsUrl = {@const startBoundaryDirectionsUrl =
boundaryLodgingItem && firstLocationItem preTimelineLodging && firstLocationItem
? buildDirectionsUrl( ? buildDirectionsUrl(
boundaryLodgingItem, preTimelineLodging,
firstLocationItem, firstLocationItem,
startBoundaryConnector?.mode || 'walking' startBoundaryConnector?.mode || 'walking'
) )
: null} : null}
{@const endBoundaryConnector = {@const endBoundaryConnector =
boundaryLodgingItem && lastLocationItem postTimelineLodging && lastLocationItem
? getLocationConnector(lastLocationItem, boundaryLodgingItem) ? getLocationConnector(lastLocationItem, postTimelineLodging)
: null} : null}
{@const endBoundaryDirectionsUrl = {@const endBoundaryDirectionsUrl =
boundaryLodgingItem && lastLocationItem postTimelineLodging && lastLocationItem
? buildDirectionsUrl( ? buildDirectionsUrl(
lastLocationItem, lastLocationItem,
boundaryLodgingItem, postTimelineLodging,
endBoundaryConnector?.mode || 'walking' endBoundaryConnector?.mode || 'walking'
) )
: null} : null}
@@ -2552,13 +2636,13 @@
<!-- Day Items (vertical timeline with ordered stops) --> <!-- Day Items (vertical timeline with ordered stops) -->
<div> <div>
{#if boundaryLodgingItem?.resolvedObject} {#if preTimelineLodging?.resolvedObject}
<div class="mb-3"> <div class="mb-3">
<LodgingCard <LodgingCard
lodging={boundaryLodgingItem.resolvedObject} lodging={preTimelineLodging.resolvedObject}
{user} {user}
{collection} {collection}
itineraryItem={boundaryLodgingItem} itineraryItem={preTimelineLodging}
showImage={false} showImage={false}
compact={true} compact={true}
on:delete={handleItemDelete} on:delete={handleItemDelete}
@@ -2627,7 +2711,7 @@
</div> </div>
{/if} {/if}
{#if getDayTimelineItems(day).length === 0 && !boundaryLodgingItem?.resolvedObject} {#if dayTimelineItems.length === 0 && !preTimelineLodging?.resolvedObject && !postTimelineLodging?.resolvedObject}
<div <div
class="card bg-base-100 shadow-sm border border-dashed border-base-300 p-4 text-center" class="card bg-base-100 shadow-sm border border-dashed border-base-300 p-4 text-center"
> >
@@ -2639,7 +2723,7 @@
{:else} {:else}
<div <div
use:dndzone={{ use:dndzone={{
items: getDayTimelineItems(day), items: dayTimelineItems,
flipDurationMs, flipDurationMs,
dropTargetStyle: { outline: 'none', border: 'none' }, dropTargetStyle: { outline: 'none', border: 'none' },
dragDisabled: isSavingOrder || !canModify, dragDisabled: isSavingOrder || !canModify,
@@ -2649,11 +2733,11 @@
on:finalize={(e) => handleDndFinalize(dayIndex, e)} on:finalize={(e) => handleDndFinalize(dayIndex, e)}
class="space-y-3" class="space-y-3"
> >
{#each getDayTimelineItems(day) as item, index (item.id)} {#each dayTimelineItems as item, index (item.id)}
{@const objectType = item.item?.type || ''} {@const objectType = item.item?.type || ''}
{@const resolvedObj = item.resolvedObject} {@const resolvedObj = item.resolvedObject}
{@const multiDay = isMultiDay(item)} {@const multiDay = isMultiDay(item)}
{@const nextLocationItem = findNextLocationItem(getDayTimelineItems(day), index)} {@const nextLocationItem = findNextLocationItem(dayTimelineItems, index)}
{@const locationConnector = getLocationConnector(item, nextLocationItem)} {@const locationConnector = getLocationConnector(item, nextLocationItem)}
{@const directionsUrl = buildDirectionsUrl( {@const directionsUrl = buildDirectionsUrl(
item, item,
@@ -2677,7 +2761,7 @@
> >
{timelineNumber} {timelineNumber}
</div> </div>
{#if index < getDayTimelineItems(day).length - 1} {#if index < dayTimelineItems.length - 1}
<div class="w-px bg-base-300 flex-1 min-h-10 mt-1"></div> <div class="w-px bg-base-300 flex-1 min-h-10 mt-1"></div>
{/if} {/if}
</div> </div>
@@ -2962,7 +3046,7 @@
</div> </div>
{/if} {/if}
{#if boundaryLodgingItem?.resolvedObject} {#if postTimelineLodging?.resolvedObject && !shouldCollapseBoundaryLodging}
<div class="mt-3"> <div class="mt-3">
{#if endBoundaryConnector} {#if endBoundaryConnector}
<div <div
@@ -3017,10 +3101,10 @@
{/if} {/if}
<LodgingCard <LodgingCard
lodging={boundaryLodgingItem.resolvedObject} lodging={postTimelineLodging.resolvedObject}
{user} {user}
{collection} {collection}
itineraryItem={boundaryLodgingItem} itineraryItem={postTimelineLodging}
showImage={false} showImage={false}
compact={true} compact={true}
on:delete={handleItemDelete} on:delete={handleItemDelete}