Initial commit: Vuelato - buscador de vuelos
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
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>
This commit is contained in:
205
app/components/tracking/CreateTrackingForm.vue
Normal file
205
app/components/tracking/CreateTrackingForm.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
created: []
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const { create } = useTrackedSearches()
|
||||
|
||||
const name = ref('')
|
||||
const departures = ref('')
|
||||
const destination = ref('')
|
||||
const dateFrom = ref('')
|
||||
const dateTo = ref('')
|
||||
const stayMinDays = ref(2)
|
||||
const stayMaxDays = ref(7)
|
||||
const passengers = ref({ adult: 1, child: 0, infant: 0 })
|
||||
const intervalHours = ref(24)
|
||||
const expiresAt = ref('')
|
||||
const submitting = ref(false)
|
||||
const error = ref('')
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
|
||||
watch(dateFrom, (from) => {
|
||||
if (from && dateTo.value && dateTo.value < from) {
|
||||
dateTo.value = from
|
||||
}
|
||||
})
|
||||
|
||||
const intervalOptions = [
|
||||
{ label: 'Cada 6 horas', value: 6 },
|
||||
{ label: 'Cada 12 horas', value: 12 },
|
||||
{ label: 'Diario (24h)', value: 24 },
|
||||
{ label: 'Cada 2 dias', value: 48 },
|
||||
{ label: 'Semanal', value: 168 }
|
||||
]
|
||||
|
||||
// Cargar preferencias de usuario
|
||||
const { profile } = useUserPreferences()
|
||||
watch(profile, (p) => {
|
||||
if (p?.home_airports?.length) {
|
||||
departures.value = p.home_airports.join(',')
|
||||
}
|
||||
if (p?.default_adults) passengers.value.adult = p.default_adults
|
||||
if (p?.default_children) passengers.value.child = p.default_children
|
||||
if (p?.default_infants) passengers.value.infant = p.default_infants
|
||||
}, { immediate: true })
|
||||
|
||||
function buildRouteSummary() {
|
||||
const dep = departures.value.split(',').filter(Boolean).join(',')
|
||||
const dest = destination.value || '?'
|
||||
return `${dep} > ${dest}`
|
||||
}
|
||||
|
||||
function buildSearchParams() {
|
||||
const depList = departures.value.split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
|
||||
const dest = destination.value.trim().toUpperCase()
|
||||
|
||||
const now = new Date()
|
||||
const defaultFrom = now.toISOString().slice(0, 10)
|
||||
const defaultTo = new Date(now.getTime() + 30 * 86400000).toISOString().slice(0, 10)
|
||||
const from = dateFrom.value || defaultFrom
|
||||
const to = dateTo.value || defaultTo
|
||||
|
||||
const isOneWay = from === to
|
||||
|
||||
const stops = isOneWay
|
||||
? [{
|
||||
locations: [dest],
|
||||
stayRange: { min: 0, max: 0 },
|
||||
stayDateRange: { begin: '0001-01-01T00:00:00', end: '0001-01-01T00:00:00' },
|
||||
continueFromAny: false
|
||||
}]
|
||||
: [
|
||||
{
|
||||
locations: [dest],
|
||||
stayRange: { min: stayMinDays.value * 24, max: stayMaxDays.value * 24 },
|
||||
stayDateRange: { begin: '0001-01-01T00:00:00', end: '0001-01-01T00:00:00' },
|
||||
continueFromAny: true
|
||||
},
|
||||
{
|
||||
locations: depList,
|
||||
stayRange: { min: 0, max: 0 },
|
||||
stayDateRange: { begin: '0001-01-01T00:00:00', end: '0001-01-01T00:00:00' },
|
||||
continueFromAny: false
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
departures: depList,
|
||||
local: 'en',
|
||||
departureDateInterval: {
|
||||
begin: `${from}T00:00:00+00:00`,
|
||||
end: `${to}T00:00:00+00:00`
|
||||
},
|
||||
stops,
|
||||
endInSameLocation: !isOneWay,
|
||||
maxStops: null,
|
||||
fixStopsOrder: false,
|
||||
stopLength: { min: 0, max: 0, isSet: false },
|
||||
maxResults: 45,
|
||||
passengersCount: passengers.value
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!departures.value || !destination.value || !name.value) {
|
||||
error.value = 'Rellena nombre, origen y destino'
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
await create({
|
||||
name: name.value,
|
||||
searchParams: buildSearchParams(),
|
||||
routeSummary: buildRouteSummary(),
|
||||
intervalHours: intervalHours.value,
|
||||
expiresAt: expiresAt.value || undefined
|
||||
})
|
||||
emit('created')
|
||||
} catch (e: unknown) {
|
||||
const err = e as { data?: { message?: string } }
|
||||
error.value = err?.data?.message || 'Error al crear seguimiento'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold">Nuevo seguimiento de precios</h3>
|
||||
<UButton icon="i-lucide-x" color="neutral" variant="ghost" size="xs" @click="emit('cancel')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormField label="Nombre" required>
|
||||
<UInput v-model="name" placeholder="Ej: Madrid-Londres julio" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<UFormField label="Origen" required>
|
||||
<SearchAirportInput v-model="departures" placeholder="Aeropuerto origen" icon="i-lucide-plane-takeoff" multiple />
|
||||
</UFormField>
|
||||
<UFormField label="Destino" required>
|
||||
<SearchAirportInput v-model="destination" placeholder="Aeropuerto destino" icon="i-lucide-plane-landing" multiple />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<UFormField label="Fecha desde">
|
||||
<UInput v-model="dateFrom" type="date" :min="today" :max="dateTo || undefined" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="Fecha hasta">
|
||||
<UInput v-model="dateTo" type="date" :min="dateFrom || today" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<UFormField label="Estancia minima (dias)">
|
||||
<UInput v-model.number="stayMinDays" type="number" :min="1" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="Estancia maxima (dias)">
|
||||
<UInput v-model.number="stayMaxDays" type="number" :min="1" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UFormField label="Pasajeros">
|
||||
<PassengerPicker v-model="passengers" />
|
||||
</UFormField>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<UFormField label="Frecuencia de busqueda">
|
||||
<select
|
||||
v-model.number="intervalHours"
|
||||
class="w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm"
|
||||
>
|
||||
<option v-for="opt in intervalOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</UFormField>
|
||||
<UFormField label="Expira el (opcional)">
|
||||
<UInput v-model="expiresAt" type="date" :min="today" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UAlert v-if="error" :title="error" color="error" icon="i-lucide-alert-circle" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton label="Cancelar" color="neutral" variant="outline" @click="emit('cancel')" />
|
||||
<UButton label="Crear seguimiento" icon="i-lucide-plus" :loading="submitting" @click="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
Reference in New Issue
Block a user