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>
125 lines
4.4 KiB
Vue
125 lines
4.4 KiB
Vue
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
search: {
|
|
id: string
|
|
name: string
|
|
route_summary: string
|
|
interval_hours: number
|
|
is_active: boolean
|
|
next_run_at: string | null
|
|
last_run_at: string | null
|
|
run_count: number
|
|
last_error: string | null
|
|
expires_at: string | null
|
|
latest_snapshot: {
|
|
cheapest_price: number
|
|
avg_price: number | null
|
|
total_results: number
|
|
recorded_at: string
|
|
} | null
|
|
}
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
toggle: [id: string, active: boolean]
|
|
remove: [id: string]
|
|
}>()
|
|
|
|
function statusBadge() {
|
|
if (!props.search.is_active) return { label: 'Pausada', color: 'neutral' as const }
|
|
if (props.search.last_error) return { label: 'Error', color: 'error' as const }
|
|
if (props.search.expires_at && new Date(props.search.expires_at) < new Date()) return { label: 'Expirada', color: 'warning' as const }
|
|
return { label: 'Activa', color: 'success' as const }
|
|
}
|
|
|
|
function formatInterval(hours: number) {
|
|
if (hours < 24) return `Cada ${hours}h`
|
|
if (hours === 24) return 'Diario'
|
|
if (hours === 48) return 'Cada 2 dias'
|
|
if (hours === 168) return 'Semanal'
|
|
return `Cada ${Math.round(hours / 24)} dias`
|
|
}
|
|
|
|
function timeAgo(date: string) {
|
|
const diff = Date.now() - new Date(date).getTime()
|
|
const mins = Math.floor(diff / 60000)
|
|
if (mins < 60) return `hace ${mins}min`
|
|
const hours = Math.floor(mins / 60)
|
|
if (hours < 24) return `hace ${hours}h`
|
|
const days = Math.floor(hours / 24)
|
|
return `hace ${days}d`
|
|
}
|
|
|
|
function timeUntil(date: string) {
|
|
const diff = new Date(date).getTime() - Date.now()
|
|
if (diff < 0) return 'pendiente'
|
|
const mins = Math.floor(diff / 60000)
|
|
if (mins < 60) return `en ${mins}min`
|
|
const hours = Math.floor(mins / 60)
|
|
if (hours < 24) return `en ${hours}h`
|
|
const days = Math.floor(hours / 24)
|
|
return `en ${days}d`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 p-4 hover:ring-1 hover:ring-primary-500 transition-all">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<NuxtLink :to="`/tracking/${search.id}`" class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<p class="font-semibold truncate">{{ search.name }}</p>
|
|
<UBadge :label="statusBadge().label" :color="statusBadge().color" size="xs" />
|
|
</div>
|
|
<p class="text-sm text-muted mb-1">{{ search.route_summary }}</p>
|
|
<div class="flex items-center gap-3 text-xs text-muted flex-wrap">
|
|
<span class="flex items-center gap-1">
|
|
<UIcon name="i-lucide-timer" class="text-xs" />
|
|
{{ formatInterval(search.interval_hours) }}
|
|
</span>
|
|
<span v-if="search.run_count > 0" class="flex items-center gap-1">
|
|
<UIcon name="i-lucide-activity" class="text-xs" />
|
|
{{ search.run_count }} ejecucion{{ search.run_count !== 1 ? 'es' : '' }}
|
|
</span>
|
|
<span v-if="search.last_run_at" class="flex items-center gap-1">
|
|
<UIcon name="i-lucide-clock" class="text-xs" />
|
|
{{ timeAgo(search.last_run_at) }}
|
|
</span>
|
|
<span v-if="search.is_active && search.next_run_at" class="flex items-center gap-1">
|
|
<UIcon name="i-lucide-calendar-clock" class="text-xs" />
|
|
Proxima: {{ timeUntil(search.next_run_at) }}
|
|
</span>
|
|
</div>
|
|
</NuxtLink>
|
|
|
|
<!-- Precio actual -->
|
|
<NuxtLink :to="`/tracking/${search.id}`" class="text-right shrink-0">
|
|
<div v-if="search.latest_snapshot" class="mb-1">
|
|
<p class="text-lg font-bold">{{ search.latest_snapshot.cheapest_price.toFixed(0) }}€</p>
|
|
<p class="text-xs text-muted">{{ search.latest_snapshot.total_results }} resultados</p>
|
|
</div>
|
|
<p v-else class="text-sm text-muted">Sin datos</p>
|
|
</NuxtLink>
|
|
|
|
<!-- Acciones -->
|
|
<div class="flex flex-col gap-1 shrink-0">
|
|
<UButton
|
|
:icon="search.is_active ? 'i-lucide-pause' : 'i-lucide-play'"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="xs"
|
|
:title="search.is_active ? 'Pausar' : 'Reanudar'"
|
|
@click="emit('toggle', search.id, !search.is_active)"
|
|
/>
|
|
<UButton
|
|
icon="i-lucide-trash-2"
|
|
color="error"
|
|
variant="ghost"
|
|
size="xs"
|
|
title="Eliminar"
|
|
@click="emit('remove', search.id)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|