diff --git a/src/lib/components/AddOtherTransportationForm.svelte b/src/lib/components/AddOtherTransportationForm.svelte index 698b1f4..8adc446 100644 --- a/src/lib/components/AddOtherTransportationForm.svelte +++ b/src/lib/components/AddOtherTransportationForm.svelte @@ -26,6 +26,8 @@ let status = $state<'idea' | 'tentative' | 'confirmed'>('idea'); let title = $state(''); let notes = $state(''); + let startLocation = $state(''); + let endLocation = $state(''); let startDate = $state(''); let startTime = $state(''); let startTimezone = $state(''); @@ -37,6 +39,8 @@ status = 'idea'; title = ''; notes = ''; + startLocation = ''; + endLocation = ''; startDate = ''; startTime = ''; startTimezone = ''; @@ -108,6 +112,35 @@ > +
+
+ + +
+
+ + +
+
+
diff --git a/src/lib/components/EditOtherTransportationForm.svelte b/src/lib/components/EditOtherTransportationForm.svelte index 9db2a7f..ce7d1cc 100644 --- a/src/lib/components/EditOtherTransportationForm.svelte +++ b/src/lib/components/EditOtherTransportationForm.svelte @@ -40,6 +40,8 @@ let status = $state<'idea' | 'tentative' | 'confirmed'>(planStatus); let title = $state(planTitle ?? ''); let notes = $state(planNotes ?? ''); + let startLocation = $state(otherTransport.start_location ?? ''); + let endLocation = $state(otherTransport.end_location ?? ''); let startDate = $state(otherTransport.start_date ?? ''); let startTime = $state(otherTransport.start_time ?? ''); let startTimezone = $state(otherTransport.start_timezone ?? ''); @@ -88,6 +90,33 @@ >
+
+
+ + +
+
+ + +
+
+
{#if onEdit} diff --git a/src/lib/server/admin/data.ts b/src/lib/server/admin/data.ts index fdeddf6..39cdbcd 100644 --- a/src/lib/server/admin/data.ts +++ b/src/lib/server/admin/data.ts @@ -463,6 +463,8 @@ export interface OperatorTourDayPlan { end_date?: string | null; end_time?: string | null; end_timezone?: string | null; + start_location?: string | null; + end_location?: string | null; // Activity/restaurant fields booking_id?: string | null; total_cost?: number | null; @@ -506,6 +508,8 @@ export function createOperatorTourDayPlan( end_date?: string | null; end_time?: string | null; end_timezone?: string | null; + start_location?: string | null; + end_location?: string | null; }, experienceFields?: { booking_id?: string | null; @@ -554,7 +558,7 @@ export function createOperatorTourDayPlan( } if (type === 'transport') { cols += - ', transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone'; + ', transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone, start_location, end_location'; vals = [ ...vals, t?.transport_kind?.trim() || 'other', @@ -563,7 +567,9 @@ export function createOperatorTourDayPlan( t?.start_timezone?.trim() || null, t?.end_date?.trim() || null, t?.end_time?.trim() || null, - t?.end_timezone?.trim() || null + t?.end_timezone?.trim() || null, + t?.start_location?.trim() || null, + t?.end_location?.trim() || null ]; } if (type === 'activity' || type === 'restaurant') { @@ -616,6 +622,8 @@ export function updateOperatorTourDayPlan( end_date?: string | null; end_time?: string | null; end_timezone?: string | null; + start_location?: string | null; + end_location?: string | null; booking_id?: string | null; total_cost?: number | null; description?: string | null; @@ -649,6 +657,8 @@ export function updateOperatorTourDayPlan( optional('end_date', updates.end_date ?? undefined); optional('end_time', updates.end_time ?? undefined); optional('end_timezone', updates.end_timezone ?? undefined); + optional('start_location', updates.start_location ?? undefined); + optional('end_location', updates.end_location ?? undefined); optional('booking_id', updates.booking_id ?? undefined); optional('total_cost', updates.total_cost ?? undefined); optional('description', updates.description ?? undefined); diff --git a/src/lib/server/admin/providers/g-adventures.ts b/src/lib/server/admin/providers/g-adventures.ts index 29ece7f..fbb8b55 100644 --- a/src/lib/server/admin/providers/g-adventures.ts +++ b/src/lib/server/admin/providers/g-adventures.ts @@ -4,6 +4,7 @@ import type { TourDay, TourDayPlan, TourDayPlanLodgingFields, + TourDayPlanTransportFields, TourProvider, TourSearchResult } from './types.js'; @@ -18,6 +19,8 @@ function authHeaders() { interface GAdventureComponent { type?: string; summary?: string; + start_location?: { id?: string; href?: string; name?: string }; + end_location?: { id?: string; href?: string; name?: string }; accommodation_dossier?: { id: string; href: string; name: string }; transport_dossier?: { id: string; href: string; name: string }; } @@ -38,11 +41,22 @@ interface GAdventureAccommodationDossier { address_line_3?: string; postal_code?: string; city?: { name?: string }; - country?: { name?: string }; + country?: { id?: string; name?: string }; }; } -async function fetchAccommodationDetails(href: string): Promise { +interface GAdventureTransportDossier { + name?: string; + details?: Array<{ + body?: string; + detail_type?: { code?: string; label?: string }; + }>; + dossier_segment?: { code?: string; label?: string }; +} + +async function fetchAccommodationDetails( + href: string +): Promise { try { const res = await fetch(href, { headers: authHeaders() }); if (!res.ok) return null; @@ -52,7 +66,37 @@ async function fetchAccommodationDetails(href: string): Promise { + try { + const res = await fetch(href, { headers: authHeaders() }); + if (!res.ok) return null; + return await res.json(); + } catch { + return null; + } +} + +function summaryFromTransportDossier(dossier: GAdventureTransportDossier | null): string | null { + if (!dossier?.details?.length) return null; + const summary = dossier.details.find((d) => d.detail_type?.code === 'COMMON__SUMMARY'); + const body = summary?.body?.trim(); + return body || null; +} + +function transportKindFromSegmentCode(code: string | undefined): string { + switch (code) { + case 'TRANSPORT__AIR': + return 'flight'; + case 'TRANSPORT__AUTOMOBILE': + return 'private_vehicle'; + default: + return 'other'; + } +} + +function lodgingFieldsFromDossier( + dossier: GAdventureAccommodationDossier +): TourDayPlanLodgingFields | undefined { const addr = dossier.address; if (!addr) return undefined; const address_line1 = addr.address_line_1?.trim() || null; @@ -61,14 +105,7 @@ function lodgingFieldsFromDossier(dossier: GAdventureAccommodationDossier): Tour const country = addr.country?.name?.trim() || null; const country_code = addr.country?.id?.trim() || null; const postal_code = addr.postal_code?.trim() || null; - if ( - !address_line1 && - !address_line2 && - !city_name && - !country && - !country_code && - !postal_code - ) { + if (!address_line1 && !address_line2 && !city_name && !country && !country_code && !postal_code) { return undefined; } return { @@ -83,13 +120,34 @@ function lodgingFieldsFromDossier(dossier: GAdventureAccommodationDossier): Tour async function dayPlansFromComponents(components: GAdventureComponent[]): Promise { const plans: TourDayPlan[] = []; + const transportDossierCache = new Map(); for (const c of components ?? []) { const type = c.type?.toUpperCase(); if (type === 'TRANSPORT' && c.transport_dossier) { + let transportDossier: GAdventureTransportDossier | null = null; + if (c.transport_dossier.href) { + transportDossier = transportDossierCache.get(c.transport_dossier.href) ?? null; + if (!transportDossierCache.has(c.transport_dossier.href)) { + transportDossier = await fetchTransportDetails(c.transport_dossier.href); + transportDossierCache.set(c.transport_dossier.href, transportDossier); + } + } + const title = + transportDossier?.name?.trim() || + c.transport_dossier.name?.trim() || + c.summary?.trim() || + 'Transport'; + const notes = (summaryFromTransportDossier(transportDossier) ?? c.summary?.trim()) || null; + const transportFields: TourDayPlanTransportFields = { + transport_kind: transportKindFromSegmentCode(transportDossier?.dossier_segment?.code), + start_location: c.start_location?.name?.trim() || null, + end_location: c.end_location?.name?.trim() || null + }; plans.push({ type: 'transport', - title: c.transport_dossier.name?.trim() || c.summary?.trim() || 'Transport', - notes: c.summary?.trim() || null + title, + notes, + transportFields }); } else if (type === 'ACCOMMODATION' && c.accommodation_dossier) { const dossier = c.accommodation_dossier; diff --git a/src/lib/server/admin/providers/types.ts b/src/lib/server/admin/providers/types.ts index d212e0a..0473b3c 100644 --- a/src/lib/server/admin/providers/types.ts +++ b/src/lib/server/admin/providers/types.ts @@ -16,6 +16,19 @@ export interface TourDayPlanLodgingFields { postal_code?: string | null; } +/** Transport-specific fields (from provider transport components) */ +export interface TourDayPlanTransportFields { + transport_kind?: string | null; + start_location?: string | null; + end_location?: string | null; + start_date?: string | null; + start_time?: string | null; + start_timezone?: string | null; + end_date?: string | null; + end_time?: string | null; + end_timezone?: string | null; +} + /** A transport or lodging plan attached to a day (from provider components) */ export interface TourDayPlan { type: 'transport' | 'lodging'; @@ -23,6 +36,8 @@ export interface TourDayPlan { notes?: string | null; /** When type is 'lodging', structured address/location from provider */ lodgingFields?: TourDayPlanLodgingFields; + /** When type is 'transport', structured movement details from provider */ + transportFields?: TourDayPlanTransportFields; } export interface TourDay { diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts index 1cfb1a7..0778b09 100644 --- a/src/lib/server/db/migrations.ts +++ b/src/lib/server/db/migrations.ts @@ -277,10 +277,19 @@ export function runMigrations(db: Database): void { end_date TEXT, end_time TEXT, end_timezone TEXT, + start_location TEXT, + end_location TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) `); + for (const col of ['start_location TEXT', 'end_location TEXT']) { + try { + db.run(`ALTER TABLE other_transports ADD COLUMN ${col}`); + } catch { + /* already exists */ + } + } // Activity / restaurant details - links to a plan db.run(` @@ -490,6 +499,8 @@ export function runMigrations(db: Database): void { end_date TEXT, end_time TEXT, end_timezone TEXT, + start_location TEXT, + end_location TEXT, booking_id TEXT, total_cost REAL, description TEXT, @@ -503,12 +514,14 @@ export function runMigrations(db: Database): void { INSERT INTO operator_tour_day_plans__new ( id, operator_tour_day_id, type, title, notes, position, created_at, updated_at, chain, address_line1, address_line2, city_name, country, country_code, postal_code, - transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone + transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone, + start_location, end_location ) SELECT id, operator_tour_day_id, type, title, notes, position, created_at, updated_at, chain, address_line1, address_line2, city_name, country, country_code, postal_code, - transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone + transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone, + NULL as start_location, NULL as end_location FROM operator_tour_day_plans `); db.run('DROP TABLE operator_tour_day_plans'); @@ -534,6 +547,8 @@ export function runMigrations(db: Database): void { 'end_date TEXT', 'end_time TEXT', 'end_timezone TEXT', + 'start_location TEXT', + 'end_location TEXT', 'booking_id TEXT', 'total_cost REAL', 'description TEXT', diff --git a/src/lib/server/other-transports.ts b/src/lib/server/other-transports.ts index 7fa0e83..12cd50a 100644 --- a/src/lib/server/other-transports.ts +++ b/src/lib/server/other-transports.ts @@ -11,6 +11,8 @@ export interface OtherTransport { end_date: string | null; end_time: string | null; end_timezone: string | null; + start_location: string | null; + end_location: string | null; created_at: string; updated_at: string; } @@ -28,6 +30,8 @@ export interface CreateOtherTransportInput { endDate?: string; endTime?: string; endTimezone?: string; + startLocation?: string; + endLocation?: string; } export interface UpdateOtherTransportInput { @@ -42,6 +46,8 @@ export interface UpdateOtherTransportInput { endDate?: string; endTime?: string; endTimezone?: string; + startLocation?: string; + endLocation?: string; } export function createOtherTransport(input: CreateOtherTransportInput): OtherTransport { @@ -72,8 +78,9 @@ export function createOtherTransport(input: CreateOtherTransportInput): OtherTra `INSERT INTO other_transports ( id, plan_id, start_date, start_time, start_timezone, - end_date, end_time, end_timezone - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + end_date, end_time, end_timezone, + start_location, end_location + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ otherTransportId, planId, @@ -82,7 +89,9 @@ export function createOtherTransport(input: CreateOtherTransportInput): OtherTra input.startTimezone ?? null, input.endDate ?? null, input.endTime ?? null, - input.endTimezone ?? null + input.endTimezone ?? null, + input.startLocation?.trim() || null, + input.endLocation?.trim() || null ] ); @@ -100,18 +109,14 @@ export function updateOtherTransport(input: UpdateOtherTransportInput): void { db.run( `UPDATE plans SET status = ?, title = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, - [ - input.status ?? 'idea', - input.title.trim(), - input.notes?.trim() || null, - existing.plan_id - ] + [input.status ?? 'idea', input.title.trim(), input.notes?.trim() || null, existing.plan_id] ); db.run( `UPDATE other_transports SET start_date = ?, start_time = ?, start_timezone = ?, end_date = ?, end_time = ?, end_timezone = ?, + start_location = ?, end_location = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [ @@ -121,6 +126,8 @@ export function updateOtherTransport(input: UpdateOtherTransportInput): void { input.endDate ?? null, input.endTime ?? null, input.endTimezone ?? null, + input.startLocation?.trim() || null, + input.endLocation?.trim() || null, input.otherTransportId ] ); diff --git a/src/lib/server/package-tours.ts b/src/lib/server/package-tours.ts index d7bdc37..82b57ed 100644 --- a/src/lib/server/package-tours.ts +++ b/src/lib/server/package-tours.ts @@ -339,7 +339,9 @@ export function cloneTemplateDaysToTour(input: { startTimezone: dp.start_timezone ?? undefined, endDate: dp.end_date ?? undefined, endTime: dp.end_time ?? undefined, - endTimezone: dp.end_timezone ?? undefined + endTimezone: dp.end_timezone ?? undefined, + startLocation: dp.start_location ?? undefined, + endLocation: dp.end_location ?? undefined }); } else if (dp.type === 'activity' || dp.type === 'restaurant') { createExperience({ diff --git a/src/routes/(protected)/admin/tour-operators/[operatorId]/+page.server.ts b/src/routes/(protected)/admin/tour-operators/[operatorId]/+page.server.ts index 8871e8c..63c80a7 100644 --- a/src/routes/(protected)/admin/tour-operators/[operatorId]/+page.server.ts +++ b/src/routes/(protected)/admin/tour-operators/[operatorId]/+page.server.ts @@ -100,7 +100,31 @@ export const actions: Actions = { dayNumber: number; title: string; description: string; - dayPlans?: { type: 'transport' | 'lodging'; title: string; notes?: string | null }[]; + dayPlans?: Array<{ + type: 'transport' | 'lodging'; + title: string; + notes?: string | null; + lodgingFields?: { + chain?: string | null; + address_line1?: string | null; + address_line2?: string | null; + city_name?: string | null; + country?: string | null; + country_code?: string | null; + postal_code?: string | null; + }; + transportFields?: { + transport_kind?: string | null; + start_location?: string | null; + end_location?: string | null; + start_date?: string | null; + start_time?: string | null; + start_timezone?: string | null; + end_date?: string | null; + end_time?: string | null; + end_timezone?: string | null; + }; + }>; }[] = []; if (provider && providerId) { @@ -126,7 +150,8 @@ export const actions: Actions = { plan.type, plan.title, plan.notes ?? undefined, - plan.type === 'lodging' && plan.lodgingFields ? plan.lodgingFields : undefined + plan.type === 'lodging' && plan.lodgingFields ? plan.lodgingFields : undefined, + plan.type === 'transport' && plan.transportFields ? plan.transportFields : undefined ); } } diff --git a/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.server.ts b/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.server.ts index 3c63b8f..89ed32d 100644 --- a/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.server.ts +++ b/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.server.ts @@ -111,6 +111,8 @@ export const actions: Actions = { end_date: string | null; end_time: string | null; end_timezone: string | null; + start_location: string | null; + end_location: string | null; } | undefined; let experienceFields: @@ -175,7 +177,9 @@ export const actions: Actions = { start_timezone: null, end_date: arrDate, end_time: arrTime, - end_timezone: null + end_timezone: null, + start_location: null, + end_location: null }; } else if (transportationType === 'private_vehicle') { const startAddress = get('start_address'); @@ -188,7 +192,9 @@ export const actions: Actions = { start_timezone: get('departure_timezone') ?? null, end_date: get('arrival_date') ?? null, end_time: get('arrival_time') ?? null, - end_timezone: get('arrival_timezone') ?? null + end_timezone: get('arrival_timezone') ?? null, + start_location: startAddress ?? null, + end_location: endAddress ?? null }; } else { transportFields = { @@ -198,7 +204,9 @@ export const actions: Actions = { start_timezone: get('start_timezone') ?? null, end_date: get('end_date') ?? null, end_time: get('end_time') ?? null, - end_timezone: get('end_timezone') ?? null + end_timezone: get('end_timezone') ?? null, + start_location: get('start_location') ?? null, + end_location: get('end_location') ?? null }; } } @@ -290,6 +298,8 @@ export const actions: Actions = { end_date: get('end_date') ?? undefined, end_time: get('end_time') ?? undefined, end_timezone: get('end_timezone') ?? undefined, + start_location: get('start_location') ?? undefined, + end_location: get('end_location') ?? undefined, booking_id: get('booking_id') ?? undefined, total_cost: (() => { const raw = get('total_cost'); diff --git a/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.svelte b/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.svelte index 5d962b1..d0788e5 100644 --- a/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.svelte +++ b/src/routes/(protected)/admin/tour-operators/[operatorId]/tours/[tourId]/+page.svelte @@ -195,6 +195,8 @@ end_date: editingTransportPlan.end_date ?? null, end_time: editingTransportPlan.end_time ?? null, end_timezone: editingTransportPlan.end_timezone ?? null, + start_location: editingTransportPlan.start_location ?? null, + end_location: editingTransportPlan.end_location ?? null, created_at: '', updated_at: '' } @@ -474,6 +476,8 @@ end_date: plan.end_date ?? null, end_time: plan.end_time ?? null, end_timezone: plan.end_timezone ?? null, + start_location: plan.start_location ?? null, + end_location: plan.end_location ?? null, created_at: '', updated_at: '' }} diff --git a/src/routes/(protected)/trips/[id]/+page.server.ts b/src/routes/(protected)/trips/[id]/+page.server.ts index f81e2fa..d327d80 100644 --- a/src/routes/(protected)/trips/[id]/+page.server.ts +++ b/src/routes/(protected)/trips/[id]/+page.server.ts @@ -279,6 +279,8 @@ export const actions: Actions = { if (transportationType === 'other') { const title = (data.get('title') as string)?.trim(); const notes = (data.get('notes') as string)?.trim() || undefined; + const startLocation = (data.get('start_location') as string)?.trim() || undefined; + const endLocation = (data.get('end_location') as string)?.trim() || undefined; const startDate = (data.get('start_date') as string)?.trim() || undefined; const startTime = (data.get('start_time') as string)?.trim() || undefined; const startTimezone = (data.get('start_timezone') as string)?.trim() || undefined; @@ -296,6 +298,8 @@ export const actions: Actions = { status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea', title, notes, + startLocation, + endLocation, startDate, startTime, startTimezone, @@ -484,6 +488,8 @@ export const actions: Actions = { const otherTransportId = (data.get('other_transport_id') as string)?.trim(); const title = (data.get('title') as string)?.trim(); const notes = (data.get('notes') as string)?.trim() || undefined; + const startLocation = (data.get('start_location') as string)?.trim() || undefined; + const endLocation = (data.get('end_location') as string)?.trim() || undefined; const startDate = (data.get('start_date') as string)?.trim() || undefined; const startTime = (data.get('start_time') as string)?.trim() || undefined; const startTimezone = (data.get('start_timezone') as string)?.trim() || undefined; @@ -501,6 +507,8 @@ export const actions: Actions = { status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea', title, notes, + startLocation, + endLocation, startDate, startTime, startTimezone,