trip) add timeline view to trip page
Some checks failed
PR Checks / lint-test-and-docker-build (pull_request) Failing after 1m5s
Some checks failed
PR Checks / lint-test-and-docker-build (pull_request) Failing after 1m5s
This commit is contained in:
131
e2e/trip-timeline.test.ts
Normal file
131
e2e/trip-timeline.test.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { TEST_USERS } from './setup/test-users.js';
|
||||||
|
import { loginAsLocalUser, ensureSelfProfile } from './helpers/auth.js';
|
||||||
|
import { addPackingList, createTrip, openAddToTripMenuItem, uniqueSuffix } from './helpers/trip.js';
|
||||||
|
|
||||||
|
function formatDate(offsetDays: number): string {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() + offsetDays);
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
await dialog.getByLabel('City', { exact: false }).fill(cityQuery);
|
||||||
|
const cityOption = dialog.locator('ul button').filter({ hasText: cityQuery }).first();
|
||||||
|
await expect(cityOption).toBeVisible();
|
||||||
|
await cityOption.click();
|
||||||
|
|
||||||
|
await dialog.getByLabel('Arrival').fill(startDate);
|
||||||
|
await dialog.getByRole('button', { name: 'Add to trip' }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addActivity(
|
||||||
|
page: import('@playwright/test').Page,
|
||||||
|
values: { name: string; startDate: string; startTime: string }
|
||||||
|
) {
|
||||||
|
await openAddToTripMenuItem(page, 'Attractions & Activities');
|
||||||
|
const dialog = page
|
||||||
|
.locator('[role="dialog"]')
|
||||||
|
.filter({ has: page.getByRole('heading', { name: /Attraction & activity/i }) })
|
||||||
|
.first();
|
||||||
|
await expect(dialog).toBeVisible();
|
||||||
|
|
||||||
|
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.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 }
|
||||||
|
) {
|
||||||
|
await openAddToTripMenuItem(page, 'Package Tours');
|
||||||
|
const dialog = page.getByRole('dialog', { name: 'Add package tour' });
|
||||||
|
await expect(dialog).toBeVisible();
|
||||||
|
|
||||||
|
await dialog.getByLabel('Tour operator', { exact: false }).fill(values.operatorName);
|
||||||
|
await dialog.getByLabel('Tour name', { exact: false }).fill(values.tourName);
|
||||||
|
await dialog.locator('input[name="start_date"]').fill(values.startDate);
|
||||||
|
await dialog.getByRole('button', { name: 'Add tour' }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
|
||||||
|
await expect(page.getByText(values.operatorName)).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add day' }).click();
|
||||||
|
const addDayForm = page.locator('form[action="?/addTourDay"]');
|
||||||
|
await expect(addDayForm).toBeVisible();
|
||||||
|
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();
|
||||||
|
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 }) => {
|
||||||
|
await context.clearCookies();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('timeline view shows scheduled and unscheduled plans with tour days', async ({ page }) => {
|
||||||
|
const suffix = uniqueSuffix();
|
||||||
|
const startDate = formatDate(10);
|
||||||
|
const activityDate = formatDate(11);
|
||||||
|
const destinationDate = formatDate(12);
|
||||||
|
const tourStartDate = formatDate(13);
|
||||||
|
|
||||||
|
const tripName = `E2E Timeline Trip ${suffix}`;
|
||||||
|
const activityName = `Waterfall hike ${suffix}`;
|
||||||
|
const destinationName = 'Tokyo';
|
||||||
|
const packingListName = `Timeline essentials ${suffix}`;
|
||||||
|
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);
|
||||||
|
|
||||||
|
await createTrip(page, {
|
||||||
|
name: tripName,
|
||||||
|
startDate,
|
||||||
|
description: 'Timeline view coverage'
|
||||||
|
});
|
||||||
|
|
||||||
|
await addDestination(page, destinationName, destinationDate);
|
||||||
|
await addActivity(page, { name: activityName, startDate: activityDate, startTime: '09:00' });
|
||||||
|
await addPackingList(page, { name: packingListName, items: ['Passport', 'Sunscreen'] });
|
||||||
|
await addPackageTourWithDayAndLodging(page, {
|
||||||
|
operatorName: tourOperator,
|
||||||
|
tourName,
|
||||||
|
startDate: tourStartDate,
|
||||||
|
dayTitle: tourDayTitle,
|
||||||
|
lodgingName: tourLodgingName
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Timeline' }).click();
|
||||||
|
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(`Day 1 — ${tourDayTitle}`)).toBeVisible();
|
||||||
|
await expect(page.getByText(tourLodgingName)).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByText('Unscheduled')).toBeVisible();
|
||||||
|
await expect(page.getByText(packingListName)).toBeVisible();
|
||||||
|
});
|
||||||
66
src/lib/components/TourBadge.svelte
Normal file
66
src/lib/components/TourBadge.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
operatorName: string;
|
||||||
|
tourName?: string | null;
|
||||||
|
highlightColor?: string | null;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
showName?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
operatorName,
|
||||||
|
tourName = null,
|
||||||
|
highlightColor = null,
|
||||||
|
size = 'sm',
|
||||||
|
showName = true
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function logoSlug(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_|_$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
let logoVariant = $state<'square' | 'wide' | 'none'>('square');
|
||||||
|
|
||||||
|
const squareLogoUrl = $derived(
|
||||||
|
`${base}/package-tour-logos/${logoSlug(operatorName)}_square.svg`
|
||||||
|
);
|
||||||
|
const wideLogoUrl = $derived(
|
||||||
|
`${base}/package-tour-logos/${logoSlug(operatorName)}_wide.svg`
|
||||||
|
);
|
||||||
|
const logoUrl = $derived(
|
||||||
|
logoVariant === 'square' ? squareLogoUrl : logoVariant === 'wide' ? wideLogoUrl : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const sizeClass = $derived(size === 'md' ? 'h-6 w-6' : 'h-5 w-5');
|
||||||
|
const textSizeClass = $derived(size === 'md' ? 'text-xs' : 'text-[10px]');
|
||||||
|
|
||||||
|
function handleLogoError() {
|
||||||
|
logoVariant = logoVariant === 'square' ? 'wide' : 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||||
|
{#if logoUrl}
|
||||||
|
<img
|
||||||
|
src={logoUrl}
|
||||||
|
alt={operatorName}
|
||||||
|
class="{sizeClass} rounded-md object-contain"
|
||||||
|
on:error={handleLogoError}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="{sizeClass} {textSizeClass} flex items-center justify-center rounded-md border border-gray-200 font-semibold"
|
||||||
|
style="background-color: {highlightColor ?? '#F3F4F6'}; color: {highlightColor ? '#111827' : '#6B7280'}"
|
||||||
|
>
|
||||||
|
{operatorName.slice(0, 1).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if showName}
|
||||||
|
<span class="truncate">{operatorName}{tourName ? `: ${tourName}` : ''}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
109
src/lib/components/TourTimelineCard.svelte
Normal file
109
src/lib/components/TourTimelineCard.svelte
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TourBadge from '$lib/components/TourBadge.svelte';
|
||||||
|
import type { Plan } from '$lib/server/plans.js';
|
||||||
|
import type { PackageTour } from '$lib/server/package-tours.js';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
plan: Plan;
|
||||||
|
tour: PackageTour & { highlight_color: string | null };
|
||||||
|
onEdit?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { plan, tour, onEdit, onDelete }: Props = $props();
|
||||||
|
|
||||||
|
function formatDate(d: string | null): string {
|
||||||
|
if (!d) return '';
|
||||||
|
return new Date(d + 'T00:00:00').toLocaleDateString(undefined, {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(t: string | null): string {
|
||||||
|
if (!t) return '';
|
||||||
|
return t.slice(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
idea: { label: 'Idea', class: 'bg-gray-100 text-gray-600' },
|
||||||
|
tentative: { label: 'Tentative', class: 'bg-yellow-100 text-yellow-800' },
|
||||||
|
confirmed: { label: 'Confirmed', class: 'bg-green-100 text-green-800' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasStart = $derived(tour.start_date || tour.start_time);
|
||||||
|
const hasEnd = $derived(tour.end_date || tour.end_time);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-xl border-2 bg-white p-4 shadow-sm"
|
||||||
|
style="border-color: {tour.highlight_color ?? '#E5E7EB'}"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<TourBadge
|
||||||
|
operatorName={tour.operator_name}
|
||||||
|
tourName={tour.tour_name}
|
||||||
|
highlightColor={tour.highlight_color}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
|
<span
|
||||||
|
class="rounded-full px-2.5 py-0.5 text-xs font-medium {statusConfig[plan.status].class}"
|
||||||
|
>
|
||||||
|
{statusConfig[plan.status].label}
|
||||||
|
</span>
|
||||||
|
{#if onEdit}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={onEdit}
|
||||||
|
class="rounded-md p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||||
|
aria-label="Edit tour"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if onDelete}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={onDelete}
|
||||||
|
class="rounded-md p-1.5 text-gray-400 hover:bg-gray-100 hover:text-red-600"
|
||||||
|
aria-label="Remove tour"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if hasStart || hasEnd}
|
||||||
|
<div class="mt-3 grid grid-cols-2 gap-4 border-t border-gray-100 pt-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium tracking-wide text-gray-400 uppercase">Start</p>
|
||||||
|
<p class="mt-0.5 text-sm text-gray-900">
|
||||||
|
{#if tour.start_date}
|
||||||
|
{formatDate(tour.start_date)} {formatTime(tour.start_time)}
|
||||||
|
{:else}
|
||||||
|
{formatTime(tour.start_time)}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium tracking-wide text-gray-400 uppercase">End</p>
|
||||||
|
<p class="mt-0.5 text-sm text-gray-900">
|
||||||
|
{#if tour.end_date}
|
||||||
|
{formatDate(tour.end_date)} {formatTime(tour.end_time)}
|
||||||
|
{:else}
|
||||||
|
{formatTime(tour.end_time)}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
156
src/lib/timeline.test.ts
Normal file
156
src/lib/timeline.test.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { buildTripTimeline } from './timeline.js';
|
||||||
|
import type { Plan } from './server/plans.js';
|
||||||
|
import type { PackageTour, TourDay } from './server/package-tours.js';
|
||||||
|
|
||||||
|
let planCounter = 0;
|
||||||
|
|
||||||
|
function makePlan(overrides: Partial<Plan>): Plan {
|
||||||
|
planCounter += 1;
|
||||||
|
return {
|
||||||
|
id: overrides.id ?? `plan-${planCounter}`,
|
||||||
|
trip_id: 'trip-1',
|
||||||
|
user_id: 'user-1',
|
||||||
|
type: overrides.type ?? 'destination',
|
||||||
|
status: overrides.status ?? 'idea',
|
||||||
|
parent_id: overrides.parent_id ?? null,
|
||||||
|
title: overrides.title ?? 'Plan',
|
||||||
|
notes: overrides.notes ?? null,
|
||||||
|
city_id: overrides.city_id ?? null,
|
||||||
|
city_name: overrides.city_name ?? null,
|
||||||
|
country: overrides.country ?? null,
|
||||||
|
country_code: overrides.country_code ?? null,
|
||||||
|
start_date: overrides.start_date ?? null,
|
||||||
|
end_date: overrides.end_date ?? null,
|
||||||
|
position: overrides.position ?? 0,
|
||||||
|
created_at: overrides.created_at ?? '2026-01-01T00:00:00Z',
|
||||||
|
updated_at: overrides.updated_at ?? '2026-01-01T00:00:00Z'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTour(overrides: Partial<PackageTour>): PackageTour {
|
||||||
|
return {
|
||||||
|
id: overrides.id ?? 'tour-1',
|
||||||
|
plan_id: overrides.plan_id ?? 'tour-plan-1',
|
||||||
|
operator_name: overrides.operator_name ?? 'Adventure Co',
|
||||||
|
tour_name: overrides.tour_name ?? 'Rainforest',
|
||||||
|
confirmation_number: overrides.confirmation_number ?? null,
|
||||||
|
start_date: overrides.start_date ?? null,
|
||||||
|
start_time: overrides.start_time ?? null,
|
||||||
|
start_timezone: overrides.start_timezone ?? null,
|
||||||
|
end_date: overrides.end_date ?? null,
|
||||||
|
end_time: overrides.end_time ?? null,
|
||||||
|
end_timezone: overrides.end_timezone ?? null,
|
||||||
|
price: overrides.price ?? null,
|
||||||
|
currency: overrides.currency ?? 'USD',
|
||||||
|
created_at: overrides.created_at ?? '2026-01-01T00:00:00Z',
|
||||||
|
updated_at: overrides.updated_at ?? '2026-01-01T00:00:00Z'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTourDay(overrides: Partial<TourDay>): TourDay {
|
||||||
|
return {
|
||||||
|
plan_id: overrides.plan_id ?? 'day-plan-1',
|
||||||
|
day_number: overrides.day_number ?? 1,
|
||||||
|
title: overrides.title ?? null,
|
||||||
|
notes: overrides.notes ?? null,
|
||||||
|
childPlans: overrides.childPlans ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('buildTripTimeline', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
planCounter = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 nextDay = makePlan({ type: 'destination', start_date: '2026-05-02' });
|
||||||
|
|
||||||
|
const timeline = buildTripTimeline({
|
||||||
|
plans: [nextDay, later, early],
|
||||||
|
flightBookings: [],
|
||||||
|
privateVehicles: [],
|
||||||
|
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' }
|
||||||
|
],
|
||||||
|
restaurants: [],
|
||||||
|
packingLists: [],
|
||||||
|
todos: [],
|
||||||
|
packageTours: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(timeline.scheduled).toHaveLength(2);
|
||||||
|
expect(timeline.scheduled[0].date).toBe('2026-05-01');
|
||||||
|
const ids = timeline.scheduled[0].entries
|
||||||
|
.filter((entry) => entry.kind === 'plan')
|
||||||
|
.map((entry) => (entry.kind === 'plan' ? entry.plan.id : ''));
|
||||||
|
expect(ids).toEqual([early.id, later.id]);
|
||||||
|
expect(timeline.scheduled[1].date).toBe('2026-05-02');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps tour day items to tour start date offsets', () => {
|
||||||
|
const tourPlan = makePlan({ id: 'tour-plan-1', type: 'tour' });
|
||||||
|
const day1 = makeTourDay({ plan_id: 'day-1', day_number: 1, title: 'Arrival' });
|
||||||
|
const day2 = makeTourDay({ plan_id: 'day-2', day_number: 2, title: 'Hike' });
|
||||||
|
const activityPlan = makePlan({ id: 'activity-1', type: 'activity', parent_id: day2.plan_id });
|
||||||
|
const tour = makeTour({ plan_id: tourPlan.id, start_date: '2027-01-01' });
|
||||||
|
|
||||||
|
const timeline = buildTripTimeline({
|
||||||
|
plans: [tourPlan, activityPlan],
|
||||||
|
flightBookings: [],
|
||||||
|
privateVehicles: [],
|
||||||
|
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' }
|
||||||
|
],
|
||||||
|
restaurants: [],
|
||||||
|
packingLists: [],
|
||||||
|
todos: [],
|
||||||
|
packageTours: [
|
||||||
|
{ ...tour, days: [day1, day2], ungroupedChildPlans: [], highlight_color: '#FFAA00' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const day2Group = timeline.scheduled.find((group) => group.date === '2027-01-02');
|
||||||
|
expect(day2Group).toBeTruthy();
|
||||||
|
const dayEntries = day2Group?.entries.filter((entry) => entry.kind === 'day') ?? [];
|
||||||
|
expect(dayEntries).toHaveLength(1);
|
||||||
|
const planEntries = day2Group?.entries.filter((entry) => entry.kind === 'plan') ?? [];
|
||||||
|
expect(planEntries).toHaveLength(1);
|
||||||
|
expect(planEntries[0].kind === 'plan' ? planEntries[0].tourDay?.dayNumber : null).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts tour days without a start date into unscheduled', () => {
|
||||||
|
const tourPlan = makePlan({ id: 'tour-plan-2', type: 'tour' });
|
||||||
|
const day1 = makeTourDay({ plan_id: 'day-3', day_number: 1, title: 'Intro' });
|
||||||
|
const activityPlan = makePlan({ id: 'activity-2', type: 'activity', parent_id: day1.plan_id });
|
||||||
|
const tour = makeTour({ id: 'tour-2', plan_id: tourPlan.id, start_date: null });
|
||||||
|
|
||||||
|
const timeline = buildTripTimeline({
|
||||||
|
plans: [tourPlan, activityPlan],
|
||||||
|
flightBookings: [],
|
||||||
|
privateVehicles: [],
|
||||||
|
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' }
|
||||||
|
],
|
||||||
|
restaurants: [],
|
||||||
|
packingLists: [],
|
||||||
|
todos: [],
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
296
src/lib/timeline.ts
Normal file
296
src/lib/timeline.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import type { Plan } from '$lib/server/plans.js';
|
||||||
|
import type { FlightBooking } from '$lib/server/flights.js';
|
||||||
|
import type { PrivateVehicleTransport } from '$lib/server/private-vehicles.js';
|
||||||
|
import type { OtherTransport } from '$lib/server/other-transports.js';
|
||||||
|
import type { Lodging } from '$lib/server/lodgings.js';
|
||||||
|
import type { ExperiencePlan } from '$lib/server/experiences.js';
|
||||||
|
import type { ChecklistWithItems } from '$lib/server/checklists.js';
|
||||||
|
import type { PackageTour, TourDay, ChildPlanSummary } from '$lib/server/package-tours.js';
|
||||||
|
|
||||||
|
export interface TripTimelineInput {
|
||||||
|
plans: Plan[];
|
||||||
|
flightBookings: Array<
|
||||||
|
FlightBooking & {
|
||||||
|
segments: Array<{ departure_date: string; route?: { departure_datetime?: string | null } | null }>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
privateVehicles: PrivateVehicleTransport[];
|
||||||
|
otherTransports: OtherTransport[];
|
||||||
|
lodgings: Array<Lodging & { planStatus?: string }>;
|
||||||
|
activities: ExperiencePlan[];
|
||||||
|
restaurants: ExperiencePlan[];
|
||||||
|
packingLists: ChecklistWithItems[];
|
||||||
|
todos: ChecklistWithItems[];
|
||||||
|
packageTours: Array<
|
||||||
|
PackageTour & {
|
||||||
|
days: TourDay[];
|
||||||
|
ungroupedChildPlans: ChildPlanSummary[];
|
||||||
|
highlight_color: string | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TourContext {
|
||||||
|
planId: string;
|
||||||
|
tourId: string;
|
||||||
|
operatorName: string;
|
||||||
|
tourName: string | null;
|
||||||
|
startDate: string | null;
|
||||||
|
startTime: string | null;
|
||||||
|
highlightColor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TourDayContext {
|
||||||
|
planId: string;
|
||||||
|
dayNumber: number;
|
||||||
|
title: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimelineEntry =
|
||||||
|
| {
|
||||||
|
kind: 'day';
|
||||||
|
id: string;
|
||||||
|
date: string | null;
|
||||||
|
tour: TourContext;
|
||||||
|
day: TourDayContext;
|
||||||
|
sortRank: number;
|
||||||
|
sortTime: string;
|
||||||
|
sequence: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: 'plan';
|
||||||
|
id: string;
|
||||||
|
date: string | null;
|
||||||
|
time: string | null;
|
||||||
|
plan: Plan;
|
||||||
|
tour: TourContext | null;
|
||||||
|
tourDay: TourDayContext | null;
|
||||||
|
sortRank: number;
|
||||||
|
sortTime: string;
|
||||||
|
sequence: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TimelineGroup {
|
||||||
|
date: string;
|
||||||
|
entries: TimelineEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TripTimeline {
|
||||||
|
scheduled: TimelineGroup[];
|
||||||
|
unscheduled: TimelineEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 normalizeTime(value: string | null | undefined): string | null {
|
||||||
|
if (!value) return null;
|
||||||
|
return value.slice(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTripTimeline(input: TripTimelineInput): TripTimeline {
|
||||||
|
const flightByPlanId = new Map(input.flightBookings.map((booking) => [booking.plan_id, booking]));
|
||||||
|
const privateVehicleByPlanId = new Map(
|
||||||
|
input.privateVehicles.map((vehicle) => [vehicle.plan_id, vehicle])
|
||||||
|
);
|
||||||
|
const otherTransportByPlanId = new Map(
|
||||||
|
input.otherTransports.map((transport) => [transport.plan_id, transport])
|
||||||
|
);
|
||||||
|
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])
|
||||||
|
);
|
||||||
|
|
||||||
|
const tourByPlanId = new Map(
|
||||||
|
input.packageTours.map((tour) => [
|
||||||
|
tour.plan_id,
|
||||||
|
{
|
||||||
|
planId: tour.plan_id,
|
||||||
|
tourId: tour.id,
|
||||||
|
operatorName: tour.operator_name,
|
||||||
|
tourName: tour.tour_name ?? null,
|
||||||
|
startDate: tour.start_date ?? null,
|
||||||
|
startTime: tour.start_time ?? null,
|
||||||
|
highlightColor: tour.highlight_color ?? null
|
||||||
|
} satisfies TourContext
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const dayByPlanId = new Map<string, { day: TourDayContext; tour: TourContext }>();
|
||||||
|
for (const tour of input.packageTours) {
|
||||||
|
const tourContext = tourByPlanId.get(tour.plan_id);
|
||||||
|
if (!tourContext) continue;
|
||||||
|
for (const day of tour.days) {
|
||||||
|
dayByPlanId.set(day.plan_id, {
|
||||||
|
day: { planId: day.plan_id, dayNumber: day.day_number, title: day.title ?? null },
|
||||||
|
tour: tourContext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: TimelineEntry[] = [];
|
||||||
|
let sequence = 0;
|
||||||
|
|
||||||
|
for (const plan of input.plans) {
|
||||||
|
if (plan.type === 'day') continue;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let date: string | null = null;
|
||||||
|
let time: string | null = null;
|
||||||
|
|
||||||
|
switch (plan.type) {
|
||||||
|
case 'destination':
|
||||||
|
date = plan.start_date ?? plan.end_date ?? null;
|
||||||
|
break;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const privateVehicle = privateVehicleByPlanId.get(plan.id);
|
||||||
|
if (privateVehicle) {
|
||||||
|
date = privateVehicle.departure_date ?? privateVehicle.arrival_date ?? null;
|
||||||
|
time =
|
||||||
|
normalizeTime(privateVehicle.departure_time) ??
|
||||||
|
normalizeTime(privateVehicle.arrival_time);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'lodging': {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'activity':
|
||||||
|
case 'restaurant': {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'tour': {
|
||||||
|
if (tourContext) {
|
||||||
|
date = tourContext.startDate;
|
||||||
|
time = normalizeTime(tourContext.startTime);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'packing':
|
||||||
|
case 'todo':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayContext?.tour.startDate) {
|
||||||
|
date = addDaysToDate(dayContext.tour.startDate, dayContext.day.dayNumber - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortTime = time ?? '24:00';
|
||||||
|
entries.push({
|
||||||
|
kind: 'plan',
|
||||||
|
id: plan.id,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
plan,
|
||||||
|
tour: tourContext,
|
||||||
|
tourDay: dayContext?.day ?? null,
|
||||||
|
sortRank: 1,
|
||||||
|
sortTime,
|
||||||
|
sequence: sequence++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tour of input.packageTours) {
|
||||||
|
const tourContext = tourByPlanId.get(tour.plan_id);
|
||||||
|
if (!tourContext) continue;
|
||||||
|
for (const day of tour.days) {
|
||||||
|
const dayContext: TourDayContext = {
|
||||||
|
planId: day.plan_id,
|
||||||
|
dayNumber: day.day_number,
|
||||||
|
title: day.title ?? null
|
||||||
|
};
|
||||||
|
const dayDate = tour.start_date
|
||||||
|
? addDaysToDate(tour.start_date, day.day_number - 1)
|
||||||
|
: null;
|
||||||
|
entries.push({
|
||||||
|
kind: 'day',
|
||||||
|
id: `day-${day.plan_id}`,
|
||||||
|
date: dayDate,
|
||||||
|
tour: tourContext,
|
||||||
|
day: dayContext,
|
||||||
|
sortRank: 0,
|
||||||
|
sortTime: '00:00',
|
||||||
|
sequence: sequence++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledEntries = entries.filter((entry) => entry.date);
|
||||||
|
const unscheduledEntries = entries
|
||||||
|
.filter((entry) => !entry.date)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.sortRank !== b.sortRank) return a.sortRank - b.sortRank;
|
||||||
|
if (a.sortTime !== b.sortTime) return a.sortTime.localeCompare(b.sortTime);
|
||||||
|
return a.sequence - b.sequence;
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduledEntries.sort((a, b) => {
|
||||||
|
if (a.date !== b.date) return (a.date ?? '').localeCompare(b.date ?? '');
|
||||||
|
if (a.sortRank !== b.sortRank) return a.sortRank - b.sortRank;
|
||||||
|
if (a.sortTime !== b.sortTime) return a.sortTime.localeCompare(b.sortTime);
|
||||||
|
return a.sequence - b.sequence;
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped = new Map<string, TimelineEntry[]>();
|
||||||
|
for (const entry of scheduledEntries) {
|
||||||
|
const date = entry.date as string;
|
||||||
|
const list = grouped.get(date);
|
||||||
|
if (list) list.push(entry);
|
||||||
|
else grouped.set(date, [entry]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduled: TimelineGroup[] = Array.from(grouped.entries())
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
.map(([date, groupEntries]) => ({
|
||||||
|
date,
|
||||||
|
entries: groupEntries
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { scheduled, unscheduled: unscheduledEntries };
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||||
import PackageTourCard from '$lib/components/PackageTourCard.svelte';
|
import PackageTourCard from '$lib/components/PackageTourCard.svelte';
|
||||||
import AddPlanMenu, { type AddPlanMenuItem } from '$lib/components/AddPlanMenu.svelte';
|
import AddPlanMenu, { type AddPlanMenuItem } from '$lib/components/AddPlanMenu.svelte';
|
||||||
|
import TourBadge from '$lib/components/TourBadge.svelte';
|
||||||
|
import TourTimelineCard from '$lib/components/TourTimelineCard.svelte';
|
||||||
import { PLAN_TYPE_DEFINITIONS, type PlanTypeId } from '$lib/components/plan-types.js';
|
import { PLAN_TYPE_DEFINITIONS, type PlanTypeId } from '$lib/components/plan-types.js';
|
||||||
import PlanCard from '$lib/components/PlanCard.svelte';
|
import PlanCard from '$lib/components/PlanCard.svelte';
|
||||||
import FlightCard from '$lib/components/FlightCard.svelte';
|
import FlightCard from '$lib/components/FlightCard.svelte';
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
import PrivateVehicleCard from '$lib/components/PrivateVehicleCard.svelte';
|
import PrivateVehicleCard from '$lib/components/PrivateVehicleCard.svelte';
|
||||||
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
||||||
import TravellerChip from '$lib/components/TravellerChip.svelte';
|
import TravellerChip from '$lib/components/TravellerChip.svelte';
|
||||||
|
import { buildTripTimeline } from '$lib/timeline.js';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
let trip = $derived(form?.trip ?? data.trip);
|
let trip = $derived(form?.trip ?? data.trip);
|
||||||
@@ -89,6 +92,12 @@
|
|||||||
let togglingChecklistItemId = $state<string | null>(null);
|
let togglingChecklistItemId = $state<string | null>(null);
|
||||||
let togglingChecklistItemChecked = $state<'0' | '1'>('0');
|
let togglingChecklistItemChecked = $state<'0' | '1'>('0');
|
||||||
let addingChildToPlanId = $state<string | null>(null);
|
let addingChildToPlanId = $state<string | null>(null);
|
||||||
|
let planView = $state<'types' | 'timeline'>('types');
|
||||||
|
|
||||||
|
const planViewOptions = [
|
||||||
|
{ id: 'types', label: 'By type' },
|
||||||
|
{ id: 'timeline', label: 'Timeline' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
let transportationLocationOptions = $derived(
|
let transportationLocationOptions = $derived(
|
||||||
plans
|
plans
|
||||||
@@ -132,11 +141,50 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTimelineDate(d: string) {
|
||||||
|
return new Date(d + 'T00:00:00').toLocaleDateString(undefined, {
|
||||||
|
weekday: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimelineTime(t: string | null) {
|
||||||
|
if (!t) return 'All day';
|
||||||
|
return t.slice(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
function toggleChecklistItem(itemId: string | number, nextChecked: boolean) {
|
function toggleChecklistItem(itemId: string | number, nextChecked: boolean) {
|
||||||
togglingChecklistItemId = String(itemId);
|
togglingChecklistItemId = String(itemId);
|
||||||
togglingChecklistItemChecked = nextChecked ? '1' : '0';
|
togglingChecklistItemChecked = nextChecked ? '1' : '0';
|
||||||
setTimeout(() => checklistToggleFormRef?.requestSubmit(), 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 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 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 timeline = $derived(
|
||||||
|
buildTripTimeline({
|
||||||
|
plans,
|
||||||
|
flightBookings,
|
||||||
|
privateVehicles,
|
||||||
|
otherTransports,
|
||||||
|
lodgings,
|
||||||
|
activities,
|
||||||
|
restaurants,
|
||||||
|
packingLists,
|
||||||
|
todos,
|
||||||
|
packageTours
|
||||||
|
})
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -542,6 +590,652 @@
|
|||||||
onAddPackingList={() => (showAddPackingList = true)}
|
onAddPackingList={() => (showAddPackingList = true)}
|
||||||
onAddTodo={() => (showAddTodo = true)}
|
onAddTodo={() => (showAddTodo = true)}
|
||||||
/>
|
/>
|
||||||
|
{: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">
|
||||||
|
{#each planViewOptions as option}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (planView = option.id)}
|
||||||
|
class="rounded-md px-2.5 py-1 transition {planView === option.id
|
||||||
|
? 'bg-gray-900 text-white'
|
||||||
|
: 'hover:bg-gray-100'}"
|
||||||
|
aria-pressed={planView === option.id}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if planView === 'timeline'}
|
||||||
|
<div class="mt-6">
|
||||||
|
{#if timeline.scheduled.length === 0}
|
||||||
|
<p class="text-sm text-gray-500">No scheduled plans yet.</p>
|
||||||
|
{:else}
|
||||||
|
{#each timeline.scheduled as group (group.date)}
|
||||||
|
<div class="mt-6 first:mt-0">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<p class="text-sm font-semibold text-gray-700">
|
||||||
|
{formatTimelineDate(group.date)}
|
||||||
|
</p>
|
||||||
|
<div class="h-px flex-1 bg-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex flex-col gap-4">
|
||||||
|
{#each group.entries as entry (entry.id)}
|
||||||
|
{#if entry.kind === 'day'}
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2"
|
||||||
|
style="border-left: 4px solid {entry.tour.highlightColor ?? '#E5E7EB'}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<TourBadge
|
||||||
|
operatorName={entry.tour.operatorName}
|
||||||
|
tourName={entry.tour.tourName}
|
||||||
|
highlightColor={entry.tour.highlightColor}
|
||||||
|
size="sm"
|
||||||
|
showName={false}
|
||||||
|
/>
|
||||||
|
<p class="text-sm font-semibold text-gray-800">
|
||||||
|
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}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{@const plan = entry.plan}
|
||||||
|
{@const timeLabel = formatTimelineTime(entry.time)}
|
||||||
|
{@const flightBooking = flightBookingByPlanId.get(plan.id)}
|
||||||
|
{@const privateVehicle = privateVehicleByPlanId.get(plan.id)}
|
||||||
|
{@const otherTransport = otherTransportByPlanId.get(plan.id)}
|
||||||
|
{@const lodging = lodgingByPlanId.get(plan.id)}
|
||||||
|
{@const activity = activityByPlanId.get(plan.id)}
|
||||||
|
{@const restaurant = restaurantByPlanId.get(plan.id)}
|
||||||
|
{@const packingList = packingListByPlanId.get(plan.id)}
|
||||||
|
{@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">
|
||||||
|
{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">
|
||||||
|
<TourBadge
|
||||||
|
operatorName={entry.tour.operatorName}
|
||||||
|
tourName={entry.tour.tourName}
|
||||||
|
highlightColor={entry.tour.highlightColor}
|
||||||
|
size="sm"
|
||||||
|
showName={false}
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-gray-600">
|
||||||
|
{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}` : ''}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="{entry.tour && plan.type !== 'tour' ? 'border-l-4 pl-3' : ''}"
|
||||||
|
style="border-left-color: {entry.tour?.highlightColor ?? '#E5E7EB'}"
|
||||||
|
>
|
||||||
|
{#if plan.type === 'destination'}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<PlanCard {plan} onDelete={submitForm} />
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'transport'}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
{#if flightBooking}
|
||||||
|
<FlightCard
|
||||||
|
{plan}
|
||||||
|
{flightBooking}
|
||||||
|
onEdit={() =>
|
||||||
|
(editingTransportation = {
|
||||||
|
type: 'flight',
|
||||||
|
flightBooking,
|
||||||
|
planStatus: plan.status
|
||||||
|
})}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
{:else if privateVehicle}
|
||||||
|
<PrivateVehicleCard
|
||||||
|
{plan}
|
||||||
|
{privateVehicle}
|
||||||
|
onEdit={() =>
|
||||||
|
(editingTransportation = {
|
||||||
|
type: 'private_vehicle',
|
||||||
|
privateVehicle,
|
||||||
|
planStatus: plan.status
|
||||||
|
})}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
{:else if otherTransport}
|
||||||
|
<OtherTransportCard
|
||||||
|
{plan}
|
||||||
|
{otherTransport}
|
||||||
|
onEdit={() =>
|
||||||
|
(editingTransportation = {
|
||||||
|
type: 'other',
|
||||||
|
otherTransport,
|
||||||
|
planStatus: plan.status,
|
||||||
|
planTitle: plan.title,
|
||||||
|
planNotes: plan.notes
|
||||||
|
})}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'activity' && activity}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ExperienceCard
|
||||||
|
{plan}
|
||||||
|
experience={activity}
|
||||||
|
onEdit={() => (editingActivity = { plan, experience: activity })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'restaurant' && restaurant}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ExperienceCard
|
||||||
|
{plan}
|
||||||
|
experience={restaurant}
|
||||||
|
onEdit={() => (editingRestaurant = { plan, experience: restaurant })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'lodging' && lodging}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<LodgingCard
|
||||||
|
{plan}
|
||||||
|
{lodging}
|
||||||
|
onEdit={() => (editingLodging = lodging)}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'packing' && packingList}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ChecklistCard
|
||||||
|
{plan}
|
||||||
|
items={packingList.items}
|
||||||
|
onEdit={() => (editingPackingList = { plan, list: packingList })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
onToggleItem={toggleChecklistItem}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'todo' && todoList}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ChecklistCard
|
||||||
|
{plan}
|
||||||
|
items={todoList.items}
|
||||||
|
onEdit={() => (editingTodo = { plan, list: todoList })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
onToggleItem={toggleChecklistItem}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'tour' && tour}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<TourTimelineCard
|
||||||
|
{plan}
|
||||||
|
{tour}
|
||||||
|
onEdit={() => (editingTour = tour)}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if timeline.unscheduled.length > 0}
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<p class="text-sm font-semibold text-gray-700">Unscheduled</p>
|
||||||
|
<div class="h-px flex-1 bg-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex flex-col gap-4">
|
||||||
|
{#each timeline.unscheduled as entry (entry.id)}
|
||||||
|
{#if entry.kind === 'day'}
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2"
|
||||||
|
style="border-left: 4px solid {entry.tour.highlightColor ?? '#E5E7EB'}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<TourBadge
|
||||||
|
operatorName={entry.tour.operatorName}
|
||||||
|
tourName={entry.tour.tourName}
|
||||||
|
highlightColor={entry.tour.highlightColor}
|
||||||
|
size="sm"
|
||||||
|
showName={false}
|
||||||
|
/>
|
||||||
|
<p class="text-sm font-semibold text-gray-800">
|
||||||
|
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}` : ''}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-400">Date TBD</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{@const plan = entry.plan}
|
||||||
|
{@const timeLabel = entry.time ? formatTimelineTime(entry.time) : 'TBD'}
|
||||||
|
{@const flightBooking = flightBookingByPlanId.get(plan.id)}
|
||||||
|
{@const privateVehicle = privateVehicleByPlanId.get(plan.id)}
|
||||||
|
{@const otherTransport = otherTransportByPlanId.get(plan.id)}
|
||||||
|
{@const lodging = lodgingByPlanId.get(plan.id)}
|
||||||
|
{@const activity = activityByPlanId.get(plan.id)}
|
||||||
|
{@const restaurant = restaurantByPlanId.get(plan.id)}
|
||||||
|
{@const packingList = packingListByPlanId.get(plan.id)}
|
||||||
|
{@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">
|
||||||
|
{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">
|
||||||
|
<TourBadge
|
||||||
|
operatorName={entry.tour.operatorName}
|
||||||
|
tourName={entry.tour.tourName}
|
||||||
|
highlightColor={entry.tour.highlightColor}
|
||||||
|
size="sm"
|
||||||
|
showName={false}
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-gray-600">
|
||||||
|
{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}` : ''}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="{entry.tour && plan.type !== 'tour' ? 'border-l-4 pl-3' : ''}"
|
||||||
|
style="border-left-color: {entry.tour?.highlightColor ?? '#E5E7EB'}"
|
||||||
|
>
|
||||||
|
{#if plan.type === 'destination'}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<PlanCard {plan} onDelete={submitForm} />
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'transport'}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
{#if flightBooking}
|
||||||
|
<FlightCard
|
||||||
|
{plan}
|
||||||
|
{flightBooking}
|
||||||
|
onEdit={() =>
|
||||||
|
(editingTransportation = {
|
||||||
|
type: 'flight',
|
||||||
|
flightBooking,
|
||||||
|
planStatus: plan.status
|
||||||
|
})}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
{:else if privateVehicle}
|
||||||
|
<PrivateVehicleCard
|
||||||
|
{plan}
|
||||||
|
{privateVehicle}
|
||||||
|
onEdit={() =>
|
||||||
|
(editingTransportation = {
|
||||||
|
type: 'private_vehicle',
|
||||||
|
privateVehicle,
|
||||||
|
planStatus: plan.status
|
||||||
|
})}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
{:else if otherTransport}
|
||||||
|
<OtherTransportCard
|
||||||
|
{plan}
|
||||||
|
{otherTransport}
|
||||||
|
onEdit={() =>
|
||||||
|
(editingTransportation = {
|
||||||
|
type: 'other',
|
||||||
|
otherTransport,
|
||||||
|
planStatus: plan.status,
|
||||||
|
planTitle: plan.title,
|
||||||
|
planNotes: plan.notes
|
||||||
|
})}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'activity' && activity}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ExperienceCard
|
||||||
|
{plan}
|
||||||
|
experience={activity}
|
||||||
|
onEdit={() => (editingActivity = { plan, experience: activity })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'restaurant' && restaurant}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ExperienceCard
|
||||||
|
{plan}
|
||||||
|
experience={restaurant}
|
||||||
|
onEdit={() => (editingRestaurant = { plan, experience: restaurant })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'lodging' && lodging}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<LodgingCard
|
||||||
|
{plan}
|
||||||
|
{lodging}
|
||||||
|
onEdit={() => (editingLodging = lodging)}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'packing' && packingList}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ChecklistCard
|
||||||
|
{plan}
|
||||||
|
items={packingList.items}
|
||||||
|
onEdit={() => (editingPackingList = { plan, list: packingList })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
onToggleItem={toggleChecklistItem}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'todo' && todoList}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<ChecklistCard
|
||||||
|
{plan}
|
||||||
|
items={todoList.items}
|
||||||
|
onEdit={() => (editingTodo = { plan, list: todoList })}
|
||||||
|
onDelete={submitForm}
|
||||||
|
onToggleItem={toggleChecklistItem}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else if plan.type === 'tour' && tour}
|
||||||
|
{@const formId = `remove-plan-timeline-${plan.id}`}
|
||||||
|
{@const submitForm = () => {
|
||||||
|
const form = document.getElementById(formId) as HTMLFormElement;
|
||||||
|
form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
method="POST"
|
||||||
|
action="?/removePlan"
|
||||||
|
use:enhance={() => {
|
||||||
|
return ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="plan_id" value={plan.id} />
|
||||||
|
<TourTimelineCard
|
||||||
|
{plan}
|
||||||
|
{tour}
|
||||||
|
onEdit={() => (editingTour = tour)}
|
||||||
|
onDelete={submitForm}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Plans list -->
|
<!-- Plans list -->
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
@@ -905,4 +1599,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user