trip) add day/week trip views #52
@@ -13,7 +13,7 @@ LOCAL_AUTH_ARGON2_PARALLELISM=1
|
|||||||
ADMIN_USER_IDS=e2e_admin
|
ADMIN_USER_IDS=e2e_admin
|
||||||
|
|
||||||
# Separate test database — never touches trips.db
|
# Separate test database — never touches trips.db
|
||||||
DATABASE_URL=file:trips.test.db
|
DATABASE_URL=file:/tmp/trips.test.db
|
||||||
|
|
||||||
# Synology OIDC — not exercised in E2E tests but must be present to avoid startup errors
|
# Synology OIDC — not exercised in E2E tests but must be present to avoid startup errors
|
||||||
SYNOLOGY_ISSUER=https://cloud.campbellwireless.net/auth/webman/sso
|
SYNOLOGY_ISSUER=https://cloud.campbellwireless.net/auth/webman/sso
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function uniqueSuffix(): string {
|
|||||||
|
|
||||||
export async function createTrip(
|
export async function createTrip(
|
||||||
page: Page,
|
page: Page,
|
||||||
values: { name: string; startDate?: string; description?: string }
|
values: { name: string; startDate?: string; endDate?: string; description?: string }
|
||||||
): Promise<{ tripUrl: string; tripId: string }> {
|
): Promise<{ tripUrl: string; tripId: string }> {
|
||||||
await page.goto(NEW_TRIP_URL);
|
await page.goto(NEW_TRIP_URL);
|
||||||
await expect(page.getByRole('heading', { name: 'Plan New Trip' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Plan New Trip' })).toBeVisible();
|
||||||
@@ -22,6 +22,9 @@ export async function createTrip(
|
|||||||
} else {
|
} else {
|
||||||
await form.getByRole('checkbox', { name: "I don't know yet" }).first().check();
|
await form.getByRole('checkbox', { name: "I don't know yet" }).first().check();
|
||||||
}
|
}
|
||||||
|
if (values.endDate) {
|
||||||
|
await form.getByLabel('End date').fill(values.endDate);
|
||||||
|
}
|
||||||
if (values.description) {
|
if (values.description) {
|
||||||
await form.getByLabel('Description').fill(values.description);
|
await form.getByLabel('Description').fill(values.description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,17 @@ import { TEST_USERS } from './test-users.js';
|
|||||||
|
|
||||||
dotenvConfig({ path: resolve(process.cwd(), '.env.test'), override: true });
|
dotenvConfig({ path: resolve(process.cwd(), '.env.test'), override: true });
|
||||||
|
|
||||||
const DB_PATH = resolve(process.cwd(), 'trips.test.db');
|
function resolveDatabasePath(): string {
|
||||||
|
const rawUrl = process.env.DATABASE_URL;
|
||||||
|
if (rawUrl && rawUrl.startsWith('file:')) {
|
||||||
|
const rawPath = rawUrl.slice('file:'.length);
|
||||||
|
if (!rawPath) return resolve(process.cwd(), 'trips.test.db');
|
||||||
|
return rawPath.startsWith('/') ? rawPath : resolve(process.cwd(), rawPath);
|
||||||
|
}
|
||||||
|
return resolve(process.cwd(), 'trips.test.db');
|
||||||
|
}
|
||||||
|
|
||||||
|
const DB_PATH = resolveDatabasePath();
|
||||||
|
|
||||||
// Delete any existing test DB so we start clean each run
|
// Delete any existing test DB so we start clean each run
|
||||||
for (const ext of ['', '-shm', '-wal']) {
|
for (const ext of ['', '-shm', '-wal']) {
|
||||||
@@ -20,6 +30,7 @@ for (const ext of ['', '-shm', '-wal']) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[e2e] Using test database at ${DB_PATH}`);
|
||||||
const db = new BunSqlite(DB_PATH);
|
const db = new BunSqlite(DB_PATH);
|
||||||
db.exec('PRAGMA foreign_keys = ON');
|
db.exec('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
@@ -40,6 +51,7 @@ const dbWrapper = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[e2e] Running migrations...');
|
||||||
const { runMigrations } = await import('../../src/lib/server/db/migrations.js');
|
const { runMigrations } = await import('../../src/lib/server/db/migrations.js');
|
||||||
runMigrations(dbWrapper);
|
runMigrations(dbWrapper);
|
||||||
console.log('[e2e] Migrations complete.');
|
console.log('[e2e] Migrations complete.');
|
||||||
@@ -53,6 +65,7 @@ async function hashPassword(password: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[e2e] Creating test users...');
|
||||||
const regularId = randomUUID();
|
const regularId = randomUUID();
|
||||||
dbWrapper.run(
|
dbWrapper.run(
|
||||||
`INSERT INTO users (id, username, full_name, email, auth_source) VALUES (?, ?, ?, ?, ?)`,
|
`INSERT INTO users (id, username, full_name, email, auth_source) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
|||||||
70
e2e/trip-date-view.test.ts
Normal file
70
e2e/trip-date-view.test.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { TEST_USERS } from './setup/test-users.js';
|
||||||
|
import { loginAsLocalUser, ensureSelfProfile } from './helpers/auth.js';
|
||||||
|
import { 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 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', { exact: true }).fill(values.startTime);
|
||||||
|
await dialog.getByRole('button', { name: 'Add' }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ context }) => {
|
||||||
|
await context.clearCookies();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('day and week views filter itinerary items', async ({ page }) => {
|
||||||
|
const suffix = uniqueSuffix();
|
||||||
|
const startDate = formatDate(0);
|
||||||
|
const endDate = formatDate(9);
|
||||||
|
const dayOneActivity = `Museum visit ${suffix}`;
|
||||||
|
const dayEightActivity = `Harbor walk ${suffix}`;
|
||||||
|
|
||||||
|
await loginAsLocalUser(page, TEST_USERS.regular.username, TEST_USERS.regular.password);
|
||||||
|
await ensureSelfProfile(page);
|
||||||
|
|
||||||
|
await createTrip(page, {
|
||||||
|
name: `E2E Date Views Trip ${suffix}`,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
description: 'Date view coverage'
|
||||||
|
});
|
||||||
|
|
||||||
|
await addActivity(page, { name: dayOneActivity, startDate, startTime: '09:00' });
|
||||||
|
await addActivity(page, { name: dayEightActivity, startDate: formatDate(7), startTime: '11:00' });
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.getByText(dayOneActivity)).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Day' }).click();
|
||||||
|
await expect(page.getByText('Day 1')).toBeVisible();
|
||||||
|
await expect(page.getByText(dayOneActivity)).toBeVisible();
|
||||||
|
await expect(page.getByText(dayEightActivity)).toBeHidden();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Week' }).click();
|
||||||
|
await expect(page.getByText('Week 1')).toBeVisible();
|
||||||
|
await expect(page.getByText(dayOneActivity)).toBeVisible();
|
||||||
|
await expect(page.getByText(dayEightActivity)).toBeHidden();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Next week' }).click();
|
||||||
|
await expect(page.getByText('Week 2')).toBeVisible();
|
||||||
|
await expect(page.getByText(dayEightActivity)).toBeVisible();
|
||||||
|
await expect(page.getByText(dayOneActivity)).toBeHidden();
|
||||||
|
});
|
||||||
@@ -5,6 +5,11 @@ import { resolve } from 'path';
|
|||||||
// Load .env.test so both this process and the webServer child process see the values.
|
// Load .env.test so both this process and the webServer child process see the values.
|
||||||
dotenvConfig({ path: resolve(process.cwd(), '.env.test'), override: true });
|
dotenvConfig({ path: resolve(process.cwd(), '.env.test'), override: true });
|
||||||
|
|
||||||
|
const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_PATH;
|
||||||
|
const launchOptions = chromiumExecutablePath
|
||||||
|
? { executablePath: chromiumExecutablePath }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
@@ -16,7 +21,8 @@ export default defineConfig({
|
|||||||
use: {
|
use: {
|
||||||
baseURL: 'http://127.0.0.1:5173',
|
baseURL: 'http://127.0.0.1:5173',
|
||||||
trace: process.env.PW_TRACE_MODE ?? 'on-first-retry',
|
trace: process.env.PW_TRACE_MODE ?? 'on-first-retry',
|
||||||
video: process.env.PW_VIDEO_MODE ?? 'off'
|
video: process.env.PW_VIDEO_MODE ?? 'off',
|
||||||
|
...(launchOptions ? { launchOptions } : {})
|
||||||
},
|
},
|
||||||
|
|
||||||
projects: [
|
projects: [
|
||||||
|
|||||||
34
src/lib/date-views.test.ts
Normal file
34
src/lib/date-views.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { addDaysToDate, buildDateSequence, buildWeekRanges } from './date-views.js';
|
||||||
|
|
||||||
|
describe('date-views', () => {
|
||||||
|
it('adds days to a date string', () => {
|
||||||
|
expect(addDaysToDate('2026-04-01', 1)).toBe('2026-04-02');
|
||||||
|
expect(addDaysToDate('2026-04-01', 7)).toBe('2026-04-08');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds an inclusive date sequence', () => {
|
||||||
|
const dates = buildDateSequence('2026-04-01', '2026-04-03');
|
||||||
|
expect(dates).toEqual(['2026-04-01', '2026-04-02', '2026-04-03']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty sequence when end is before start', () => {
|
||||||
|
expect(buildDateSequence('2026-04-03', '2026-04-01')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chunks weeks into seven-day ranges', () => {
|
||||||
|
const dates = buildDateSequence('2026-04-01', '2026-04-10');
|
||||||
|
const ranges = buildWeekRanges(dates);
|
||||||
|
expect(ranges).toHaveLength(2);
|
||||||
|
expect(ranges[0]).toEqual({
|
||||||
|
start: '2026-04-01',
|
||||||
|
end: '2026-04-07',
|
||||||
|
weekNumber: 1
|
||||||
|
});
|
||||||
|
expect(ranges[1]).toEqual({
|
||||||
|
start: '2026-04-08',
|
||||||
|
end: '2026-04-10',
|
||||||
|
weekNumber: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/lib/date-views.ts
Normal file
33
src/lib/date-views.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDateSequence(start: string, end: string): string[] {
|
||||||
|
if (!start || !end) return [];
|
||||||
|
if (start > end) return [];
|
||||||
|
const dates: string[] = [];
|
||||||
|
let current = start;
|
||||||
|
while (current <= end) {
|
||||||
|
dates.push(current);
|
||||||
|
current = addDaysToDate(current, 1);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWeekRanges(
|
||||||
|
dates: string[]
|
||||||
|
): Array<{ start: string; end: string; weekNumber: number }> {
|
||||||
|
const ranges: Array<{ start: string; end: string; weekNumber: number }> = [];
|
||||||
|
for (let i = 0; i < dates.length; i += 7) {
|
||||||
|
const slice = dates.slice(i, i + 7);
|
||||||
|
if (slice.length === 0) continue;
|
||||||
|
ranges.push({
|
||||||
|
start: slice[0],
|
||||||
|
end: slice[slice.length - 1],
|
||||||
|
weekNumber: Math.floor(i / 7) + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
@@ -600,14 +600,21 @@ function seedCities(db: Database): void {
|
|||||||
const cities: { name: string; country: string; country_code: string; population: number }[] =
|
const cities: { name: string; country: string; country_code: string; population: number }[] =
|
||||||
JSON.parse(data);
|
JSON.parse(data);
|
||||||
let id = 1;
|
let id = 1;
|
||||||
|
db.run('BEGIN');
|
||||||
for (const city of cities) {
|
for (const city of cities) {
|
||||||
db.run(
|
db.run(
|
||||||
'INSERT OR IGNORE INTO cities (id, name, country, country_code, population) VALUES (?, ?, ?, ?, ?)',
|
'INSERT OR IGNORE INTO cities (id, name, country, country_code, population) VALUES (?, ?, ?, ?, ?)',
|
||||||
[id++, city.name, city.country, city.country_code, city.population]
|
[id++, city.name, city.country, city.country_code, city.population]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
db.run('COMMIT');
|
||||||
console.log(`[db] Seeded ${cities.length} cities`);
|
console.log(`[db] Seeded ${cities.length} cities`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* ignore rollback failure */
|
||||||
|
}
|
||||||
console.error('[db] Failed to seed cities:', e);
|
console.error('[db] Failed to seed cities:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,6 +629,7 @@ function seedAirports(db: Database): void {
|
|||||||
const lines = data.split('\n').filter((line) => line.trim());
|
const lines = data.split('\n').filter((line) => line.trim());
|
||||||
let imported = 0;
|
let imported = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
db.run('BEGIN');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// Skip comments
|
// Skip comments
|
||||||
@@ -676,9 +684,15 @@ function seedAirports(db: Database): void {
|
|||||||
skipped++;
|
skipped++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
db.run('COMMIT');
|
||||||
console.log(`[db] Seeded ${imported} airports from airports.dat (${skipped} skipped)`);
|
console.log(`[db] Seeded ${imported} airports from airports.dat (${skipped} skipped)`);
|
||||||
return;
|
return;
|
||||||
} catch (_fileError) {
|
} catch (_fileError) {
|
||||||
|
try {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* ignore rollback failure */
|
||||||
|
}
|
||||||
// File doesn't exist, fall back to minimal seed only if table is empty
|
// File doesn't exist, fall back to minimal seed only if table is empty
|
||||||
const airportsRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airports');
|
const airportsRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airports');
|
||||||
if ((airportsRow?.count ?? 0) === 0) {
|
if ((airportsRow?.count ?? 0) === 0) {
|
||||||
@@ -782,6 +796,7 @@ function seedAirports(db: Database): void {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let id = 1;
|
let id = 1;
|
||||||
|
db.run('BEGIN');
|
||||||
for (const airport of majorAirports) {
|
for (const airport of majorAirports) {
|
||||||
db.run(
|
db.run(
|
||||||
`INSERT OR IGNORE INTO airports (id, iata_code, icao_code, name, city, country, country_code, latitude, longitude, timezone)
|
`INSERT OR IGNORE INTO airports (id, iata_code, icao_code, name, city, country, country_code, latitude, longitude, timezone)
|
||||||
@@ -800,8 +815,14 @@ function seedAirports(db: Database): void {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
db.run('COMMIT');
|
||||||
console.log(`[db] Seeded ${majorAirports.length} airports`);
|
console.log(`[db] Seeded ${majorAirports.length} airports`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* ignore rollback failure */
|
||||||
|
}
|
||||||
console.error('[db] Failed to seed airports:', e);
|
console.error('[db] Failed to seed airports:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -816,6 +837,7 @@ function seedAirlines(db: Database): void {
|
|||||||
const lines = data.split('\n').filter((line) => line.trim());
|
const lines = data.split('\n').filter((line) => line.trim());
|
||||||
let imported = 0;
|
let imported = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
db.run('BEGIN');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// Skip comments
|
// Skip comments
|
||||||
@@ -857,9 +879,15 @@ function seedAirlines(db: Database): void {
|
|||||||
skipped++;
|
skipped++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
db.run('COMMIT');
|
||||||
console.log(`[db] Seeded ${imported} airlines from airlines.dat (${skipped} skipped)`);
|
console.log(`[db] Seeded ${imported} airlines from airlines.dat (${skipped} skipped)`);
|
||||||
return;
|
return;
|
||||||
} catch (_fileError) {
|
} catch (_fileError) {
|
||||||
|
try {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* ignore rollback failure */
|
||||||
|
}
|
||||||
// File doesn't exist, fall back to minimal seed only if table is empty
|
// File doesn't exist, fall back to minimal seed only if table is empty
|
||||||
const airlinesRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airlines');
|
const airlinesRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airlines');
|
||||||
if ((airlinesRow?.count ?? 0) === 0) {
|
if ((airlinesRow?.count ?? 0) === 0) {
|
||||||
@@ -921,6 +949,7 @@ function seedAirlines(db: Database): void {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let id = 1;
|
let id = 1;
|
||||||
|
db.run('BEGIN');
|
||||||
for (const airline of majorAirlines) {
|
for (const airline of majorAirlines) {
|
||||||
db.run(
|
db.run(
|
||||||
`INSERT OR IGNORE INTO airlines (id, iata_code, icao_code, name, country, country_code)
|
`INSERT OR IGNORE INTO airlines (id, iata_code, icao_code, name, country, country_code)
|
||||||
@@ -928,8 +957,14 @@ function seedAirlines(db: Database): void {
|
|||||||
[id++, airline.iata, airline.icao, airline.name, airline.country, airline.country_code]
|
[id++, airline.iata, airline.icao, airline.name, airline.country, airline.country_code]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
db.run('COMMIT');
|
||||||
console.log(`[db] Seeded ${majorAirlines.length} airlines`);
|
console.log(`[db] Seeded ${majorAirlines.length} airlines`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* ignore rollback failure */
|
||||||
|
}
|
||||||
console.error('[db] Failed to seed airlines:', e);
|
console.error('[db] Failed to seed airlines:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,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 { buildDateSequence, buildWeekRanges, addDaysToDate } from '$lib/date-views.js';
|
||||||
import { buildTripTimeline } from '$lib/timeline.js';
|
import { buildTripTimeline } from '$lib/timeline.js';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
@@ -93,12 +94,21 @@
|
|||||||
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');
|
let planView = $state<'types' | 'timeline'>('types');
|
||||||
|
let scheduleView = $state<'trip' | 'day' | 'week'>('trip');
|
||||||
|
let dayIndex = $state(0);
|
||||||
|
let weekIndex = $state(0);
|
||||||
|
|
||||||
const planViewOptions = [
|
const planViewOptions = [
|
||||||
{ id: 'types', label: 'By type' },
|
{ id: 'types', label: 'By type' },
|
||||||
{ id: 'timeline', label: 'Timeline' }
|
{ id: 'timeline', label: 'Timeline' }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const scheduleViewOptions = [
|
||||||
|
{ id: 'trip', label: 'Trip' },
|
||||||
|
{ id: 'day', label: 'Day' },
|
||||||
|
{ id: 'week', label: 'Week' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
let transportationLocationOptions = $derived(
|
let transportationLocationOptions = $derived(
|
||||||
plans
|
plans
|
||||||
.filter((plan) => plan.type !== 'transport' && plan.type !== 'day')
|
.filter((plan) => plan.type !== 'transport' && plan.type !== 'day')
|
||||||
@@ -155,10 +165,27 @@
|
|||||||
return t.slice(0, 5);
|
return t.slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDaysToDate(date: string, offset: number): string {
|
function toLocalDateString(date: Date): string {
|
||||||
const [year, month, day] = date.split('-').map(Number);
|
const year = date.getFullYear();
|
||||||
const ts = Date.UTC(year, month - 1, day + offset);
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
return new Date(ts).toISOString().slice(0, 10);
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLongDate(d: string) {
|
||||||
|
return new Date(d + 'T00:00:00').toLocaleDateString(undefined, {
|
||||||
|
weekday: 'long',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRangeDate(d: string) {
|
||||||
|
return new Date(d + 'T00:00:00').toLocaleDateString(undefined, {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleChecklistItem(itemId: string | number, nextChecked: boolean) {
|
function toggleChecklistItem(itemId: string | number, nextChecked: boolean) {
|
||||||
@@ -253,6 +280,132 @@
|
|||||||
packageTours
|
packageTours
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tripDateRange = $derived(
|
||||||
|
trip.start_date && trip.end_date
|
||||||
|
? { start: trip.start_date, end: trip.end_date }
|
||||||
|
: timeline.scheduled.length === 0
|
||||||
|
? null
|
||||||
|
: (() => {
|
||||||
|
const dates = timeline.scheduled.map((group) => group.date).sort();
|
||||||
|
return { start: dates[0], end: dates[dates.length - 1] };
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
const tripDates = $derived(
|
||||||
|
tripDateRange ? buildDateSequence(tripDateRange.start, tripDateRange.end) : []
|
||||||
|
);
|
||||||
|
|
||||||
|
const weekRanges = $derived(buildWeekRanges(tripDates));
|
||||||
|
|
||||||
|
const currentRange = $derived(
|
||||||
|
scheduleView === 'day'
|
||||||
|
? tripDates[dayIndex]
|
||||||
|
? { start: tripDates[dayIndex], end: tripDates[dayIndex] }
|
||||||
|
: null
|
||||||
|
: scheduleView === 'week'
|
||||||
|
? weekRanges[weekIndex] ?? null
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredTimeline = $derived(
|
||||||
|
currentRange
|
||||||
|
? {
|
||||||
|
scheduled: timeline.scheduled.filter(
|
||||||
|
(group) => group.date >= currentRange.start && group.date <= currentRange.end
|
||||||
|
),
|
||||||
|
unscheduled: []
|
||||||
|
}
|
||||||
|
: timeline
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredPlanIds = $derived(
|
||||||
|
currentRange
|
||||||
|
? (() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const group of timeline.scheduled) {
|
||||||
|
if (group.date < currentRange.start || group.date > currentRange.end) continue;
|
||||||
|
for (const entry of group.entries) {
|
||||||
|
if (entry.kind === 'plan') ids.add(entry.plan.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
})()
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const visiblePlans = $derived(
|
||||||
|
filteredPlanIds ? plans.filter((plan) => filteredPlanIds.has(plan.id)) : plans
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleFlightBookings = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? flightBookings.filter((booking) => filteredPlanIds.has(booking.plan_id))
|
||||||
|
: flightBookings
|
||||||
|
);
|
||||||
|
|
||||||
|
const visiblePrivateVehicles = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? privateVehicles.filter((vehicle) => filteredPlanIds.has(vehicle.plan_id))
|
||||||
|
: privateVehicles
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleOtherTransports = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? otherTransports.filter((transport) => filteredPlanIds.has(transport.plan_id))
|
||||||
|
: otherTransports
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleLodgings = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? lodgings.filter((lodging) => filteredPlanIds.has(lodging.plan_id))
|
||||||
|
: lodgings
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleActivities = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? activities.filter((activity) => filteredPlanIds.has(activity.plan_id))
|
||||||
|
: activities
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleRestaurants = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? restaurants.filter((restaurant) => filteredPlanIds.has(restaurant.plan_id))
|
||||||
|
: restaurants
|
||||||
|
);
|
||||||
|
|
||||||
|
const visiblePackingLists = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? packingLists.filter((list) => filteredPlanIds.has(list.plan_id))
|
||||||
|
: packingLists
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleTodos = $derived(
|
||||||
|
filteredPlanIds ? todos.filter((list) => filteredPlanIds.has(list.plan_id)) : todos
|
||||||
|
);
|
||||||
|
|
||||||
|
const visiblePackageTours = $derived(
|
||||||
|
filteredPlanIds
|
||||||
|
? packageTours.filter((tour) => filteredPlanIds.has(tour.plan_id))
|
||||||
|
: packageTours
|
||||||
|
);
|
||||||
|
|
||||||
|
const todayDate = $derived(toLocalDateString(new Date()));
|
||||||
|
const showTodayButton = $derived(
|
||||||
|
!!trip.start_date &&
|
||||||
|
!!trip.end_date &&
|
||||||
|
todayDate >= trip.start_date &&
|
||||||
|
todayDate <= trip.end_date
|
||||||
|
);
|
||||||
|
const todayIndex = $derived(Array.isArray(tripDates) ? tripDates.indexOf(todayDate) : -1);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (dayIndex < 0 || dayIndex >= tripDates.length) dayIndex = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (weekIndex < 0 || weekIndex >= weekRanges.length) weekIndex = 0;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -661,6 +814,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="mt-8 flex flex-wrap items-center justify-between gap-3">
|
<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>
|
<h2 class="text-sm font-semibold tracking-wider text-gray-400 uppercase">Plans</h2>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div
|
<div
|
||||||
class="inline-flex rounded-md border border-gray-200 bg-white p-1 text-xs font-medium text-gray-500"
|
class="inline-flex rounded-md border border-gray-200 bg-white p-1 text-xs font-medium text-gray-500"
|
||||||
>
|
>
|
||||||
@@ -677,14 +831,113 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="inline-flex rounded-md border border-gray-200 bg-white p-1 text-xs font-medium text-gray-500"
|
||||||
|
>
|
||||||
|
{#each scheduleViewOptions as option}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (scheduleView = option.id)}
|
||||||
|
class="rounded-md px-2.5 py-1 transition {scheduleView === option.id
|
||||||
|
? 'bg-gray-900 text-white'
|
||||||
|
: 'hover:bg-gray-100'}"
|
||||||
|
aria-pressed={scheduleView === option.id}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if showTodayButton}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
scheduleView = 'day';
|
||||||
|
if (todayIndex >= 0) dayIndex = todayIndex;
|
||||||
|
}}
|
||||||
|
class="rounded-md border border-gray-200 px-2.5 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if scheduleView !== 'trip' && currentRange}
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="relative rounded-lg border border-gray-200 bg-white px-4 py-3 text-center">
|
||||||
|
{#if scheduleView === 'day' && tripDates.length > 1}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute left-3 top-1/2 -translate-y-1/2 rounded-md border border-gray-200 px-2 py-1 text-xs text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
onclick={() => (dayIndex = Math.max(0, dayIndex - 1))}
|
||||||
|
disabled={dayIndex === 0}
|
||||||
|
aria-label="Previous day"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if scheduleView === 'week' && weekRanges.length > 1}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute left-3 top-1/2 -translate-y-1/2 rounded-md border border-gray-200 px-2 py-1 text-xs text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
onclick={() => (weekIndex = Math.max(0, weekIndex - 1))}
|
||||||
|
disabled={weekIndex === 0}
|
||||||
|
aria-label="Previous week"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if scheduleView === 'day'}
|
||||||
|
<p class="text-base font-semibold text-gray-800">
|
||||||
|
{formatLongDate(currentRange.start)}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500">Day {dayIndex + 1}</p>
|
||||||
|
{:else if scheduleView === 'week'}
|
||||||
|
<p class="text-base font-semibold text-gray-800">
|
||||||
|
Week {weekRanges[weekIndex]?.weekNumber ?? 1}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{formatRangeDate(currentRange.start)} - {formatRangeDate(currentRange.end)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if scheduleView === 'day' && tripDates.length > 1}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 rounded-md border border-gray-200 px-2 py-1 text-xs text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
onclick={() => (dayIndex = Math.min(tripDates.length - 1, dayIndex + 1))}
|
||||||
|
disabled={dayIndex >= tripDates.length - 1}
|
||||||
|
aria-label="Next day"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if scheduleView === 'week' && weekRanges.length > 1}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 rounded-md border border-gray-200 px-2 py-1 text-xs text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
onclick={() => (weekIndex = Math.min(weekRanges.length - 1, weekIndex + 1))}
|
||||||
|
disabled={weekIndex >= weekRanges.length - 1}
|
||||||
|
aria-label="Next week"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if planView === 'timeline'}
|
{#if planView === 'timeline'}
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
{#if timeline.scheduled.length === 0}
|
{#if filteredTimeline.scheduled.length === 0}
|
||||||
<p class="text-sm text-gray-500">No scheduled plans yet.</p>
|
<p class="text-sm text-gray-500">
|
||||||
|
{scheduleView === 'trip'
|
||||||
|
? 'No scheduled plans yet.'
|
||||||
|
: 'No scheduled plans for this range.'}
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each timeline.scheduled as group (group.date)}
|
{#each filteredTimeline.scheduled as group (group.date)}
|
||||||
<div class="mt-6 first:mt-0">
|
<div class="mt-6 first:mt-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<p class="text-sm font-semibold text-gray-700">
|
<p class="text-sm font-semibold text-gray-700">
|
||||||
@@ -1028,14 +1281,14 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if timeline.unscheduled.length > 0}
|
{#if filteredTimeline.unscheduled.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<p class="text-sm font-semibold text-gray-700">Unscheduled</p>
|
<p class="text-sm font-semibold text-gray-700">Unscheduled</p>
|
||||||
<div class="h-px flex-1 bg-gray-200"></div>
|
<div class="h-px flex-1 bg-gray-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-col gap-4">
|
<div class="mt-4 flex flex-col gap-4">
|
||||||
{#each timeline.unscheduled as entry (entry.id)}
|
{#each filteredTimeline.unscheduled as entry (entry.id)}
|
||||||
{#if entry.kind === 'day'}
|
{#if entry.kind === 'day'}
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2"
|
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2"
|
||||||
@@ -1361,7 +1614,7 @@
|
|||||||
Destinations
|
Destinations
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each plans.filter((p) => p.type === 'destination') as plan (plan.id)}
|
{#each visiblePlans.filter((p) => p.type === 'destination') as plan (plan.id)}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
||||||
{@const submitForm = () => {
|
{@const submitForm = () => {
|
||||||
@@ -1421,16 +1674,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transportation section -->
|
<!-- Transportation section -->
|
||||||
{#if flightBookings.length > 0 || privateVehicles.length > 0 || otherTransports.length > 0}
|
{#if
|
||||||
|
visibleFlightBookings.length > 0 ||
|
||||||
|
visiblePrivateVehicles.length > 0 ||
|
||||||
|
visibleOtherTransports.length > 0
|
||||||
|
}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
Transportation
|
Transportation
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each plans.filter((p) => p.type === 'transport') as plan (plan.id)}
|
{#each visiblePlans.filter((p) => p.type === 'transport') as plan (plan.id)}
|
||||||
{@const flightBooking = flightBookings.find((b) => b.plan_id === plan.id)}
|
{@const flightBooking = visibleFlightBookings.find((b) => b.plan_id === plan.id)}
|
||||||
{@const privateVehicle = privateVehicles.find((pv) => pv.plan_id === plan.id)}
|
{@const privateVehicle = visiblePrivateVehicles.find((pv) => pv.plan_id === plan.id)}
|
||||||
{@const otherTransport = otherTransports.find((ot) => ot.plan_id === plan.id)}
|
{@const otherTransport = visibleOtherTransports.find((ot) => ot.plan_id === plan.id)}
|
||||||
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
@@ -1532,14 +1789,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Activities section -->
|
<!-- Activities section -->
|
||||||
{#if activities.length > 0}
|
{#if visibleActivities.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
Attractions & Activities
|
Attractions & Activities
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each activities as activity (activity.id)}
|
{#each visibleActivities as activity (activity.id)}
|
||||||
{@const plan = plans.find((p) => p.id === activity.plan_id)}
|
{@const plan = visiblePlans.find((p) => p.id === activity.plan_id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
||||||
@@ -1608,14 +1865,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Restaurants section -->
|
<!-- Restaurants section -->
|
||||||
{#if restaurants.length > 0}
|
{#if visibleRestaurants.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
Restaurants
|
Restaurants
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each restaurants as restaurant (restaurant.id)}
|
{#each visibleRestaurants as restaurant (restaurant.id)}
|
||||||
{@const plan = plans.find((p) => p.id === restaurant.plan_id)}
|
{@const plan = visiblePlans.find((p) => p.id === restaurant.plan_id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
||||||
@@ -1684,14 +1941,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Packing list section -->
|
<!-- Packing list section -->
|
||||||
{#if packingLists.length > 0}
|
{#if visiblePackingLists.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
Packing Lists
|
Packing Lists
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each packingLists as list (list.id)}
|
{#each visiblePackingLists as list (list.id)}
|
||||||
{@const plan = plans.find((p) => p.id === list.plan_id)}
|
{@const plan = visiblePlans.find((p) => p.id === list.plan_id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const submitForm = () => {
|
{@const submitForm = () => {
|
||||||
@@ -1725,14 +1982,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- To-dos section -->
|
<!-- To-dos section -->
|
||||||
{#if todos.length > 0}
|
{#if visibleTodos.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
To-dos
|
To-dos
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each todos as list (list.id)}
|
{#each visibleTodos as list (list.id)}
|
||||||
{@const plan = plans.find((p) => p.id === list.plan_id)}
|
{@const plan = visiblePlans.find((p) => p.id === list.plan_id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const submitForm = () => {
|
{@const submitForm = () => {
|
||||||
@@ -1766,14 +2023,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Lodgings section -->
|
<!-- Lodgings section -->
|
||||||
{#if lodgings.length > 0}
|
{#if visibleLodgings.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
Lodgings
|
Lodgings
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each lodgings as lodging (lodging.id)}
|
{#each visibleLodgings as lodging (lodging.id)}
|
||||||
{@const plan = plans.find((p) => p.id === lodging.plan_id)}
|
{@const plan = visiblePlans.find((p) => p.id === lodging.plan_id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
{@const packageTourContext = byTypeTourContextByPlanId.get(plan.id)}
|
||||||
@@ -1842,14 +2099,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Package Tours section -->
|
<!-- Package Tours section -->
|
||||||
{#if packageTours.length > 0}
|
{#if visiblePackageTours.length > 0}
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||||
Package Tours
|
Package Tours
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each packageTours as tour (tour.id)}
|
{#each visiblePackageTours as tour (tour.id)}
|
||||||
{@const plan = plans.find((p) => p.id === tour.plan_id)}
|
{@const plan = visiblePlans.find((p) => p.id === tour.plan_id)}
|
||||||
{#if plan}
|
{#if plan}
|
||||||
{@const formId = `remove-plan-${plan.id}`}
|
{@const formId = `remove-plan-${plan.id}`}
|
||||||
{@const submitForm = () => {
|
{@const submitForm = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user