From a31664d30ae21e5194c6255326b341d9f56abe85 Mon Sep 17 00:00:00 2001 From: Shaun Campbell Date: Sun, 22 Feb 2026 22:10:53 -0500 Subject: [PATCH] tests) fixing tests --- e2e/helpers/trip.ts | 15 +- e2e/trip-timeline.test.ts | 36 +- src/lib/components/TripList.svelte | 41 +- src/lib/timeline.test.ts | 159 +- src/lib/timeline.ts | 69 +- .../(protected)/trips/[id]/+page.svelte | 1376 ++++++++++------- 6 files changed, 1092 insertions(+), 604 deletions(-) diff --git a/e2e/helpers/trip.ts b/e2e/helpers/trip.ts index 42fa481..684b207 100644 --- a/e2e/helpers/trip.ts +++ b/e2e/helpers/trip.ts @@ -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 }; diff --git a/e2e/trip-timeline.test.ts b/e2e/trip-timeline.test.ts index 23dba8f..c5383fb 100644 --- a/e2e/trip-timeline.test.ts +++ b/e2e/trip-timeline.test.ts @@ -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(); diff --git a/src/lib/components/TripList.svelte b/src/lib/components/TripList.svelte index 35bfcb6..ab3a61c 100644 --- a/src/lib/components/TripList.svelte +++ b/src/lib/components/TripList.svelte @@ -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'); -
+

{title}

