trip) add timeline view to trip page #48
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user