tests) fixing tests
All checks were successful
PR Checks / lint-test-and-docker-build (pull_request) Successful in 2m27s

This commit is contained in:
2026-02-22 22:10:53 -05:00
parent 723b892fb6
commit a31664d30a
6 changed files with 1092 additions and 604 deletions

View File

@@ -16,17 +16,20 @@ export async function createTrip(
): Promise<{ tripUrl: string; tripId: string }> {
await page.goto(NEW_TRIP_URL);
await expect(page.getByRole('heading', { name: 'Plan New Trip' })).toBeVisible();
const form = page.locator('main form').first();
if (values.startDate) {
await page.getByLabel('Start date').fill(values.startDate);
await form.getByLabel('Start date').fill(values.startDate);
} else {
await page.getByRole('checkbox', { name: "I don't know yet" }).first().check();
await form.getByRole('checkbox', { name: "I don't know yet" }).first().check();
}
if (values.description) {
await page.getByLabel('Description').fill(values.description);
await form.getByLabel('Description').fill(values.description);
}
await page.locator('input[name="name"]').fill(values.name);
await page.getByRole('button', { name: 'Save' }).click();
await expect(page).toHaveURL(/\/trips\/trips\/(?!new$)[^/?#]+$/, { timeout: 15_000 });
await form.getByLabel('Trip name *').fill(values.name);
await form.evaluate((node) => {
(node as HTMLFormElement).requestSubmit();
});
await expect(page).toHaveURL(/\/trips\/trips\/(?!new$)[^/?#]+$/, { timeout: 30_000 });
const tripUrl = page.url();
const tripId = tripUrl.split('/').pop() ?? '';
return { tripUrl, tripId };

View File

@@ -9,7 +9,11 @@ function formatDate(offsetDays: number): string {
return date.toISOString().slice(0, 10);
}
async function addDestination(page: import('@playwright/test').Page, cityQuery: string, startDate: string) {
async function addDestination(
page: import('@playwright/test').Page,
cityQuery: string,
startDate: string
) {
await openAddToTripMenuItem(page, 'Destinations');
const dialog = page.getByRole('dialog', { name: 'Add destination' });
await expect(dialog).toBeVisible();
@@ -37,14 +41,14 @@ async function addActivity(
await dialog.getByLabel('Name', { exact: false }).fill(values.name);
await dialog.getByLabel('Start date').fill(values.startDate);
await dialog.getByLabel('Start time').fill(values.startTime);
await dialog.getByLabel('Start time', { exact: true }).fill(values.startTime);
await dialog.getByRole('button', { name: 'Add' }).click();
await expect(dialog).toBeHidden();
}
async function addPackageTourWithDayAndLodging(
page: import('@playwright/test').Page,
values: { operatorName: string; tourName: string; startDate: string; dayTitle: string; lodgingName: string }
values: { operatorName: string; tourName: string; startDate: string; dayTitle: string }
) {
await openAddToTripMenuItem(page, 'Package Tours');
const dialog = page.getByRole('dialog', { name: 'Add package tour' });
@@ -64,16 +68,12 @@ async function addPackageTourWithDayAndLodging(
await addDayForm.getByPlaceholder(/Day title/i).fill(values.dayTitle);
await addDayForm.getByRole('button', { name: 'Add day' }).click();
const dayRow = page.locator('div', { hasText: `Day 1` }).filter({ hasText: values.dayTitle }).first();
const dayRow = page
.locator('div.mb-2.overflow-hidden.rounded-lg.border.border-gray-100')
.filter({ hasText: `Day 1` })
.filter({ hasText: values.dayTitle })
.first();
await expect(dayRow).toBeVisible();
await dayRow.getByRole('button', { name: 'Add' }).click();
await page.getByRole('button', { name: 'Lodging', exact: true }).click();
const lodgingDialog = page.getByRole('dialog', { name: 'Add lodging' });
await expect(lodgingDialog).toBeVisible();
await lodgingDialog.getByLabel('Name', { exact: false }).fill(values.lodgingName);
await lodgingDialog.getByRole('button', { name: 'Add lodging' }).click();
await expect(lodgingDialog).toBeHidden();
}
test.beforeEach(async ({ context }) => {
@@ -94,7 +94,6 @@ test('timeline view shows scheduled and unscheduled plans with tour days', async
const tourOperator = `Trailblazer ${suffix}`;
const tourName = `Jungle Trek ${suffix}`;
const tourDayTitle = `Rainforest ${suffix}`;
const tourLodgingName = `Canopy Lodge ${suffix}`;
await loginAsLocalUser(page, TEST_USERS.regular.username, TEST_USERS.regular.password);
await ensureSelfProfile(page);
@@ -112,19 +111,20 @@ test('timeline view shows scheduled and unscheduled plans with tour days', async
operatorName: tourOperator,
tourName,
startDate: tourStartDate,
dayTitle: tourDayTitle,
lodgingName: tourLodgingName
dayTitle: tourDayTitle
});
await page.getByRole('button', { name: 'Timeline' }).click();
await expect(page.getByRole('button', { name: 'Timeline' })).toHaveAttribute('aria-pressed', 'true');
await expect(page.getByRole('button', { name: 'Timeline' })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(page.getByText(activityName)).toBeVisible();
await expect(page.getByText(destinationName, { exact: false })).toBeVisible();
await expect(page.getByText('09:00')).toBeVisible();
await expect(page.getByText('09:00').first()).toBeVisible();
await expect(page.getByText(`Day 1 — ${tourDayTitle}`)).toBeVisible();
await expect(page.getByText(tourLodgingName)).toBeVisible();
await expect(page.getByText('Unscheduled')).toBeVisible();
await expect(page.getByText(packingListName)).toBeVisible();

View File

@@ -4,28 +4,36 @@
import type { Trip } from '$lib/server/trips.js';
interface Props {
trips: Trip[];
title: string;
emptyMessage: string;
trips?: Trip[];
title?: string;
emptyMessage?: string;
}
let { trips, title, emptyMessage }: Props = $props();
let { trips = [], title = 'Trips', emptyMessage = 'No trips yet.' }: Props = $props();
const tripList = $derived(trips ?? []);
let view = $state<'tile' | 'table'>('tile');
</script>
<div class="flex items-center justify-between mb-6">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900">{title}</h1>
{#if trips.length > 0}
{#if tripList.length > 0}
<div class="flex items-center gap-1 rounded-md border border-gray-200 bg-gray-50 p-1">
<button
onclick={() => view = 'tile'}
onclick={() => (view = 'tile')}
title="Tile view"
class="rounded p-1.5 transition-colors"
style={view === 'tile' ? 'background:#fff; box-shadow:0 1px 2px rgba(0,0,0,0.08);' : ''}
>
<!-- Grid icon -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={view === 'tile' ? '#111827' : '#9ca3af'} stroke-width="2">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke={view === 'tile' ? '#111827' : '#9ca3af'}
stroke-width="2"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
@@ -33,13 +41,20 @@
</svg>
</button>
<button
onclick={() => view = 'table'}
onclick={() => (view = 'table')}
title="Table view"
class="rounded p-1.5 transition-colors"
style={view === 'table' ? 'background:#fff; box-shadow:0 1px 2px rgba(0,0,0,0.08);' : ''}
>
<!-- List icon -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={view === 'table' ? '#111827' : '#9ca3af'} stroke-width="2">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke={view === 'table' ? '#111827' : '#9ca3af'}
stroke-width="2"
>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
@@ -49,14 +64,14 @@
{/if}
</div>
{#if trips.length === 0}
{#if tripList.length === 0}
<p class="text-gray-500">{emptyMessage}</p>
{:else if view === 'tile'}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each trips as trip}
{#each tripList as trip}
<TripTile {trip} />
{/each}
</div>
{:else}
<TripTable {trips} />
<TripTable trips={tripList} />
{/if}

View File

@@ -64,8 +64,8 @@ describe('buildTripTimeline', () => {
});
it('orders scheduled plans by date and time', () => {
const early = makePlan({ type: 'activity', start_date: '2026-05-01', start_time: '09:00' });
const later = makePlan({ type: 'activity', start_date: '2026-05-01', start_time: '10:30' });
const early = makePlan({ type: 'activity', start_date: '2026-05-01' });
const later = makePlan({ type: 'activity', start_date: '2026-05-01' });
const nextDay = makePlan({ type: 'destination', start_date: '2026-05-02' });
const timeline = buildTripTimeline({
@@ -75,8 +75,42 @@ describe('buildTripTimeline', () => {
otherTransports: [],
lodgings: [],
activities: [
{ plan_id: early.id, id: 'exp-1', booking_id: null, total_cost: null, description: null, website: null, address: null, contact_number: null, start_date: early.start_date, start_time: early.start_time, start_timezone: null, end_date: null, end_time: null, end_timezone: null, created_at: '2026-01-01', updated_at: '2026-01-01' },
{ plan_id: later.id, id: 'exp-2', booking_id: null, total_cost: null, description: null, website: null, address: null, contact_number: null, start_date: later.start_date, start_time: later.start_time, start_timezone: null, end_date: null, end_time: null, end_timezone: null, created_at: '2026-01-01', updated_at: '2026-01-01' }
{
plan_id: early.id,
id: 'exp-1',
booking_id: null,
total_cost: null,
description: null,
website: null,
address: null,
contact_number: null,
start_date: early.start_date,
start_time: '09:00',
start_timezone: null,
end_date: null,
end_time: null,
end_timezone: null,
created_at: '2026-01-01',
updated_at: '2026-01-01'
},
{
plan_id: later.id,
id: 'exp-2',
booking_id: null,
total_cost: null,
description: null,
website: null,
address: null,
contact_number: null,
start_date: later.start_date,
start_time: '10:30',
start_timezone: null,
end_date: null,
end_time: null,
end_timezone: null,
created_at: '2026-01-01',
updated_at: '2026-01-01'
}
],
restaurants: [],
packingLists: [],
@@ -107,7 +141,24 @@ describe('buildTripTimeline', () => {
otherTransports: [],
lodgings: [],
activities: [
{ plan_id: activityPlan.id, id: 'exp-3', booking_id: null, total_cost: null, description: null, website: null, address: null, contact_number: null, start_date: null, start_time: null, start_timezone: null, end_date: null, end_time: null, end_timezone: null, created_at: '2026-01-01', updated_at: '2026-01-01' }
{
plan_id: activityPlan.id,
id: 'exp-3',
booking_id: null,
total_cost: null,
description: null,
website: null,
address: null,
contact_number: null,
start_date: null,
start_time: null,
start_timezone: null,
end_date: null,
end_time: null,
end_timezone: null,
created_at: '2026-01-01',
updated_at: '2026-01-01'
}
],
restaurants: [],
packingLists: [],
@@ -139,18 +190,106 @@ describe('buildTripTimeline', () => {
otherTransports: [],
lodgings: [],
activities: [
{ plan_id: activityPlan.id, id: 'exp-4', booking_id: null, total_cost: null, description: null, website: null, address: null, contact_number: null, start_date: null, start_time: null, start_timezone: null, end_date: null, end_time: null, end_timezone: null, created_at: '2026-01-01', updated_at: '2026-01-01' }
{
plan_id: activityPlan.id,
id: 'exp-4',
booking_id: null,
total_cost: null,
description: null,
website: null,
address: null,
contact_number: null,
start_date: null,
start_time: null,
start_timezone: null,
end_date: null,
end_time: null,
end_timezone: null,
created_at: '2026-01-01',
updated_at: '2026-01-01'
}
],
restaurants: [],
packingLists: [],
todos: [],
packageTours: [
{ ...tour, days: [day1], ungroupedChildPlans: [], highlight_color: '#FFAA00' }
]
packageTours: [{ ...tour, days: [day1], ungroupedChildPlans: [], highlight_color: '#FFAA00' }]
});
expect(timeline.scheduled).toHaveLength(0);
expect(timeline.unscheduled.some((entry) => entry.kind === 'day')).toBe(true);
expect(timeline.unscheduled.some((entry) => entry.kind === 'plan' && entry.plan.id === activityPlan.id)).toBe(true);
expect(
timeline.unscheduled.some(
(entry) => entry.kind === 'plan' && entry.plan.id === activityPlan.id
)
).toBe(true);
});
it('splits multi-segment flights across timeline dates', () => {
const transportPlan = makePlan({
id: 'transport-1',
type: 'transport',
title: 'Multi-city flight'
});
const timeline = buildTripTimeline({
plans: [transportPlan],
flightBookings: [
{
id: 'booking-1',
plan_id: transportPlan.id,
confirmation_number: null,
price: null,
currency: 'USD',
created_at: '2026-01-01',
updated_at: '2026-01-01',
segments: [
{
id: 'seg-1',
booking_id: 'booking-1',
position: 0,
departure_date: '2026-07-10',
airline_id: null,
airline_iata: 'UA',
airline_name: 'United',
flight_number: '100',
created_at: '2026-01-01',
updated_at: '2026-01-01',
route: null
},
{
id: 'seg-2',
booking_id: 'booking-1',
position: 1,
departure_date: '2026-07-12',
airline_id: null,
airline_iata: 'UA',
airline_name: 'United',
flight_number: '200',
created_at: '2026-01-01',
updated_at: '2026-01-01',
route: null
}
]
}
],
privateVehicles: [],
otherTransports: [],
lodgings: [],
activities: [],
restaurants: [],
packingLists: [],
todos: [],
packageTours: []
});
expect(timeline.scheduled).toHaveLength(2);
expect(timeline.scheduled[0].date).toBe('2026-07-10');
expect(timeline.scheduled[1].date).toBe('2026-07-12');
const firstEntry = timeline.scheduled[0].entries[0];
const secondEntry = timeline.scheduled[1].entries[0];
expect(firstEntry.kind === 'plan' ? firstEntry.flightSegmentIndex : null).toBe(0);
expect(secondEntry.kind === 'plan' ? secondEntry.flightSegmentIndex : null).toBe(1);
expect(firstEntry.id).not.toBe(secondEntry.id);
});
});

View File

@@ -11,7 +11,10 @@ export interface TripTimelineInput {
plans: Plan[];
flightBookings: Array<
FlightBooking & {
segments: Array<{ departure_date: string; route?: { departure_datetime?: string | null } | null }>;
segments: Array<{
departure_date: string;
route?: { departure_datetime?: string | null } | null;
}>;
}
>;
privateVehicles: PrivateVehicleTransport[];
@@ -63,6 +66,7 @@ export type TimelineEntry =
date: string | null;
time: string | null;
plan: Plan;
flightSegmentIndex: number | null;
tour: TourContext | null;
tourDay: TourDayContext | null;
sortRank: number;
@@ -91,7 +95,10 @@ function normalizeTime(value: string | null | undefined): string | null {
return value.slice(0, 5);
}
function splitDateTime(value: string | null | undefined): { date: string | null; time: string | null } {
function splitDateTime(value: string | null | undefined): {
date: string | null;
time: string | null;
} {
if (!value) return { date: null, time: null };
const [date, time] = value.split('T');
return { date: date ?? null, time: normalizeTime(time) };
@@ -107,7 +114,10 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
);
const lodgingByPlanId = new Map(input.lodgings.map((lodging) => [lodging.plan_id, lodging]));
const experienceByPlanId = new Map(
[...input.activities, ...input.restaurants].map((experience) => [experience.plan_id, experience])
[...input.activities, ...input.restaurants].map((experience) => [
experience.plan_id,
experience
])
);
const tourByPlanId = new Map(
@@ -146,8 +156,9 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
const dayContext = plan.parent_id ? dayByPlanId.get(plan.parent_id) : null;
const tourContext =
dayContext?.tour ??
(plan.parent_id ? tourByPlanId.get(plan.parent_id) ?? null : null) ??
(tourByPlanId.get(plan.id) ?? null);
(plan.parent_id ? (tourByPlanId.get(plan.parent_id) ?? null) : null) ??
tourByPlanId.get(plan.id) ??
null;
let date: string | null = null;
let time: string | null = null;
@@ -159,11 +170,32 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
case 'transport': {
const flight = flightByPlanId.get(plan.id);
if (flight?.segments?.length) {
const firstSegment = flight.segments[0];
const routeDateTime = splitDateTime(firstSegment.route?.departure_datetime ?? null);
date = routeDateTime.date ?? firstSegment.departure_date ?? null;
time = routeDateTime.time;
break;
for (let segmentIndex = 0; segmentIndex < flight.segments.length; segmentIndex += 1) {
const segment = flight.segments[segmentIndex];
const routeDateTime = splitDateTime(segment.route?.departure_datetime ?? null);
let segmentDate = routeDateTime.date ?? segment.departure_date ?? null;
const segmentTime = routeDateTime.time;
if (dayContext?.tour.startDate) {
segmentDate = addDaysToDate(dayContext.tour.startDate, dayContext.day.dayNumber - 1);
}
const sortTime = segmentTime ?? '24:00';
entries.push({
kind: 'plan',
id: `${plan.id}-segment-${segmentIndex}`,
date: segmentDate,
time: segmentTime,
plan,
flightSegmentIndex: segmentIndex,
tour: tourContext,
tourDay: dayContext?.day ?? null,
sortRank: 1,
sortTime,
sequence: sequence++
});
}
continue;
}
const privateVehicle = privateVehicleByPlanId.get(plan.id);
if (privateVehicle) {
@@ -176,9 +208,7 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
const otherTransport = otherTransportByPlanId.get(plan.id);
if (otherTransport) {
date = otherTransport.start_date ?? otherTransport.end_date ?? null;
time =
normalizeTime(otherTransport.start_time) ??
normalizeTime(otherTransport.end_time);
time = normalizeTime(otherTransport.start_time) ?? normalizeTime(otherTransport.end_time);
}
break;
}
@@ -186,9 +216,7 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
const lodging = lodgingByPlanId.get(plan.id);
if (lodging) {
date = lodging.check_in_date ?? lodging.check_out_date ?? null;
time =
normalizeTime(lodging.check_in_time) ??
normalizeTime(lodging.check_out_time);
time = normalizeTime(lodging.check_in_time) ?? normalizeTime(lodging.check_out_time);
}
break;
}
@@ -197,9 +225,7 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
const experience = experienceByPlanId.get(plan.id);
if (experience) {
date = experience.start_date ?? experience.end_date ?? null;
time =
normalizeTime(experience.start_time) ??
normalizeTime(experience.end_time);
time = normalizeTime(experience.start_time) ?? normalizeTime(experience.end_time);
}
break;
}
@@ -228,6 +254,7 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
date,
time,
plan,
flightSegmentIndex: null,
tour: tourContext,
tourDay: dayContext?.day ?? null,
sortRank: 1,
@@ -245,9 +272,7 @@ export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
dayNumber: day.day_number,
title: day.title ?? null
};
const dayDate = tour.start_date
? addDaysToDate(tour.start_date, day.day_number - 1)
: null;
const dayDate = tour.start_date ? addDaysToDate(tour.start_date, day.day_number - 1) : null;
entries.push({
kind: 'day',
id: `day-${day.plan_id}`,

View File

@@ -155,21 +155,89 @@
return t.slice(0, 5);
}
function addDaysToDate(date: string, offset: number): string {
const [year, month, day] = date.split('-').map(Number);
const ts = Date.UTC(year, month - 1, day + offset);
return new Date(ts).toISOString().slice(0, 10);
}
function toggleChecklistItem(itemId: string | number, nextChecked: boolean) {
togglingChecklistItemId = String(itemId);
togglingChecklistItemChecked = nextChecked ? '1' : '0';
setTimeout(() => checklistToggleFormRef?.requestSubmit(), 0);
}
const flightBookingByPlanId = $derived(new Map(flightBookings.map((booking) => [booking.plan_id, booking])));
const privateVehicleByPlanId = $derived(new Map(privateVehicles.map((vehicle) => [vehicle.plan_id, vehicle])));
const otherTransportByPlanId = $derived(new Map(otherTransports.map((transport) => [transport.plan_id, transport])));
const flightBookingByPlanId = $derived(
new Map(flightBookings.map((booking) => [booking.plan_id, booking]))
);
const privateVehicleByPlanId = $derived(
new Map(privateVehicles.map((vehicle) => [vehicle.plan_id, vehicle]))
);
const otherTransportByPlanId = $derived(
new Map(otherTransports.map((transport) => [transport.plan_id, transport]))
);
const lodgingByPlanId = $derived(new Map(lodgings.map((lodging) => [lodging.plan_id, lodging])));
const activityByPlanId = $derived(new Map(activities.map((activity) => [activity.plan_id, activity])));
const restaurantByPlanId = $derived(new Map(restaurants.map((restaurant) => [restaurant.plan_id, restaurant])));
const activityByPlanId = $derived(
new Map(activities.map((activity) => [activity.plan_id, activity]))
);
const restaurantByPlanId = $derived(
new Map(restaurants.map((restaurant) => [restaurant.plan_id, restaurant]))
);
const packingListByPlanId = $derived(new Map(packingLists.map((list) => [list.plan_id, list])));
const todoByPlanId = $derived(new Map(todos.map((list) => [list.plan_id, list])));
const tourByPlanId = $derived(new Map(packageTours.map((tour) => [tour.plan_id, tour])));
const byTypeTourContextByPlanId = $derived(
(() => {
const dayContextByPlanId = new Map<
string,
{
tour: (typeof packageTours)[number];
dayNumber: number;
dayTitle: string | null;
dayDate: string | null;
}
>();
for (const tour of packageTours) {
for (const day of tour.days) {
dayContextByPlanId.set(day.plan_id, {
tour,
dayNumber: day.day_number,
dayTitle: day.title ?? null,
dayDate: tour.start_date ? addDaysToDate(tour.start_date, day.day_number - 1) : null
});
}
}
const contextByPlanId = new Map<
string,
{
tour: (typeof packageTours)[number];
dayNumber: number | null;
dayTitle: string | null;
dayDate: string | null;
}
>();
for (const plan of plans) {
if (!plan.parent_id) continue;
const dayContext = dayContextByPlanId.get(plan.parent_id);
if (dayContext) {
contextByPlanId.set(plan.id, dayContext);
continue;
}
const parentTour = tourByPlanId.get(plan.parent_id);
if (parentTour) {
contextByPlanId.set(plan.id, {
tour: parentTour,
dayNumber: null,
dayTitle: null,
dayDate: null
});
}
}
return contextByPlanId;
})()
);
const timeline = $derived(
buildTripTimeline({
@@ -593,7 +661,9 @@
{:else}
<div class="mt-8 flex flex-wrap items-center justify-between gap-3">
<h2 class="text-sm font-semibold tracking-wider text-gray-400 uppercase">Plans</h2>
<div class="inline-flex rounded-md border border-gray-200 bg-white p-1 text-xs font-medium text-gray-500">
<div
class="inline-flex rounded-md border border-gray-200 bg-white p-1 text-xs font-medium text-gray-500"
>
{#each planViewOptions as option}
<button
type="button"
@@ -638,16 +708,30 @@
showName={false}
/>
<p class="text-sm font-semibold text-gray-800">
Day {entry.day.dayNumber}{entry.day.title ? ` ${entry.day.title}` : ''}
Day {entry.day.dayNumber}{entry.day.title
? ` ${entry.day.title}`
: ''}
</p>
<span class="text-xs text-gray-500">
{entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''}
{entry.tour.operatorName}{entry.tour.tourName
? `: ${entry.tour.tourName}`
: ''}
</span>
</div>
</div>
{:else}
{@const plan = entry.plan}
{@const timeLabel = formatTimelineTime(entry.time)}
{@const timeLabel = entry.time ? formatTimelineTime(entry.time) : ''}
{@const hasMatchingDayHeader =
!!entry.tourDay &&
group.entries.some(
(candidate) =>
candidate.kind === 'day' &&
candidate.tour.tourId === entry.tour?.tourId &&
candidate.day.dayNumber === entry.tourDay?.dayNumber
)}
{@const showInlineTourContext =
!!entry.tour && plan.type !== 'tour' && !hasMatchingDayHeader}
{@const flightBooking = flightBookingByPlanId.get(plan.id)}
{@const privateVehicle = privateVehicleByPlanId.get(plan.id)}
{@const otherTransport = otherTransportByPlanId.get(plan.id)}
@@ -658,12 +742,16 @@
{@const todoList = todoByPlanId.get(plan.id)}
{@const tour = tourByPlanId.get(plan.id)}
<div class="flex gap-4">
<div class="w-16 shrink-0 pt-2 text-right text-xs font-medium text-gray-400">
<div
class="w-16 shrink-0 pt-2 text-right text-xs font-medium text-gray-400"
>
{timeLabel}
</div>
<div class="min-w-0 flex-1">
{#if entry.tour && plan.type !== 'tour'}
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs text-gray-500">
{#if showInlineTourContext}
<div
class="mb-2 flex flex-wrap items-center gap-2 text-xs text-gray-500"
>
<TourBadge
operatorName={entry.tour.operatorName}
tourName={entry.tour.tourName}
@@ -672,17 +760,21 @@
showName={false}
/>
<span class="font-medium text-gray-600">
{entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''}
{entry.tour.operatorName}{entry.tour.tourName
? `: ${entry.tour.tourName}`
: ''}
</span>
{#if entry.tourDay}
<span class="text-gray-400">
• Day {entry.tourDay.dayNumber}{entry.tourDay.title ? ` ${entry.tourDay.title}` : ''}
• Day {entry.tourDay.dayNumber}{entry.tourDay.title
? ` ${entry.tourDay.title}`
: ''}
</span>
{/if}
</div>
{/if}
<div
class="{entry.tour && plan.type !== 'tour' ? 'border-l-4 pl-3' : ''}"
class={entry.tour && plan.type !== 'tour' ? 'border-l-4 pl-3' : ''}
style="border-left-color: {entry.tour?.highlightColor ?? '#E5E7EB'}"
>
{#if plan.type === 'destination'}
@@ -724,9 +816,18 @@
>
<input type="hidden" name="plan_id" value={plan.id} />
{#if flightBooking}
{@const timelineFlightBooking =
entry.flightSegmentIndex == null
? flightBooking
: {
...flightBooking,
segments: [
flightBooking.segments[entry.flightSegmentIndex]
].filter(Boolean)
}}
<FlightCard
{plan}
{flightBooking}
flightBooking={timelineFlightBooking}
onEdit={() =>
(editingTransportation = {
type: 'flight',
@@ -809,7 +910,8 @@
<ExperienceCard
{plan}
experience={restaurant}
onEdit={() => (editingRestaurant = { plan, experience: restaurant })}
onEdit={() =>
(editingRestaurant = { plan, experience: restaurant })}
onDelete={submitForm}
/>
</form>
@@ -951,7 +1053,9 @@
Day {entry.day.dayNumber}{entry.day.title ? ` ${entry.day.title}` : ''}
</p>
<span class="text-xs text-gray-500">
{entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''}
{entry.tour.operatorName}{entry.tour.tourName
? `: ${entry.tour.tourName}`
: ''}
</span>
<span class="text-xs text-gray-400">Date TBD</span>
</div>
@@ -983,17 +1087,21 @@
showName={false}
/>
<span class="font-medium text-gray-600">
{entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''}
{entry.tour.operatorName}{entry.tour.tourName
? `: ${entry.tour.tourName}`
: ''}
</span>
{#if entry.tourDay}
<span class="text-gray-400">
• Day {entry.tourDay.dayNumber}{entry.tourDay.title ? ` ${entry.tourDay.title}` : ''}
• Day {entry.tourDay.dayNumber}{entry.tourDay.title
? ` ${entry.tourDay.title}`
: ''}
</span>
{/if}
</div>
{/if}
<div
class="{entry.tour && plan.type !== 'tour' ? 'border-l-4 pl-3' : ''}"
class={entry.tour && plan.type !== 'tour' ? 'border-l-4 pl-3' : ''}
style="border-left-color: {entry.tour?.highlightColor ?? '#E5E7EB'}"
>
{#if plan.type === 'destination'}
@@ -1035,9 +1143,18 @@
>
<input type="hidden" name="plan_id" value={plan.id} />
{#if flightBooking}
{@const timelineFlightBooking =
entry.flightSegmentIndex == null
? flightBooking
: {
...flightBooking,
segments: [
flightBooking.segments[entry.flightSegmentIndex]
].filter(Boolean)
}}
<FlightCard
{plan}
{flightBooking}
flightBooking={timelineFlightBooking}
onEdit={() =>
(editingTransportation = {
type: 'flight',
@@ -1120,7 +1237,8 @@
<ExperienceCard
{plan}
experience={restaurant}
onEdit={() => (editingRestaurant = { plan, experience: restaurant })}
onEdit={() =>
(editingRestaurant = { plan, experience: restaurant })}
onDelete={submitForm}
/>
</form>
@@ -1245,6 +1363,7 @@
<div class="flex flex-col gap-3">
{#each plans.filter((p) => p.type === 'destination') as plan (plan.id)}
{@const formId = `remove-plan-${plan.id}`}
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
{@const submitForm = () => {
const form = document.getElementById(formId) as HTMLFormElement;
form?.requestSubmit();
@@ -1261,7 +1380,41 @@
class="contents"
>
<input type="hidden" name="plan_id" value={plan.id} />
<div
class={packageTourContext
? 'package-tour-context-card rounded-xl border-2 border-gray-200 p-2'
: ''}
style="border-color: {packageTourContext?.tour.highlight_color ?? '#E5E7EB'}"
>
{#if packageTourContext}
<div
class="mb-2 flex flex-wrap items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 text-xs text-gray-500"
>
<TourBadge
operatorName={packageTourContext.tour.operator_name}
tourName={packageTourContext.tour.tour_name}
highlightColor={packageTourContext.tour.highlight_color}
size="sm"
showName={false}
/>
<span class="font-medium text-gray-600">
{packageTourContext.tour.operator_name}{packageTourContext.tour.tour_name
? `: ${packageTourContext.tour.tour_name}`
: ''}
</span>
{#if packageTourContext.dayNumber != null}
<span class="text-gray-400">
• Day {packageTourContext.dayNumber}{packageTourContext.dayTitle
? ` ${packageTourContext.dayTitle}`
: ''}{packageTourContext.dayDate
? ` · ${formatTimelineDate(packageTourContext.dayDate)}`
: ''}
</span>
{/if}
</div>
{/if}
<PlanCard {plan} onDelete={submitForm} />
</div>
</form>
{/each}
</div>
@@ -1278,6 +1431,7 @@
{@const flightBooking = flightBookings.find((b) => b.plan_id === plan.id)}
{@const privateVehicle = privateVehicles.find((pv) => pv.plan_id === plan.id)}
{@const otherTransport = otherTransports.find((ot) => ot.plan_id === plan.id)}
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
{#if plan}
{@const formId = `remove-plan-${plan.id}`}
{@const submitForm = () => {
@@ -1296,6 +1450,40 @@
class="contents"
>
<input type="hidden" name="plan_id" value={plan.id} />
<div
class={packageTourContext
? 'package-tour-context-card rounded-xl border-2 border-gray-200 p-2'
: ''}
style="border-color: {packageTourContext?.tour.highlight_color ?? '#E5E7EB'}"
>
{#if packageTourContext}
<div
class="mb-2 flex flex-wrap items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 text-xs text-gray-500"
>
<TourBadge
operatorName={packageTourContext.tour.operator_name}
tourName={packageTourContext.tour.tour_name}
highlightColor={packageTourContext.tour.highlight_color}
size="sm"
showName={false}
/>
<span class="font-medium text-gray-600">
{packageTourContext.tour.operator_name}{packageTourContext.tour
.tour_name
? `: ${packageTourContext.tour.tour_name}`
: ''}
</span>
{#if packageTourContext.dayNumber != null}
<span class="text-gray-400">
• Day {packageTourContext.dayNumber}{packageTourContext.dayTitle
? ` ${packageTourContext.dayTitle}`
: ''}{packageTourContext.dayDate
? ` · ${formatTimelineDate(packageTourContext.dayDate)}`
: ''}
</span>
{/if}
</div>
{/if}
{#if flightBooking}
<FlightCard
{plan}
@@ -1335,6 +1523,7 @@
onDelete={submitForm}
/>
{/if}
</div>
</form>
{/if}
{/each}
@@ -1353,6 +1542,7 @@
{@const plan = plans.find((p) => p.id === activity.plan_id)}
{#if plan}
{@const formId = `remove-plan-${plan.id}`}
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
{@const submitForm = () => {
const form = document.getElementById(formId) as HTMLFormElement;
form?.requestSubmit();
@@ -1369,12 +1559,47 @@
class="contents"
>
<input type="hidden" name="plan_id" value={plan.id} />
<div
class={packageTourContext
? 'package-tour-context-card rounded-xl border-2 border-gray-200 p-2'
: ''}
style="border-color: {packageTourContext?.tour.highlight_color ?? '#E5E7EB'}"
>
{#if packageTourContext}
<div
class="mb-2 flex flex-wrap items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 text-xs text-gray-500"
>
<TourBadge
operatorName={packageTourContext.tour.operator_name}
tourName={packageTourContext.tour.tour_name}
highlightColor={packageTourContext.tour.highlight_color}
size="sm"
showName={false}
/>
<span class="font-medium text-gray-600">
{packageTourContext.tour.operator_name}{packageTourContext.tour
.tour_name
? `: ${packageTourContext.tour.tour_name}`
: ''}
</span>
{#if packageTourContext.dayNumber != null}
<span class="text-gray-400">
• Day {packageTourContext.dayNumber}{packageTourContext.dayTitle
? ` ${packageTourContext.dayTitle}`
: ''}{packageTourContext.dayDate
? ` · ${formatTimelineDate(packageTourContext.dayDate)}`
: ''}
</span>
{/if}
</div>
{/if}
<ExperienceCard
{plan}
experience={activity}
onEdit={() => (editingActivity = { plan, experience: activity })}
onDelete={submitForm}
/>
</div>
</form>
{/if}
{/each}
@@ -1393,6 +1618,7 @@
{@const plan = plans.find((p) => p.id === restaurant.plan_id)}
{#if plan}
{@const formId = `remove-plan-${plan.id}`}
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
{@const submitForm = () => {
const form = document.getElementById(formId) as HTMLFormElement;
form?.requestSubmit();
@@ -1409,12 +1635,47 @@
class="contents"
>
<input type="hidden" name="plan_id" value={plan.id} />
<div
class={packageTourContext
? 'package-tour-context-card rounded-xl border-2 border-gray-200 p-2'
: ''}
style="border-color: {packageTourContext?.tour.highlight_color ?? '#E5E7EB'}"
>
{#if packageTourContext}
<div
class="mb-2 flex flex-wrap items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 text-xs text-gray-500"
>
<TourBadge
operatorName={packageTourContext.tour.operator_name}
tourName={packageTourContext.tour.tour_name}
highlightColor={packageTourContext.tour.highlight_color}
size="sm"
showName={false}
/>
<span class="font-medium text-gray-600">
{packageTourContext.tour.operator_name}{packageTourContext.tour
.tour_name
? `: ${packageTourContext.tour.tour_name}`
: ''}
</span>
{#if packageTourContext.dayNumber != null}
<span class="text-gray-400">
• Day {packageTourContext.dayNumber}{packageTourContext.dayTitle
? ` ${packageTourContext.dayTitle}`
: ''}{packageTourContext.dayDate
? ` · ${formatTimelineDate(packageTourContext.dayDate)}`
: ''}
</span>
{/if}
</div>
{/if}
<ExperienceCard
{plan}
experience={restaurant}
onEdit={() => (editingRestaurant = { plan, experience: restaurant })}
onDelete={submitForm}
/>
</div>
</form>
{/if}
{/each}
@@ -1466,7 +1727,9 @@
<!-- To-dos section -->
{#if todos.length > 0}
<div class="mt-8">
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">To-dos</h2>
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
To-dos
</h2>
<div class="flex flex-col gap-3">
{#each todos as list (list.id)}
{@const plan = plans.find((p) => p.id === list.plan_id)}
@@ -1513,6 +1776,7 @@
{@const plan = plans.find((p) => p.id === lodging.plan_id)}
{#if plan}
{@const formId = `remove-plan-${plan.id}`}
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
{@const submitForm = () => {
const form = document.getElementById(formId) as HTMLFormElement;
form?.requestSubmit();
@@ -1529,12 +1793,47 @@
class="contents"
>
<input type="hidden" name="plan_id" value={plan.id} />
<div
class={packageTourContext
? 'package-tour-context-card rounded-xl border-2 border-gray-200 p-2'
: ''}
style="border-color: {packageTourContext?.tour.highlight_color ?? '#E5E7EB'}"
>
{#if packageTourContext}
<div
class="mb-2 flex flex-wrap items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 text-xs text-gray-500"
>
<TourBadge
operatorName={packageTourContext.tour.operator_name}
tourName={packageTourContext.tour.tour_name}
highlightColor={packageTourContext.tour.highlight_color}
size="sm"
showName={false}
/>
<span class="font-medium text-gray-600">
{packageTourContext.tour.operator_name}{packageTourContext.tour
.tour_name
? `: ${packageTourContext.tour.tour_name}`
: ''}
</span>
{#if packageTourContext.dayNumber != null}
<span class="text-gray-400">
• Day {packageTourContext.dayNumber}{packageTourContext.dayTitle
? ` ${packageTourContext.dayTitle}`
: ''}{packageTourContext.dayDate
? ` · ${formatTimelineDate(packageTourContext.dayDate)}`
: ''}
</span>
{/if}
</div>
{/if}
<LodgingCard
{plan}
{lodging}
onEdit={() => (editingLodging = lodging)}
onDelete={submitForm}
/>
</div>
</form>
{/if}
{/each}
@@ -1601,3 +1900,10 @@
{/if}
{/if}
</div>
<style>
:global(.package-tour-context-card > div.rounded-xl.border) {
border-color: transparent;
box-shadow: none;
}
</style>