- {#if trips.length > 0} + {#if tripList.length > 0}
-{#if trips.length === 0} +{#if tripList.length === 0}

{emptyMessage}

{:else if view === 'tile'}
- {#each trips as trip} + {#each tripList as trip} {/each}
{:else} - + {/if} diff --git a/src/lib/timeline.test.ts b/src/lib/timeline.test.ts index 6ddd52a..ead7177 100644 --- a/src/lib/timeline.test.ts +++ b/src/lib/timeline.test.ts @@ -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); }); }); diff --git a/src/lib/timeline.ts b/src/lib/timeline.ts index fa719f9..e6a2366 100644 --- a/src/lib/timeline.ts +++ b/src/lib/timeline.ts @@ -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}`, diff --git a/src/routes/(protected)/trips/[id]/+page.svelte b/src/routes/(protected)/trips/[id]/+page.svelte index 9c9cc46..40389e2 100644 --- a/src/routes/(protected)/trips/[id]/+page.svelte +++ b/src/routes/(protected)/trips/[id]/+page.svelte @@ -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}

Plans

-
+
{#each planViewOptions as option}
{: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)}
-
+
{timeLabel}
- {#if entry.tour && plan.type !== 'tour'} -
+ {#if showInlineTourContext} +
- {entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''} + {entry.tour.operatorName}{entry.tour.tourName + ? `: ${entry.tour.tourName}` + : ''} {#if entry.tourDay} - • Day {entry.tourDay.dayNumber}{entry.tourDay.title ? ` — ${entry.tourDay.title}` : ''} + • Day {entry.tourDay.dayNumber}{entry.tourDay.title + ? ` — ${entry.tourDay.title}` + : ''} {/if}
{/if}
{#if plan.type === 'destination'} @@ -724,9 +816,18 @@ > {#if flightBooking} + {@const timelineFlightBooking = + entry.flightSegmentIndex == null + ? flightBooking + : { + ...flightBooking, + segments: [ + flightBooking.segments[entry.flightSegmentIndex] + ].filter(Boolean) + }} (editingTransportation = { type: 'flight', @@ -809,7 +910,8 @@ (editingRestaurant = { plan, experience: restaurant })} + onEdit={() => + (editingRestaurant = { plan, experience: restaurant })} onDelete={submitForm} /> @@ -951,7 +1053,9 @@ Day {entry.day.dayNumber}{entry.day.title ? ` — ${entry.day.title}` : ''}

- {entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''} + {entry.tour.operatorName}{entry.tour.tourName + ? `: ${entry.tour.tourName}` + : ''} Date TBD
@@ -973,8 +1077,8 @@ {timeLabel}
- {#if entry.tour && plan.type !== 'tour'} -
+ {#if entry.tour && plan.type !== 'tour'} +
- {entry.tour.operatorName}{entry.tour.tourName ? `: ${entry.tour.tourName}` : ''} + {entry.tour.operatorName}{entry.tour.tourName + ? `: ${entry.tour.tourName}` + : ''} {#if entry.tourDay} - • Day {entry.tourDay.dayNumber}{entry.tourDay.title ? ` — ${entry.tourDay.title}` : ''} + • Day {entry.tourDay.dayNumber}{entry.tourDay.title + ? ` — ${entry.tourDay.title}` + : ''} {/if}
{/if} -
+
{#if plan.type === 'destination'} {@const formId = `remove-plan-timeline-${plan.id}`} {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }}
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - - - {:else if plan.type === 'transport'} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); + id={formId} + method="POST" + action="?/removePlan" + use:enhance={() => { + return ({ update }) => { + update(); + }; }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - {#if flightBooking} - - (editingTransportation = { - type: 'flight', - flightBooking, - planStatus: plan.status - })} - onDelete={submitForm} - /> - {:else if privateVehicle} - - (editingTransportation = { - type: 'private_vehicle', - privateVehicle, - planStatus: plan.status - })} - onDelete={submitForm} - /> - {:else if otherTransport} - - (editingTransportation = { - type: 'other', - otherTransport, - planStatus: plan.status, - planTitle: plan.title, - planNotes: plan.notes - })} - onDelete={submitForm} - /> - {/if} - - {:else if plan.type === 'activity' && activity} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); + class="contents" + > + + + + {:else if plan.type === 'transport'} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; }} - { - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - + + {#if flightBooking} + {@const timelineFlightBooking = + entry.flightSegmentIndex == null + ? flightBooking + : { + ...flightBooking, + segments: [ + flightBooking.segments[entry.flightSegmentIndex] + ].filter(Boolean) + }} + (editingActivity = { plan, experience: activity })} + flightBooking={timelineFlightBooking} + onEdit={() => + (editingTransportation = { + type: 'flight', + flightBooking, + planStatus: plan.status + })} onDelete={submitForm} /> - - {:else if plan.type === 'restaurant' && restaurant} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingRestaurant = { plan, experience: restaurant })} + {privateVehicle} + onEdit={() => + (editingTransportation = { + type: 'private_vehicle', + privateVehicle, + planStatus: plan.status + })} onDelete={submitForm} /> - - {:else if plan.type === 'lodging' && lodging} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingLodging = lodging)} + {otherTransport} + onEdit={() => + (editingTransportation = { + type: 'other', + otherTransport, + planStatus: plan.status, + planTitle: plan.title, + planNotes: plan.notes + })} onDelete={submitForm} /> - - {:else if plan.type === 'packing' && packingList} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); + {/if} + + {:else if plan.type === 'activity' && activity} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; }} - { - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingPackingList = { plan, list: packingList })} - onDelete={submitForm} - onToggleItem={toggleChecklistItem} - /> - - {:else if plan.type === 'todo' && todoList} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); + class="contents" + > + + (editingActivity = { plan, experience: activity })} + onDelete={submitForm} + /> + + {:else if plan.type === 'restaurant' && restaurant} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; }} - { - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingTodo = { plan, list: todoList })} - onDelete={submitForm} - onToggleItem={toggleChecklistItem} - /> - - {:else if plan.type === 'tour' && tour} - {@const formId = `remove-plan-timeline-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); + class="contents" + > + + + (editingRestaurant = { plan, experience: restaurant })} + onDelete={submitForm} + /> + + {:else if plan.type === 'lodging' && lodging} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; }} - { - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingTour = tour)} - onDelete={submitForm} - /> - - {/if} -
+ class="contents" + > + + (editingLodging = lodging)} + onDelete={submitForm} + /> + + {:else if plan.type === 'packing' && packingList} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingPackingList = { plan, list: packingList })} + onDelete={submitForm} + onToggleItem={toggleChecklistItem} + /> + + {:else if plan.type === 'todo' && todoList} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingTodo = { plan, list: todoList })} + onDelete={submitForm} + onToggleItem={toggleChecklistItem} + /> + + {:else if plan.type === 'tour' && tour} + {@const formId = `remove-plan-timeline-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingTour = tour)} + onDelete={submitForm} + /> + + {/if}
+
{/if} {/each}
@@ -1245,6 +1363,7 @@
{#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" > - +
+ {#if packageTourContext} +
+ + + {packageTourContext.tour.operator_name}{packageTourContext.tour.tour_name + ? `: ${packageTourContext.tour.tour_name}` + : ''} + + {#if packageTourContext.dayNumber != null} + + • Day {packageTourContext.dayNumber}{packageTourContext.dayTitle + ? ` — ${packageTourContext.dayTitle}` + : ''}{packageTourContext.dayDate + ? ` · ${formatTimelineDate(packageTourContext.dayDate)}` + : ''} + + {/if} +
+ {/if} + +
{/each}
@@ -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,308 +1450,460 @@ class="contents" > - {#if flightBooking} - - (editingTransportation = { - type: 'flight', - flightBooking, - planStatus: plan.status - })} - onDelete={submitForm} - /> - {:else if privateVehicle} - - (editingTransportation = { - type: 'private_vehicle', - privateVehicle, - planStatus: plan.status - })} - onDelete={submitForm} - /> - {:else if otherTransport} - - (editingTransportation = { - type: 'other', - otherTransport, - planStatus: plan.status, - planTitle: plan.title, - planNotes: plan.notes - })} - onDelete={submitForm} - /> - {/if} +
+ {#if packageTourContext} +
+ + + {packageTourContext.tour.operator_name}{packageTourContext.tour + .tour_name + ? `: ${packageTourContext.tour.tour_name}` + : ''} + + {#if packageTourContext.dayNumber != null} + + • Day {packageTourContext.dayNumber}{packageTourContext.dayTitle + ? ` — ${packageTourContext.dayTitle}` + : ''}{packageTourContext.dayDate + ? ` · ${formatTimelineDate(packageTourContext.dayDate)}` + : ''} + + {/if} +
+ {/if} + {#if flightBooking} + + (editingTransportation = { + type: 'flight', + flightBooking, + planStatus: plan.status + })} + onDelete={submitForm} + /> + {:else if privateVehicle} + + (editingTransportation = { + type: 'private_vehicle', + privateVehicle, + planStatus: plan.status + })} + onDelete={submitForm} + /> + {:else if otherTransport} + + (editingTransportation = { + type: 'other', + otherTransport, + planStatus: plan.status, + planTitle: plan.title, + planNotes: plan.notes + })} + onDelete={submitForm} + /> + {/if} +
{/if} - {/each} + {/each} +
-
- {/if} + {/if} - - {#if activities.length > 0} -
-

- Attractions & Activities -

-
- {#each activities as activity (activity.id)} - {@const plan = plans.find((p) => p.id === activity.plan_id)} - {#if plan} - {@const formId = `remove-plan-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; + + {#if activities.length > 0} +
+

+ Attractions & Activities +

+
+ {#each activities as activity (activity.id)} + {@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(); }} - class="contents" - > - - (editingActivity = { plan, experience: activity })} - onDelete={submitForm} - /> - - {/if} - {/each} -
-
- {/if} - - - {#if restaurants.length > 0} -
-

- Restaurants -

-
- {#each restaurants as restaurant (restaurant.id)} - {@const plan = plans.find((p) => p.id === restaurant.plan_id)} - {#if plan} - {@const formId = `remove-plan-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingRestaurant = { plan, experience: restaurant })} - onDelete={submitForm} - /> - - {/if} - {/each} -
-
- {/if} - - - {#if packingLists.length > 0} -
-

- Packing Lists -

-
- {#each packingLists as list (list.id)} - {@const plan = plans.find((p) => p.id === list.plan_id)} - {#if plan} - {@const formId = `remove-plan-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingPackingList = { plan, list })} - onDelete={submitForm} - onToggleItem={toggleChecklistItem} - /> - - {/if} - {/each} -
-
- {/if} - - - {#if todos.length > 0} -
-

To-dos

-
- {#each todos as list (list.id)} - {@const plan = plans.find((p) => p.id === list.plan_id)} - {#if plan} - {@const formId = `remove-plan-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingTodo = { plan, list })} - onDelete={submitForm} - onToggleItem={toggleChecklistItem} - /> - - {/if} - {/each} -
-
- {/if} - - - {#if lodgings.length > 0} -
-

- Lodgings -

-
- {#each lodgings as lodging (lodging.id)} - {@const plan = plans.find((p) => p.id === lodging.plan_id)} - {#if plan} - {@const formId = `remove-plan-${plan.id}`} - {@const submitForm = () => { - const form = document.getElementById(formId) as HTMLFormElement; - form?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingLodging = lodging)} - onDelete={submitForm} - /> - - {/if} - {/each} -
-
- {/if} - - - {#if packageTours.length > 0} -
-

- Package Tours -

-
- {#each packageTours as tour (tour.id)} - {@const plan = plans.find((p) => p.id === tour.plan_id)} - {#if plan} - {@const formId = `remove-plan-${plan.id}`} - {@const submitForm = () => { - const f = document.getElementById(formId) as HTMLFormElement; - f?.requestSubmit(); - }} -
{ - return ({ update }) => { - update(); - }; - }} - class="contents" - > - - (editingTour = tour)} - onDelete={submitForm} - onAddTransportation={() => { - addingChildToPlanId = plan.id; - showAddTransportation = true; + { + return ({ update }) => { + update(); + }; }} - onAddLodging={() => { - addingChildToPlanId = plan.id; - showAddLodging = true; - }} - onAddTransportationToDay={(dayPlanId) => { - addingChildToPlanId = dayPlanId; - showAddTransportation = true; - }} - onAddLodgingToDay={(dayPlanId) => { - addingChildToPlanId = dayPlanId; - showAddLodging = true; - }} - /> - - {/if} - {/each} + class="contents" + > + +
+ {#if packageTourContext} +
+ + + {packageTourContext.tour.operator_name}{packageTourContext.tour + .tour_name + ? `: ${packageTourContext.tour.tour_name}` + : ''} + + {#if packageTourContext.dayNumber != null} + + • Day {packageTourContext.dayNumber}{packageTourContext.dayTitle + ? ` — ${packageTourContext.dayTitle}` + : ''}{packageTourContext.dayDate + ? ` · ${formatTimelineDate(packageTourContext.dayDate)}` + : ''} + + {/if} +
+ {/if} + (editingActivity = { plan, experience: activity })} + onDelete={submitForm} + /> +
+ + {/if} + {/each} +
-
- {/if} + {/if} + + + {#if restaurants.length > 0} +
+

+ Restaurants +

+
+ {#each restaurants as restaurant (restaurant.id)} + {@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(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + +
+ {#if packageTourContext} +
+ + + {packageTourContext.tour.operator_name}{packageTourContext.tour + .tour_name + ? `: ${packageTourContext.tour.tour_name}` + : ''} + + {#if packageTourContext.dayNumber != null} + + • Day {packageTourContext.dayNumber}{packageTourContext.dayTitle + ? ` — ${packageTourContext.dayTitle}` + : ''}{packageTourContext.dayDate + ? ` · ${formatTimelineDate(packageTourContext.dayDate)}` + : ''} + + {/if} +
+ {/if} + (editingRestaurant = { plan, experience: restaurant })} + onDelete={submitForm} + /> +
+
+ {/if} + {/each} +
+
+ {/if} + + + {#if packingLists.length > 0} +
+

+ Packing Lists +

+
+ {#each packingLists as list (list.id)} + {@const plan = plans.find((p) => p.id === list.plan_id)} + {#if plan} + {@const formId = `remove-plan-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingPackingList = { plan, list })} + onDelete={submitForm} + onToggleItem={toggleChecklistItem} + /> + + {/if} + {/each} +
+
+ {/if} + + + {#if todos.length > 0} +
+

+ To-dos +

+
+ {#each todos as list (list.id)} + {@const plan = plans.find((p) => p.id === list.plan_id)} + {#if plan} + {@const formId = `remove-plan-${plan.id}`} + {@const submitForm = () => { + const form = document.getElementById(formId) as HTMLFormElement; + form?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingTodo = { plan, list })} + onDelete={submitForm} + onToggleItem={toggleChecklistItem} + /> + + {/if} + {/each} +
+
+ {/if} + + + {#if lodgings.length > 0} +
+

+ Lodgings +

+
+ {#each lodgings as lodging (lodging.id)} + {@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(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + +
+ {#if packageTourContext} +
+ + + {packageTourContext.tour.operator_name}{packageTourContext.tour + .tour_name + ? `: ${packageTourContext.tour.tour_name}` + : ''} + + {#if packageTourContext.dayNumber != null} + + • Day {packageTourContext.dayNumber}{packageTourContext.dayTitle + ? ` — ${packageTourContext.dayTitle}` + : ''}{packageTourContext.dayDate + ? ` · ${formatTimelineDate(packageTourContext.dayDate)}` + : ''} + + {/if} +
+ {/if} + (editingLodging = lodging)} + onDelete={submitForm} + /> +
+
+ {/if} + {/each} +
+
+ {/if} + + + {#if packageTours.length > 0} +
+

+ Package Tours +

+
+ {#each packageTours as tour (tour.id)} + {@const plan = plans.find((p) => p.id === tour.plan_id)} + {#if plan} + {@const formId = `remove-plan-${plan.id}`} + {@const submitForm = () => { + const f = document.getElementById(formId) as HTMLFormElement; + f?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingTour = tour)} + onDelete={submitForm} + onAddTransportation={() => { + addingChildToPlanId = plan.id; + showAddTransportation = true; + }} + onAddLodging={() => { + addingChildToPlanId = plan.id; + showAddLodging = true; + }} + onAddTransportationToDay={(dayPlanId) => { + addingChildToPlanId = dayPlanId; + showAddTransportation = true; + }} + onAddLodgingToDay={(dayPlanId) => { + addingChildToPlanId = dayPlanId; + showAddLodging = true; + }} + /> + + {/if} + {/each} +
+
+ {/if} {/if} {/if} {/if}
+ +