diff --git a/src/lib/server/admin/data.ts b/src/lib/server/admin/data.ts index 39cdbcd..0a0397d 100644 --- a/src/lib/server/admin/data.ts +++ b/src/lib/server/admin/data.ts @@ -474,6 +474,7 @@ export interface OperatorTourDayPlan { contact_number?: string | null; // Packing/todo fields items_json?: string | null; + is_optional?: number | null; } export function listPlansForOperatorTour(tourId: number): OperatorTourDayPlan[] { @@ -525,7 +526,8 @@ export function createOperatorTourDayPlan( end_time?: string | null; end_timezone?: string | null; }, - checklistItemsJson?: string | null + checklistItemsJson?: string | null, + isOptional?: boolean ): OperatorTourDayPlan { const maxPos = db.get<{ pos: number }>( 'SELECT COALESCE(MAX(position), -1) + 1 as pos FROM operator_tour_day_plans WHERE operator_tour_day_id = ?', @@ -535,13 +537,14 @@ export function createOperatorTourDayPlan( const l = lodgingFields; const t = transportFields; - let cols = 'operator_tour_day_id, type, title, notes, position'; + let cols = 'operator_tour_day_id, type, title, notes, position, is_optional'; let vals: Array = [ dayId, type, title.trim(), notes?.trim() || null, - position + position, + isOptional ? 1 : 0 ]; if (type === 'lodging' && l) { cols += ', chain, address_line1, address_line2, city_name, country, country_code, postal_code'; diff --git a/src/lib/server/admin/providers/g-adventures.ts b/src/lib/server/admin/providers/g-adventures.ts index fbb8b55..d49b3d6 100644 --- a/src/lib/server/admin/providers/g-adventures.ts +++ b/src/lib/server/admin/providers/g-adventures.ts @@ -23,6 +23,7 @@ interface GAdventureComponent { end_location?: { id?: string; href?: string; name?: string }; accommodation_dossier?: { id: string; href: string; name: string }; transport_dossier?: { id: string; href: string; name: string }; + activity_dossier?: { id?: string; href?: string; name?: string }; } interface GAdventureItineraryDay { @@ -30,6 +31,9 @@ interface GAdventureItineraryDay { label?: string; summary?: string; components?: GAdventureComponent[]; + optional_activities?: Array<{ + activity_dossier?: { id?: string; href?: string; name?: string }; + }>; } // Accommodation dossier detail response (from following accommodation_dossier href) @@ -160,11 +164,30 @@ async function dayPlansFromComponents(components: GAdventureComponent[]): Promis if (details) lodgingFields = lodgingFieldsFromDossier(details); } plans.push({ type: 'lodging', title, notes: notes ?? undefined, lodgingFields }); + } else if (type === 'ACTIVITY') { + const title = c.activity_dossier?.name?.trim() || c.summary?.trim() || 'Activity'; + const notes = c.summary?.trim() || null; + plans.push({ type: 'activity', title, notes: notes ?? undefined, isOptional: false }); } } return plans; } +function optionalActivityPlansFromDay(day: GAdventureItineraryDay): TourDayPlan[] { + const plans: TourDayPlan[] = []; + for (const opt of day.optional_activities ?? []) { + const title = opt.activity_dossier?.name?.trim(); + if (!title) continue; + plans.push({ + type: 'activity', + title, + notes: 'Optional activity', + isOptional: true + }); + } + return plans; +} + export const GAdventuresProvider: TourProvider = { name: 'G Adventures', @@ -203,7 +226,9 @@ export const GAdventuresProvider: TourProvider = { const days: TourDay[] = []; for (const d of rawDays) { - const dayPlans = await dayPlansFromComponents(d.components ?? []); + const includedDayPlans = await dayPlansFromComponents(d.components ?? []); + const optionalActivityPlans = optionalActivityPlansFromDay(d); + const dayPlans = [...includedDayPlans, ...optionalActivityPlans]; days.push({ dayNumber: d.day, title: d.label ?? '', diff --git a/src/lib/server/admin/providers/types.ts b/src/lib/server/admin/providers/types.ts index 0473b3c..a2dff51 100644 --- a/src/lib/server/admin/providers/types.ts +++ b/src/lib/server/admin/providers/types.ts @@ -31,9 +31,10 @@ export interface TourDayPlanTransportFields { /** A transport or lodging plan attached to a day (from provider components) */ export interface TourDayPlan { - type: 'transport' | 'lodging'; + type: 'transport' | 'lodging' | 'activity'; title: string; notes?: string | null; + isOptional?: boolean; /** When type is 'lodging', structured address/location from provider */ lodgingFields?: TourDayPlanLodgingFields; /** When type is 'transport', structured movement details from provider */ diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts index 0778b09..8565ff3 100644 --- a/src/lib/server/db/migrations.ts +++ b/src/lib/server/db/migrations.ts @@ -507,7 +507,8 @@ export function runMigrations(db: Database): void { website TEXT, address TEXT, contact_number TEXT, - items_json TEXT + items_json TEXT, + is_optional INTEGER NOT NULL DEFAULT 0 ) `); db.run(` @@ -515,13 +516,13 @@ export function runMigrations(db: Database): void { 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, - start_location, end_location + start_location, end_location, is_optional ) 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, - NULL as start_location, NULL as end_location + NULL as start_location, NULL as end_location, 0 as is_optional FROM operator_tour_day_plans `); db.run('DROP TABLE operator_tour_day_plans'); @@ -555,7 +556,8 @@ export function runMigrations(db: Database): void { 'website TEXT', 'address TEXT', 'contact_number TEXT', - 'items_json TEXT' + 'items_json TEXT', + 'is_optional INTEGER NOT NULL DEFAULT 0' ]) { try { db.run(`ALTER TABLE operator_tour_day_plans ADD COLUMN ${col}`); diff --git a/src/lib/server/package-tours.ts b/src/lib/server/package-tours.ts index 82b57ed..38f3fb6 100644 --- a/src/lib/server/package-tours.ts +++ b/src/lib/server/package-tours.ts @@ -5,6 +5,7 @@ import { createPlan } from './plans.js'; import { createOtherTransport } from './other-transports.js'; import { createExperience } from './experiences.js'; import { createChecklist } from './checklists.js'; +import { createLodging } from './lodgings.js'; import { listDaysForOperatorTour, listPlansForOperatorTour } from './admin/data.js'; export interface PackageTour { @@ -308,6 +309,12 @@ export function updateTourDay(input: { ]); } +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 cloneTemplateDaysToTour(input: { operatorTourId: number; tripId: string; @@ -316,7 +323,13 @@ export function cloneTemplateDaysToTour(input: { }): void { const days = listDaysForOperatorTour(input.operatorTourId); const allDayPlans = listPlansForOperatorTour(input.operatorTourId); + const tourStartDate = + db.get<{ start_date: string | null }>( + 'SELECT start_date FROM package_tours WHERE plan_id = ?', + [input.tourPlanId] + )?.start_date ?? null; for (const day of days) { + const dayDate = tourStartDate ? addDaysToDate(tourStartDate, day.day_number - 1) : null; const newDay = createTourDay({ tripId: input.tripId, userId: input.userId, @@ -331,7 +344,7 @@ export function cloneTemplateDaysToTour(input: { tripId: input.tripId, userId: input.userId, parentId: newDay.plan_id, - status: 'idea', + status: dp.is_optional ? 'idea' : 'confirmed', title: dp.title, notes: dp.notes ?? undefined, startDate: dp.start_date ?? undefined, @@ -350,7 +363,7 @@ export function cloneTemplateDaysToTour(input: { type: dp.type, name: dp.title, parentId: newDay.plan_id, - status: 'idea', + status: dp.type === 'activity' ? (dp.is_optional ? 'idea' : 'confirmed') : 'idea', bookingId: dp.booking_id ?? undefined, totalCost: dp.total_cost ?? undefined, description: dp.description ?? dp.notes ?? undefined, @@ -364,6 +377,27 @@ export function cloneTemplateDaysToTour(input: { endTime: dp.end_time ?? undefined, endTimezone: dp.end_timezone ?? undefined }); + } else if (dp.type === 'lodging') { + createLodging({ + tripId: input.tripId, + userId: input.userId, + parentId: newDay.plan_id, + status: dp.is_optional ? 'idea' : 'confirmed', + name: dp.title, + chain: dp.chain ?? undefined, + checkInDate: dp.start_date ?? dayDate ?? undefined, + checkInTime: dp.start_time ?? undefined, + checkInTimezone: dp.start_timezone ?? undefined, + checkOutDate: dp.end_date ?? dayDate ?? undefined, + checkOutTime: dp.end_time ?? undefined, + checkOutTimezone: dp.end_timezone ?? undefined, + addressLine1: dp.address_line1 ?? undefined, + addressLine2: dp.address_line2 ?? undefined, + cityName: dp.city_name ?? undefined, + country: dp.country ?? undefined, + countryCode: dp.country_code ?? undefined, + postalCode: dp.postal_code ?? undefined + }); } else if (dp.type === 'packing' || dp.type === 'todo') { let items: string[] = []; try { 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 63c80a7..0b14c76 100644 --- a/src/routes/(protected)/admin/tour-operators/[operatorId]/+page.server.ts +++ b/src/routes/(protected)/admin/tour-operators/[operatorId]/+page.server.ts @@ -101,9 +101,10 @@ export const actions: Actions = { title: string; description: string; dayPlans?: Array<{ - type: 'transport' | 'lodging'; + type: 'transport' | 'lodging' | 'activity'; title: string; notes?: string | null; + isOptional?: boolean; lodgingFields?: { chain?: string | null; address_line1?: string | null; @@ -151,7 +152,10 @@ export const actions: Actions = { plan.title, plan.notes ?? undefined, plan.type === 'lodging' && plan.lodgingFields ? plan.lodgingFields : undefined, - plan.type === 'transport' && plan.transportFields ? plan.transportFields : undefined + plan.type === 'transport' && plan.transportFields ? plan.transportFields : undefined, + undefined, + undefined, + plan.isOptional ?? false ); } } diff --git a/src/routes/(protected)/trips/[id]/+page.svelte b/src/routes/(protected)/trips/[id]/+page.svelte index 64525ec..b468608 100644 --- a/src/routes/(protected)/trips/[id]/+page.svelte +++ b/src/routes/(protected)/trips/[id]/+page.svelte @@ -304,18 +304,18 @@ ? { start: tripDates[dayIndex], end: tripDates[dayIndex] } : null : scheduleView === 'week' - ? weekRanges[weekIndex] ?? null + ? (weekRanges[weekIndex] ?? null) : null ); const filteredTimeline = $derived( currentRange ? { - scheduled: timeline.scheduled.filter( - (group) => group.date >= currentRange.start && group.date <= currentRange.end - ), - unscheduled: [] - } + scheduled: timeline.scheduled.filter( + (group) => group.date >= currentRange.start && group.date <= currentRange.end + ), + unscheduled: [] + } : timeline ); @@ -357,9 +357,7 @@ ); const visibleLodgings = $derived( - filteredPlanIds - ? lodgings.filter((lodging) => filteredPlanIds.has(lodging.plan_id)) - : lodgings + filteredPlanIds ? lodgings.filter((lodging) => filteredPlanIds.has(lodging.plan_id)) : lodgings ); const visibleActivities = $derived( @@ -868,7 +866,7 @@ {#if scheduleView === 'day' && tripDates.length > 1}