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>
97 lines
3.7 KiB
Vue
97 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
defineProps<{
|
|
runs: SearchRun[]
|
|
}>()
|
|
|
|
function statusBadge(status: string) {
|
|
switch (status) {
|
|
case 'completed': return { label: 'Completado', color: 'success' as const }
|
|
case 'running': return { label: 'Ejecutando', color: 'info' as const }
|
|
case 'failed': return { label: 'Error', color: 'error' as const }
|
|
default: return { label: 'Pendiente', color: 'neutral' as const }
|
|
}
|
|
}
|
|
|
|
function formatDate(date: string) {
|
|
return new Date(date).toLocaleString('es-ES', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
function formatShortDate(dateStr: string) {
|
|
const d = new Date(dateStr)
|
|
return d.toLocaleDateString('es-ES', { day: 'numeric', month: 'short' })
|
|
}
|
|
|
|
function duration(start: string | null, end: string | null) {
|
|
if (!start || !end) return '-'
|
|
const ms = new Date(end).getTime() - new Date(start).getTime()
|
|
if (ms < 1000) return `${ms}ms`
|
|
return `${(ms / 1000).toFixed(1)}s`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-2">
|
|
<h3 class="font-semibold text-sm mb-3">Historial de ejecuciones</h3>
|
|
|
|
<div v-if="runs.length === 0" class="text-center py-6">
|
|
<p class="text-sm text-neutral-500">Aun no hay ejecuciones</p>
|
|
</div>
|
|
|
|
<UCard v-for="run in runs" :key="run.id" class="!p-3">
|
|
<div class="flex items-center justify-between gap-3">
|
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
|
<UBadge :label="statusBadge(run.status).label" :color="statusBadge(run.status).color" size="xs" />
|
|
<span class="text-xs text-muted">{{ formatDate(run.created_at) }}</span>
|
|
<UBadge v-if="run.from_cache" label="Cache" color="neutral" variant="outline" size="xs" />
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 text-sm shrink-0">
|
|
<span v-if="run.cheapest_price != null" class="font-medium">
|
|
{{ run.cheapest_price.toFixed(0) }}€
|
|
</span>
|
|
<span v-if="run.total_trips_found > 0" class="text-xs text-muted">
|
|
{{ run.total_trips_found }} vuelos
|
|
</span>
|
|
<span class="text-xs text-muted">
|
|
{{ duration(run.started_at, run.completed_at) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error message -->
|
|
<p v-if="run.error_message" class="text-xs text-red-500 mt-1">
|
|
{{ run.error_message }}
|
|
</p>
|
|
|
|
<!-- Top trips preview -->
|
|
<div v-if="run.top_trips && run.top_trips.length > 0" class="mt-2 space-y-1.5">
|
|
<NuxtLink
|
|
v-for="(trip, i) in run.top_trips.slice(0, 3)"
|
|
:key="i"
|
|
:to="trip.bookingToken ? `/detail/${encodeURIComponent(trip.bookingToken)}?adults=1` : undefined"
|
|
class="block text-xs text-muted rounded px-1.5 py-1 -mx-1.5 transition-colors"
|
|
:class="trip.bookingToken ? 'hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer' : ''"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium text-foreground">{{ trip.price?.toFixed(0) }}€</span>
|
|
<div class="flex flex-wrap gap-x-3 gap-y-0.5 flex-1">
|
|
<span v-for="(leg, j) in trip.legs" :key="j" class="flex items-center gap-1">
|
|
<UIcon :name="j === 0 ? 'i-lucide-plane-takeoff' : 'i-lucide-plane-landing'" class="text-[10px]" />
|
|
{{ leg.from }} > {{ leg.to }}
|
|
<span v-if="leg.departure" class="text-muted">{{ formatShortDate(leg.departure) }}</span>
|
|
<span v-if="leg.airlines?.length" class="text-muted">({{ leg.airlines.join(', ') }})</span>
|
|
</span>
|
|
</div>
|
|
<UIcon v-if="trip.bookingToken" name="i-lucide-arrow-right" class="text-neutral-400 text-xs shrink-0" />
|
|
</div>
|
|
</NuxtLink>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</template>
|