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 { 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 { 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 { 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' }); const later = makePlan({ type: 'activity', start_date: '2026-05-01' }); 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: '09:00', start_timezone: null, end_date: null, end_time: null, end_timezone: null, created_at: '2026-01-01', updated_at: '2026-01-01' }, { plan_id: later.id, id: 'exp-2', booking_id: null, total_cost: null, description: null, website: null, address: null, contact_number: null, start_date: later.start_date, start_time: '10:30', start_timezone: null, end_date: null, end_time: null, end_timezone: null, created_at: '2026-01-01', updated_at: '2026-01-01' } ], restaurants: [], packingLists: [], 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); }); it('splits multi-segment flights across timeline dates', () => { const transportPlan = makePlan({ id: 'transport-1', type: 'transport', title: 'Multi-city flight' }); const timeline = buildTripTimeline({ plans: [transportPlan], flightBookings: [ { id: 'booking-1', plan_id: transportPlan.id, confirmation_number: null, price: null, currency: 'USD', created_at: '2026-01-01', updated_at: '2026-01-01', segments: [ { id: 'seg-1', booking_id: 'booking-1', position: 0, departure_date: '2026-07-10', airline_id: null, airline_iata: 'UA', airline_name: 'United', flight_number: '100', created_at: '2026-01-01', updated_at: '2026-01-01', route: null }, { id: 'seg-2', booking_id: 'booking-1', position: 1, departure_date: '2026-07-12', airline_id: null, airline_iata: 'UA', airline_name: 'United', flight_number: '200', created_at: '2026-01-01', updated_at: '2026-01-01', route: null } ] } ], privateVehicles: [], otherTransports: [], lodgings: [], activities: [], restaurants: [], packingLists: [], todos: [], packageTours: [] }); expect(timeline.scheduled).toHaveLength(2); expect(timeline.scheduled[0].date).toBe('2026-07-10'); expect(timeline.scheduled[1].date).toBe('2026-07-12'); const firstEntry = timeline.scheduled[0].entries[0]; const secondEntry = timeline.scheduled[1].entries[0]; expect(firstEntry.kind === 'plan' ? firstEntry.flightSegmentIndex : null).toBe(0); expect(secondEntry.kind === 'plan' ? secondEntry.flightSegmentIndex : null).toBe(1); expect(firstEntry.id).not.toBe(secondEntry.id); }); });