Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Nuxt 4 + Supabase + Flightics API. Incluye búsqueda de vuelos, inspiraciones, watchlist, tracking de precios y mapa interactivo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
5.1 KiB
Vue
134 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import type { Trip } from '~/server/utils/flightics'
|
|
|
|
const props = defineProps<{ trip: Trip }>()
|
|
defineEmits<{ select: [trip: Trip] }>()
|
|
|
|
const { showOriginTime } = useOriginTime()
|
|
|
|
// Timezone offset (in seconds) of the origin airport: local - UTC
|
|
const originTzOffset = computed(() => {
|
|
const seg = props.trip.legs[0]?.segments[0]
|
|
if (!seg) return 0
|
|
return seg.departureTimestamp - seg.departureUtcTimestamp
|
|
})
|
|
|
|
const departureCode = computed(() => props.trip.legs[0]?.segments[0]?.departureCode ?? '')
|
|
const arrivalCode = computed(() => props.trip.legs[0]?.segments.at(-1)?.arrivalCode ?? '')
|
|
const departureDate = computed(() => props.trip.legs[0]?.segments[0]?.departureDate ?? '')
|
|
const routeSummary = computed(() => {
|
|
const codes = props.trip.legs.map(l => l.segments[0]?.departureCode).filter(Boolean)
|
|
const lastArr = props.trip.legs.at(-1)?.segments.at(-1)?.arrivalCode
|
|
if (lastArr) codes.push(lastArr)
|
|
return codes.join(' > ')
|
|
})
|
|
|
|
// Total flight time across all legs (sum of each segment's flight duration, using UTC)
|
|
const totalFlightMs = computed(() => {
|
|
let ms = 0
|
|
for (const leg of props.trip.legs) {
|
|
for (const seg of leg.segments) {
|
|
ms += (seg.arrivalUtcTimestamp - seg.departureUtcTimestamp) * 1000
|
|
}
|
|
}
|
|
return ms
|
|
})
|
|
|
|
// Time at destination: from arrival of last segment of outbound leg to departure of first segment of return leg (using UTC)
|
|
const stayInfo = computed(() => {
|
|
const legs = props.trip.legs
|
|
if (legs.length < 2) return null
|
|
|
|
const arrivalUtc = legs[0].segments.at(-1)?.arrivalUtcTimestamp
|
|
const departureUtc = legs[1].segments[0]?.departureUtcTimestamp
|
|
if (!arrivalUtc || !departureUtc) return null
|
|
|
|
const stayMs = (departureUtc - arrivalUtc) * 1000
|
|
if (stayMs <= 0) return null
|
|
|
|
const stayHours = stayMs / 3600000
|
|
const fullDays = Math.floor(stayHours / 24)
|
|
const remainingHours = Math.round(stayHours - fullDays * 24)
|
|
const nights = fullDays
|
|
|
|
return { nights, fullDays, remainingHours, stayMs }
|
|
})
|
|
|
|
// Total trip days: from first departure to last arrival (local dates for calendar/vacation planning)
|
|
const totalTripDays = computed(() => {
|
|
const legs = props.trip.legs
|
|
if (!legs.length) return null
|
|
|
|
const firstDep = legs[0].segments[0]?.departureDate
|
|
const lastArr = legs.at(-1)?.segments.at(-1)?.arrivalDate
|
|
if (!firstDep || !lastArr) return null
|
|
|
|
const depDate = new Date(firstDep)
|
|
const arrDate = new Date(lastArr)
|
|
|
|
// Calendar days: count from departure day to arrival day inclusive
|
|
const depDay = new Date(depDate.getFullYear(), depDate.getMonth(), depDate.getDate())
|
|
const arrDay = new Date(arrDate.getFullYear(), arrDate.getMonth(), arrDate.getDate())
|
|
const calendarDays = Math.round((arrDay.getTime() - depDay.getTime()) / 86400000) + 1
|
|
|
|
return calendarDays
|
|
})
|
|
|
|
function formatDuration(ms: number) {
|
|
const totalMin = Math.round(ms / 60000)
|
|
const h = Math.floor(totalMin / 60)
|
|
const m = totalMin % 60
|
|
return h > 0 ? `${h}h ${m}m` : `${m}m`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UCard class="hover:ring-primary-500 transition-all cursor-pointer" @click="$emit('select', trip)">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div class="flex-1 space-y-1">
|
|
<FlightLeg v-for="(leg, i) in trip.legs" :key="i" :leg="leg" :index="i" :origin-tz-offset="originTzOffset" :show-origin-time="showOriginTime" />
|
|
</div>
|
|
|
|
<div class="text-right shrink-0 pl-4 border-l border-neutral-200 dark:border-neutral-700 space-y-1">
|
|
<p class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
|
{{ trip.totalCost.toFixed(0) }}<span class="text-sm font-normal ml-0.5">€</span>
|
|
</p>
|
|
|
|
<p class="text-xs text-muted flex items-center gap-1 justify-end">
|
|
<UIcon name="i-lucide-plane" class="text-xs" />
|
|
{{ formatDuration(totalFlightMs) }}
|
|
</p>
|
|
|
|
<template v-if="stayInfo">
|
|
<p class="text-xs text-muted flex items-center gap-1 justify-end">
|
|
<UIcon name="i-lucide-map-pin" class="text-xs" />
|
|
{{ stayInfo.fullDays }}d {{ stayInfo.remainingHours }}h en destino
|
|
</p>
|
|
<p class="text-xs text-muted flex items-center gap-1 justify-end">
|
|
<UIcon name="i-lucide-moon" class="text-xs" />
|
|
{{ stayInfo.nights }} noche{{ stayInfo.nights !== 1 ? 's' : '' }}
|
|
</p>
|
|
</template>
|
|
|
|
<p v-if="totalTripDays" class="text-xs text-muted flex items-center gap-1 justify-end">
|
|
<UIcon name="i-lucide-calendar-range" class="text-xs" />
|
|
{{ totalTripDays }} dia{{ totalTripDays !== 1 ? 's' : '' }} total
|
|
</p>
|
|
|
|
<div class="flex items-center gap-1 justify-end pt-1">
|
|
<DetailWatchlistToggle
|
|
:booking-token="trip.bookingToken"
|
|
:route-summary="routeSummary"
|
|
:departure-code="departureCode"
|
|
:arrival-code="arrivalCode"
|
|
:departure-date="departureDate"
|
|
:price="trip.totalCost"
|
|
:passengers="{ adult: 1, child: 0, infant: 0 }"
|
|
/>
|
|
<UButton size="xs" label="Ver" trailing-icon="i-lucide-arrow-right" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</template>
|