import type { SearchResponse, DetailResponse, PassengersCount, InspirationsResponse, Trip } from '~/server/utils/flightics' interface SearchFormData { departures: string[] destination: string[] dateFrom: string dateTo: string stayMinDays: number stayMaxDays: number passengers: PassengersCount maxResults?: number maxStops?: number | null multiCityStops?: string[] } export function useFlightSearch() { const trips = ref([]) const loading = ref(false) const polling = ref(false) const error = ref(null) const searchMeta = ref<{ notComplete: boolean, responseId: string, pollCount: number }>({ notComplete: false, responseId: '', pollCount: 0 }) let abortController: AbortController | null = null function buildPayload(form: SearchFormData) { // Default dates: from today, to +30 days if not provided const now = new Date() const defaultFrom = now.toISOString().slice(0, 10) const defaultTo = new Date(now.getTime() + 30 * 86400000).toISOString().slice(0, 10) const dateFrom = form.dateFrom || defaultFrom const dateTo = form.dateTo || defaultTo const isOneWay = dateFrom === dateTo const isMultiCity = form.multiCityStops && form.multiCityStops.length > 0 const defaultStayRange = { begin: '0001-01-01T00:00:00', end: '0001-01-01T00:00:00' } let stops if (isMultiCity) { // Each intermediate city gets a stay, then return to origin stops = [ ...form.multiCityStops!.map(code => ({ locations: [code], stayRange: { min: form.stayMinDays * 24, max: form.stayMaxDays * 24 }, stayDateRange: defaultStayRange, continueFromAny: true })), { locations: form.departures, stayRange: { min: 0, max: 0 }, stayDateRange: defaultStayRange, continueFromAny: false } ] } else if (isOneWay) { stops = [{ locations: form.destination, stayRange: { min: 0, max: 0 }, stayDateRange: defaultStayRange, continueFromAny: false }] } else { stops = [ { locations: form.destination, stayRange: { min: form.stayMinDays * 24, max: form.stayMaxDays * 24 }, stayDateRange: defaultStayRange, continueFromAny: true }, { locations: form.departures, stayRange: { min: 0, max: 0 }, stayDateRange: defaultStayRange, continueFromAny: false } ] } return { departures: form.departures, local: 'en', departureDateInterval: { begin: `${dateFrom}T00:00:00+00:00`, end: `${dateTo}T00:00:00+00:00` }, stops, endInSameLocation: isMultiCity || !isOneWay, maxStops: form.maxStops ?? null, fixStopsOrder: false, stopLength: { min: 0, max: 0, isSet: false }, maxResults: form.maxResults || 45, passengersCount: form.passengers } } const { learn: learnAirline } = useAirlineNames() function learnAirlineNames(tripList: Trip[]) { for (const trip of tripList) { for (const leg of trip.legs) { for (const seg of leg.segments) { if (seg.company?.name && seg.company.code) { learnAirline(seg.company.code, seg.company.name) } } } } } // Deduplicate trips by bookingToken function mergeTrips(existing: Trip[], incoming: Trip[]): Trip[] { learnAirlineNames(incoming) const seen = new Set(existing.map(t => t.bookingToken)) const newTrips = incoming.filter(t => !seen.has(t.bookingToken)) return [...existing, ...newTrips] } async function search(form: SearchFormData, maxPolls = 3) { // Abort any ongoing search abortController?.abort() abortController = new AbortController() loading.value = true error.value = null trips.value = [] searchMeta.value = { notComplete: false, responseId: '', pollCount: 0 } const payload = buildPayload(form) try { // Initial search const data = await $fetch('/api/search', { method: 'POST', body: payload, signal: abortController.signal }) const initialTrips = data.trips || [] learnAirlineNames(initialTrips) trips.value = initialTrips searchMeta.value = { notComplete: data.notComplete, responseId: data.responseId, pollCount: 1 } loading.value = false // Progressive polling if more results available if (data.notComplete && maxPolls > 1) { polling.value = true for (let i = 1; i < maxPolls; i++) { if (abortController.signal.aborted) break await new Promise(r => setTimeout(r, 800)) const more = await $fetch('/api/search', { method: 'POST', body: payload, signal: abortController.signal }) trips.value = mergeTrips(trips.value, more.trips || []) searchMeta.value = { notComplete: more.notComplete, responseId: more.responseId, pollCount: i + 1 } if (!more.notComplete) break } polling.value = false } } catch (e: any) { if (e.name === 'AbortError') return error.value = e?.data?.message || e?.message || 'Error searching flights' loading.value = false polling.value = false } } function stopPolling() { abortController?.abort() polling.value = false } async function getDetail(bookingToken: string, passengers: PassengersCount) { return $fetch('/api/detail', { method: 'POST', body: { bookingToken, local: 'en', passengersCount: passengers } }) } async function checkPrice(bookingToken: string, passengers: PassengersCount) { return $fetch('/api/check', { method: 'POST', body: { bookingToken, local: 'en', passengersCount: passengers } }) } async function fetchInspirations(from: string, take: number = 100) { return $fetch('/api/inspirations', { query: { from, take, locale: 'en' } }) } return { trips, loading, polling, error, searchMeta, search, stopPolling, getDetail, checkPrice, fetchInspirations, buildPayload } }