vueloenglobo.mx/resources/views/template/checkout.blade.php

2104 lines
111 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('template.layout')
@push('css')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.3/css/all.min.css">
<style>
/* ========== VARIABLES GLOBALES (MODO DÍA POR DEFECTO) ========== */
:root {
color-scheme: light dark;
--primary-color: #2563eb;
--primary-dark: #1d4ed8;
--primary-light: #eff6ff;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
--bg-body: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--bg-card: #ffffff;
--bg-input: #ffffff;
--border-radius: 1rem;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}
/* ========== MODO OSCURO (DARK MODE) ========== */
.dark {
--bg-body: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
--bg-card: #1f2937;
--bg-input: #374151;
--gray-50: #111827;
--gray-100: #1f2937;
--gray-200: #374151;
--gray-300: #4b5563;
--gray-400: #6b7280;
--gray-500: #9ca3af;
--gray-600: #d1d5db;
--gray-700: #e5e7eb;
--gray-800: #f3f4f6;
--gray-900: #f9fafb;
--primary-light: #1e3a8a;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5);
}
/* ========== ESTILOS BASE CON TRANSICIONES ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg-body);
min-height: 100vh;
color: var(--gray-800);
transition: background 0.3s ease, color 0.3s ease;
}
.flight-details {
padding: 2rem 1rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.details-grid {
display: grid;
grid-template-columns: 1fr 380px;
gap: 2rem;
}
@media (max-width:1024px) {
.details-grid {
grid-template-columns: 1fr;
}
}
.main-card, .sidebar-card, .boarding-pass {
background: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: var(--shadow-xl);
overflow: hidden;
transition: background 0.3s ease, box-shadow 0.3s ease;
}
.card-body {
padding: 2rem;
}
@media (max-width:768px) {
.card-body {
padding: 1.5rem;
}
}
.flight-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--gray-200);
}
.flight-title h1 {
font-size: 1.875rem;
font-weight: 700;
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.flight-title .subtitle {
color: var(--gray-600);
font-size: 0.875rem;
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 1rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
color: white;
margin-top: 0.75rem;
}
.price-box {
text-align: right;
background: linear-gradient(135deg, var(--primary-light) 0%, #e0e7ff 100%);
padding: 1rem 1.5rem;
border-radius: var(--border-radius);
position: relative;
transition: background 0.3s ease;
}
.dark .price-box {
background: linear-gradient(135deg, #1e3a8a 0%, #312e81 100%);
}
.price-badge {
position: absolute;
top: -0.75rem;
right: 1rem;
background: var(--danger-color);
color: white;
font-size: 0.75rem;
font-weight: 700;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
}
.price-old {
font-size: 0.875rem;
color: var(--gray-500);
text-decoration: line-through;
}
.price-current {
font-size: 2rem;
font-weight: 800;
color: var(--primary-color);
line-height: 1;
}
.price-label {
font-size: 0.75rem;
color: var(--gray-500);
}
.meta-info {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--gray-200);
}
.meta-item {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--gray-600);
font-size: 0.875rem;
}
.meta-item i {
color: var(--primary-color);
}
.rating {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stars {
display: flex;
gap: 0.125rem;
color: #fbbf24;
}
.rating-value {
font-weight: 600;
color: var(--gray-700);
}
.gallery-section {
margin-bottom: 2rem;
transition: all 0.3s ease;
}
.gallery-section.hide-gallery {
display: none !important;
}
.main-swiper {
border-radius: var(--border-radius);
overflow: hidden;
margin-bottom: 1rem;
}
.main-swiper .swiper-slide img {
width: 100%;
height: 400px;
object-fit: cover;
}
.thumbs-swiper {
margin-top: 1rem;
}
.thumbs-swiper .swiper-slide {
width: 100px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.3s;
}
.thumbs-swiper .swiper-slide-thumb-active {
opacity: 1;
}
.thumbs-swiper .swiper-slide img {
width: 100%;
height: 70px;
object-fit: cover;
border-radius: 0.5rem;
}
@media (max-width:768px) {
.main-swiper .swiper-slide img {
height: 250px;
}
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
background: var(--gray-100);
padding: 1rem;
border-radius: var(--border-radius);
transition: background 0.3s ease;
}
.step-item {
flex: 1;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
cursor: pointer;
transition: all 0.3s;
border-radius: 0.5rem;
color: var(--gray-700);
}
.step-item:hover {
background: var(--gray-200);
}
.step-item.active {
background: var(--primary-light);
color: var(--primary-color);
}
.step-item.completed .step-number {
background: var(--success-color);
color: white;
}
.step-number {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-300);
border-radius: 50%;
font-weight: 700;
font-size: 0.875rem;
color: var(--gray-700);
transition: all 0.3s;
}
.step-item.active .step-number {
background: var(--primary-color);
color: white;
}
.step-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--gray-700);
}
.step-item.active .step-label {
color: var(--primary-color);
font-weight: 600;
}
.requirements-checklist {
background: var(--gray-50);
border-radius: var(--border-radius);
padding: 1rem;
margin: 1rem 0;
border-left: 4px solid var(--warning-color);
transition: background 0.3s ease;
}
.requirement-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
color: var(--gray-700);
}
.requirement-icon {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--bg-card);
border-radius: 50%;
color: var(--warning-color);
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--gray-700);
font-size: 0.875rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--gray-200);
border-radius: 0.75rem;
font-size: 1rem;
transition: all 0.3s;
background: var(--bg-input);
color: var(--gray-900);
}
.form-control::placeholder {
color: var(--gray-400);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.dark .form-control:focus {
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
}
.inline-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width:640px) {
.inline-fields {
grid-template-columns: 1fr;
}
}
.btn-primary, .btn-secondary, .btn-pay, .btn-imprimir {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: var(--gray-200);
color: var(--gray-700);
}
.btn-secondary:hover {
background: var(--gray-300);
}
.btn-pay {
background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%);
color: white;
width: 100%;
justify-content: center;
font-size: 1.125rem;
padding: 1rem;
margin-top: 1rem;
}
.btn-pay:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.counter-group, .service-qty-control {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--gray-100);
border-radius: 0.75rem;
padding: 0.25rem;
transition: background 0.3s ease;
}
.counter-btn, .qty-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 0.5rem;
background: var(--bg-card);
font-size: 1.25rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
color: var(--gray-700);
}
.counter-btn:hover, .qty-btn:hover {
background: var(--primary-color);
color: white;
}
.counter-input {
width: 60px;
text-align: center;
border: none;
font-size: 1rem;
font-weight: 600;
background: transparent;
color: var(--gray-900);
}
.checkbox-group {
margin-bottom: 1rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
user-select: none;
color: var(--gray-700);
}
.checkbox-label input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: var(--primary-color);
}
.extras-category {
margin-bottom: 2rem;
}
.category-title {
font-weight: 700;
font-size: 1.125rem;
color: var(--gray-800);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary-color);
display: inline-block;
}
.extras-table {
width: 100%;
border-collapse: collapse;
}
.extras-table th {
text-align: left;
padding: 0.75rem;
background: var(--gray-100);
font-weight: 600;
font-size: 0.875rem;
color: var(--gray-600);
transition: background 0.3s ease, color 0.3s ease;
}
.extras-table td {
padding: 0.75rem;
border-bottom: 1px solid var(--gray-200);
color: var(--gray-700);
}
.service-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 0.5rem;
}
.section {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--gray-200);
}
.section-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--gray-800);
margin-bottom: 1rem;
}
.section-subtitle {
font-size: 1rem;
font-weight: 600;
color: var(--gray-700);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.note {
background: var(--primary-light);
padding: 1rem;
border-radius: var(--border-radius);
margin-top: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
color: var(--primary-color);
transition: background 0.3s ease, color 0.3s ease;
}
.dark .note {
background: #1e3a8a;
color: #93c5fd;
}
.error-message {
color: var(--danger-color);
font-size: 0.75rem;
margin-top: 0.25rem;
display: none;
}
.error-message.show {
display: block;
}
hr {
margin: 1.5rem 0;
border: none;
border-top: 1px solid var(--gray-200);
}
.includes-list-modern {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 0.75rem;
}
.includes-list-modern li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 0.75rem;
border: 1px solid var(--gray-200);
transition: all 0.3s ease;
color: var(--gray-700);
}
.includes-list-modern li:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-sm);
transform: translateY(-2px);
}
.includes-list-modern li i {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--primary-light);
color: var(--primary-color);
border-radius: 50%;
font-size: 0.75rem;
flex-shrink: 0;
transition: all 0.3s ease;
}
.dark .includes-list-modern li i {
background: #1e3a8a;
color: #93c5fd;
}
.sidebar {
position: sticky;
top: 2rem;
height: fit-content;
}
.card-title {
font-size: 1.125rem;
font-weight: 700;
color: var(--gray-800);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--gray-200);
}
.edit-link {
color: var(--primary-color);
cursor: pointer;
font-size: 0.75rem;
margin-left: 0.5rem;
}
.edit-link:hover {
text-decoration: underline;
}
.summary-item {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px dashed var(--gray-200);
font-size: 0.875rem;
color: var(--gray-700);
}
.summary-item span:first-child {
color: var(--gray-600);
}
.summary-item span:last-child {
font-weight: 600;
color: var(--gray-800);
}
.subtotal-vuelo {
font-weight: 700;
color: var(--gray-900);
}
.summary-extras {
margin: 0.5rem 0;
}
.extra-item {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
font-size: 0.75rem;
color: var(--gray-600);
border-bottom: 1px dashed var(--gray-200);
}
.summary-total {
display: flex;
justify-content: space-between;
padding: 1rem 0;
margin-top: 1rem;
border-top: 2px solid var(--gray-200);
font-weight: 800;
font-size: 1.125rem;
color: var(--primary-color);
}
.trust-badges {
display: flex;
justify-content: space-between;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
}
.trust-badge {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--gray-500);
}
.trust-badge i {
color: var(--success-color);
}
.discount-toggle {
color: var(--primary-color);
cursor: pointer;
font-size: 0.875rem;
display: inline-block;
margin-bottom: 0.5rem;
}
.discount-toggle:hover {
text-decoration: underline;
}
.discount-input {
display: none;
gap: 0.5rem;
margin-bottom: 1rem;
}
.discount-input.active {
display: flex;
}
.boarding-pass {
font-family: monospace;
padding: 20px;
max-width: 800px;
margin: 0 auto;
border: 2px solid var(--gray-800);
}
/* ========== FLATPICKR DARK MODE OVERRIDES ========== */
.dark .flatpickr-calendar {
background: var(--bg-card);
border-color: var(--gray-200);
box-shadow: var(--shadow-xl);
}
.dark .flatpickr-day.selected, .dark .flatpickr-day.startRange, .dark .flatpickr-day.endRange {
background: var(--primary-color);
border-color: var(--primary-color);
}
.dark .flatpickr-day.inRange {
background: rgba(37, 99, 235, 0.2);
}
.dark .flatpickr-months .flatpickr-month {
background: var(--bg-card);
color: var(--gray-900);
}
.dark .flatpickr-current-month .flatpickr-monthDropdown-months,
.dark .flatpickr-current-month input.cur-year {
color: var(--gray-900);
}
.dark .flatpickr-weekdays {
background: var(--bg-card);
}
.dark .flatpickr-weekday {
color: var(--gray-600);
}
.dark .flatpickr-day {
color: var(--gray-900);
}
.dark .flatpickr-day.prevMonthDay, .dark .flatpickr-day.nextMonthDay {
color: var(--gray-500);
}
/* ========== BOTÓN FLOTANTE DE TEMA ========== */
.theme-toggle-btn {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--bg-card);
color: var(--gray-700);
border: 2px solid var(--gray-200);
box-shadow: var(--shadow-lg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
z-index: 1000;
transition: all 0.3s ease;
}
.theme-toggle-btn:hover {
transform: scale(1.1);
border-color: var(--primary-color);
color: var(--primary-color);
}
.dark .theme-toggle-btn {
background: var(--gray-700);
color: #fbbf24;
border-color: var(--gray-600);
}
.dark .theme-toggle-btn:hover {
border-color: #fbbf24;
color: #fbbf24;
}
/* ========== MEDIA QUERIES ========== */
@media (max-width:768px) {
.flight-header {
flex-direction: column;
}
.price-box {
text-align: center;
width: 100%;
}
.step-label {
display: none;
}
.step-item {
justify-content: center;
}
.trust-badges {
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
.flatpickr-calendar {
width: 90% !important;
left: 5% !important;
right: 5% !important;
}
}
@media print {
body * {
visibility: hidden;
}
.boarding-pass, .boarding-pass * {
visibility: visible;
}
.boarding-pass {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.no-print, .theme-toggle-btn {
display: none !important;
}
}
</style>
@endpush
@section('content')
<br><br><br>
@php
// ========== VARIABLES SEGURAS ==========
$vuelo = $datosVuelo['vuelo'] ?? null;
$categoria = $datosVuelo['categoria'] ?? null;
$imagenesGaleria = collect($datosVuelo['imagenes'] ?? [])->pluck('ruta_imagen_vuelo')->toArray();
$incluye = $datosVuelo['incluye'] ?? [];
$adicionales = $adicionales ?? [];
$extrasArray = is_array($adicionales) ? $adicionales : [];
if (empty($imagenesGaleria)) {
$imagenesGaleria = ['https://placehold.co/1200x800/2563eb/white?text=Sky+Balloons'];
}
$moneda = 'MXN';
$simbolo = '$';
$precioActual = (float) ($vuelo['precio_especial'] ?? ($vuelo['precio_publico'] ?? 0));
$precioOriginal = (float) ($vuelo['precio_publico'] ?? 0);
$precioNino = (float) ($vuelo['precios_adicionales'][0]['precio'] ?? 1900);
if (($vuelo['mostrar_precios_dolares'] ?? 0) == 1) {
$moneda = 'USD';
$simbolo = 'US$';
}
$tieneDescuento = $precioActual < $precioOriginal;
$descuentoPorcentaje = $tieneDescuento ? round((1 - $precioActual / $precioOriginal) * 100) : 0;
$locale = app()->getLocale();
$isSpanish = $locale === 'es';
$serviciosIncluidosIds = [];
foreach ($incluye as $item) {
$posiblesIds = [$item['id_item'] ?? null, $item['id'] ?? null, $item['servicio_id'] ?? null, $item['id_servicio'] ?? null, $item['item_id'] ?? null, $item['servicio']['id'] ?? null];
foreach ($posiblesIds as $id) {
if ($id && is_numeric($id)) {
$serviciosIncluidosIds[] = (int) $id;
}
}
}
$serviciosIncluidosIds = array_unique($serviciosIncluidosIds);
function isServicioIncluido($servicioId, $incluidosIds) {
return in_array($servicioId, $incluidosIds);
}
$extrasFiltrados = [];
$totalOriginalExtras = 0;
foreach ($extrasArray as $categoriaData) {
$categoriaNombre = $categoriaData['nombre_categoria'] ?? ($isSpanish ? 'Otros servicios' : 'Other services');
$serviciosFiltrados = [];
foreach ($categoriaData['servicios'] ?? [] as $servicio) {
$servicioId = $servicio['id'] ?? null;
$totalOriginalExtras++;
if (!$servicioId || !isServicioIncluido($servicioId, $serviciosIncluidosIds)) {
$serviciosFiltrados[] = $servicio;
}
}
if (!empty($serviciosFiltrados)) {
$extrasFiltrados[] = ['nombre_categoria' => $categoriaNombre, 'servicios' => $serviciosFiltrados];
}
}
$totalExtrasDisponibles = collect($extrasFiltrados)->sum(fn($cat) => count($cat['servicios']));
$todosIncluidos = ($totalOriginalExtras > 0 && $totalExtrasDisponibles === 0);
$clipApiKey = config('clip.api_key', '');
@endphp
@if ($vuelo)
<article class="flight-details" data-flight-id="{{ $vuelo['id_vuelo'] }}">
<div class="container">
<div class="details-grid">
<div class="main-card">
<div class="card-body">
<header class="flight-header">
<div class="flight-title">
<h1>{{ $vuelo['nombre_vuelo'] }}</h1>
<p class="subtitle">{{ $vuelo['descripcion_corta'] }}</p>
@if (!empty($vuelo['highlighted_text_flight_es']) && $isSpanish)
<span class="badge" style="background: {{ $vuelo['highlighted_background_color_badge_flight_es'] ?? '#2563eb' }}">
<i class="fas fa-star"></i> {{ $vuelo['highlighted_text_flight_es'] }}
</span>
@endif
</div>
<div class="price-box">
@if ($tieneDescuento)
<div class="price-badge">-{{ $descuentoPorcentaje }}%</div>
<div class="price-old">{{ $simbolo }}{{ number_format($precioOriginal, 2, '.', ',') }}</div>
@endif
<div class="price-current">{{ $simbolo }}{{ number_format($precioActual, 2, '.', ',') }}</div>
<div class="price-label">{{ $isSpanish ? 'por adulto' : 'per adult' }}</div>
</div>
</header>
<div class="meta-info">
<div class="meta-item"><i class="fas fa-tag"></i><span>{{ $categoria['nombre'] ?? ($isSpanish ? 'Aventura' : 'Adventure') }}</span></div>
<div class="meta-item"><i class="far fa-clock"></i><span>{{ \Carbon\Carbon::parse($vuelo['hora_actividad'] ?? '06:30')->format('g:i A') }}</span></div>
<div class="rating">
<div class="stars">
@for ($i = 1; $i <= 5; $i++)
@if ($i <= round($vuelo['raiting_global_number'] ?? 0))
<i class="fas fa-star"></i>
@elseif($i - 0.5 <= round($vuelo['raiting_global_number'] ?? 0))
<i class="fas fa-star-half-alt"></i>
@else
<i class="far fa-star"></i>
@endif
@endfor
</div>
<span class="rating-value">{{ number_format($vuelo['raiting_global_number'] ?? 0, 1) }}</span>
</div>
</div>
<!-- GALERÍA -->
<div class="gallery-section" id="gallerySection">
<div class="swiper main-swiper">
<div class="swiper-wrapper">
@foreach ($imagenesGaleria as $index => $img)
<div class="swiper-slide"><img src="{{ $img }}" alt="Imagen {{ $index + 1 }}" loading="{{ $index === 0 ? 'eager' : 'lazy' }}"></div>
@endforeach
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-pagination"></div>
</div>
@if (count($imagenesGaleria) > 1)
<div class="swiper thumbs-swiper">
<div class="swiper-wrapper">
@foreach ($imagenesGaleria as $index => $img)
<div class="swiper-slide"><img src="{{ $img }}" alt="Miniatura {{ $index + 1 }}"></div>
@endforeach
</div>
</div>
@endif
</div>
<!-- STEP INDICATOR -->
<div class="step-indicator" role="tablist">
<div class="step-item active" data-step="1">
<div class="step-number">1</div><span class="step-label">{{ $isSpanish ? 'Vuelo' : 'Flight' }}</span>
</div>
<div class="step-item" data-step="2">
<div class="step-number">2</div><span class="step-label">{{ $isSpanish ? 'Responsable' : 'Responsible' }}</span>
</div>
<div class="step-item" data-step="3">
<div class="step-number">3</div><span class="step-label">{{ $isSpanish ? 'Extras' : 'Extras' }}</span>
</div>
<div class="step-item" data-step="4">
<div class="step-number">4</div><span class="step-label">{{ $isSpanish ? 'Pago' : 'Payment' }}</span>
</div>
<div class="step-item" data-step="5">
<div class="step-number">5</div><span class="step-label">{{ $isSpanish ? 'Pasajeros' : 'Passengers' }}</span>
</div>
</div>
<!-- STEP 1 -->
<div id="step-1" class="step-content">
<form id="formStep1">
<div class="form-group">
<label>{{ $isSpanish ? 'Fecha del vuelo *' : 'Flight date *' }}</label>
<input type="text" id="fecha_vuelo" class="form-control flatpickr-input" placeholder="{{ $isSpanish ? 'Selecciona una fecha' : 'Select a date' }}">
<input type="date" id="fecha_vuelo_native" class="form-control native-datepicker" style="display: none;" hidden>
<div class="error-message" id="fecha-error"></div>
</div>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="fechaLibreCheckbox">
<span class="checkbox-text">{{ $isSpanish ? 'Fecha por confirmar (me pondré en contacto)' : 'Date to be confirmed (I will contact you)' }}</span>
</label>
</div>
<div class="form-group">
<label>{{ $isSpanish ? 'Adultos (+12 años) *' : 'Adults (+12 years) *' }}</label>
<div class="counter-group">
<button type="button" id="minusAdult" class="counter-btn">-</button>
<input type="number" id="adultos" value="1" min="1" max="10" class="counter-input">
<button type="button" id="plusAdult" class="counter-btn">+</button>
</div>
</div>
<div class="form-group">
<label>{{ $isSpanish ? 'Niños (4-12 años)' : 'Children (4-12 years)' }}</label>
<div class="counter-group">
<button type="button" id="minusChild" class="counter-btn">-</button>
<input type="number" id="ninios" value="0" min="0" max="10" class="counter-input">
<button type="button" id="plusChild" class="counter-btn">+</button>
</div>
</div>
<button type="button" class="btn-primary next-step" data-next="2">{{ $isSpanish ? 'Continuar' : 'Continue' }} <i class="fas fa-arrow-right"></i></button>
</form>
</div>
<!-- STEP 2 -->
<div id="step-2" class="step-content" style="display: none;">
<form id="formStep2">
<h3 style="font-size: 1.2rem; margin-bottom: 1rem; color: var(--gray-800);">
{{ $isSpanish ? 'Datos del responsable de la reserva' : 'Booking responsible data' }}
</h3>
<div class="inline-fields">
<div class="form-group">
<label>{{ $isSpanish ? 'Nombres *' : 'First names *' }}</label>
<input type="text" id="resp_nombres" class="form-control" placeholder="{{ $isSpanish ? 'Nombres' : 'First names' }}">
<div class="error-message" id="resp_nombres-error"></div>
</div>
<div class="form-group">
<label>{{ $isSpanish ? 'Apellidos *' : 'Last names *' }}</label>
<input type="text" id="resp_apellidos" class="form-control" placeholder="{{ $isSpanish ? 'Apellidos' : 'Last names' }}">
<div class="error-message" id="resp_apellidos-error"></div>
</div>
</div>
<div class="inline-fields">
<div class="form-group">
<label>{{ $isSpanish ? 'Correo electrónico *' : 'Email *' }}</label>
<input type="email" id="resp_email" class="form-control" placeholder="ejemplo@correo.com">
<div class="error-message" id="resp_email-error"></div>
</div>
<div class="form-group">
<label>{{ $isSpanish ? 'Teléfono (con lada) *' : 'Phone (with area code) *' }}</label>
<input type="tel" id="resp_telefono" class="form-control">
<div class="error-message" id="resp_telefono-error"></div>
</div>
</div>
<div class="note"><i class="fas fa-info-circle"></i>
{{ $isSpanish ? 'Los datos de cada pasajero se solicitarán después de realizar el pago.' : 'Passenger details will be requested after payment.' }}
</div>
<div class="requirements-checklist">
<div class="requirement-item">
<div class="requirement-icon">⚠️</div>
<span>{{ $isSpanish ? 'Presentar identificación oficial el día del vuelo' : 'Present official ID on flight day' }}</span>
</div>
<div class="requirement-item">
<div class="requirement-icon">⚠️</div>
<span>{{ $isSpanish ? 'Llegar 30 minutos antes de la hora programada' : 'Arrive 30 minutes before scheduled time' }}</span>
</div>
<div class="requirement-item">
<div class="requirement-icon">⚠️</div>
<span>{{ $isSpanish ? 'El peso registrado debe ser exacto' : 'Registered weight must be accurate' }}</span>
</div>
</div>
<div style="display: flex; gap: 1rem; margin-top: 1.5rem;">
<button type="button" class="btn-secondary prev-step" data-prev="1"><i class="fas fa-arrow-left"></i> {{ $isSpanish ? 'Volver' : 'Back' }}</button>
<button type="button" class="btn-primary next-step" data-next="3">{{ $isSpanish ? 'Continuar' : 'Continue' }} <i class="fas fa-arrow-right"></i></button>
</div>
</form>
</div>
<!-- STEP 3: EXTRAS -->
<div id="step-3" class="step-content" style="display: none;">
<h3 style="font-size: 1.3rem; margin-bottom: 1.5rem; color: var(--gray-800);">
{{ $isSpanish ? 'Servicios adicionales' : 'Additional services' }}</h3>
<p style="margin-bottom: 1.5rem; color: var(--gray-600);">
{{ $isSpanish ? 'Selecciona los servicios que deseas agregar a tu reserva.' : 'Select the services you want to add to your booking.' }}
</p>
@if($todosIncluidos)
<div class="alert alert-info" style="padding:2rem; text-align:center; background:var(--gray-100); border-radius:1rem; color: var(--gray-700);">
<i class="fas fa-check-circle" style="font-size:2rem; color:var(--success-color); margin-bottom:0.5rem; display:inline-block;"></i>
<p style="margin:0;">
{{ $isSpanish ? '¡Todos los servicios adicionales ya están incluidos en tu paquete! No hay extras disponibles para seleccionar.' : 'All additional services are already included in your package! No extras available to select.' }}
</p>
</div>
@elseif($totalExtrasDisponibles > 0)
<div id="extras-container">
@foreach ($extrasFiltrados as $categoriaData)
@php $servicios = $categoriaData['servicios']; @endphp
@if (!empty($servicios))
<div class="extras-category">
<div class="category-title">{{ $categoriaData['nombre_categoria'] }}</div>
<table class="extras-table">
<thead>
<tr>
<th style="width:80px"></th>
<th>{{ $isSpanish ? 'Servicio' : 'Service' }}</th>
<th style="width:100px;text-align:center">{{ $isSpanish ? 'Costo' : 'Cost' }}</th>
<th style="width:120px;text-align:center">{{ $isSpanish ? 'Cantidad' : 'Quantity' }}</th>
</tr>
</thead>
<tbody>
@foreach ($servicios as $servicio)
@php
$servicioId = $servicio['id'] ?? null;
$servicioPrecio = $servicio['precio'] ?? 0;
$servicioDescripcion = $servicio['descripcion'] ?? 'Sin descripción';
$servicioImagen = !empty($servicio['imagenes'][0]) ? $servicio['imagenes'][0] : null;
@endphp
@if ($servicioId)
<tr data-service-id="{{ $servicioId }}" data-price="{{ $servicioPrecio }}" data-description="{{ addslashes($servicioDescripcion) }}">
<td>
@if($servicioImagen)
<img src="{{ $servicioImagen }}" class="service-image" alt="{{ $servicioDescripcion }}" onerror="this.style.display='none'">
@else
<div class="service-image" style="background: var(--gray-200); display: flex; align-items: center; justify-content: center;">
<i class="fas fa-gift" style="color: var(--gray-400); font-size: 1.5rem;"></i>
</div>
@endif
</td>
<td>
<strong>{{ $servicioDescripcion }}</strong>
@if(!empty($servicio['observaciones']))
<br><small style="color: var(--gray-500); font-size: 0.7rem;">{{ Str::limit($servicio['observaciones'], 100) }}</small>
@endif
</td>
<td style="text-align: center; font-weight: 600;">{{ $simbolo }}{{ number_format($servicioPrecio, 0, ',', '.') }}</td>
<td style="text-align: center;">
<div class="service-qty-control">
<button type="button" class="qty-btn btn-decrement" data-service-id="{{ $servicioId }}">−</button>
<span class="qty-display service-qty-display" data-service-id="{{ $servicioId }}">0</span>
<button type="button" class="qty-btn btn-increment" data-service-id="{{ $servicioId }}">+</button>
</div>
</td>
</tr>
@endif
@endforeach
</tbody>
</table>
</div>
@endif
@endforeach
</div>
@else
<div class="alert alert-info" style="padding:2rem; text-align:center; background:var(--gray-100); border-radius:1rem; color: var(--gray-700);">
<i class="fas fa-info-circle" style="font-size:2rem; color:var(--primary-color); margin-bottom:0.5rem; display:inline-block;"></i>
<p style="margin:0;">
{{ $isSpanish ? 'No hay servicios adicionales disponibles para este vuelo.' : 'No additional services available for this flight.' }}
</p>
</div>
@endif
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
<button type="button" class="btn-secondary prev-step" data-prev="2"><i class="fas fa-arrow-left"></i> {{ $isSpanish ? 'Volver' : 'Back' }}</button>
<button type="button" class="btn-primary next-step" data-next="4">{{ $isSpanish ? 'Continuar' : 'Continue' }} <i class="fas fa-arrow-right"></i></button>
</div>
</div>
<!-- STEP 4: PAYMENT (Clip) -->
<div id="step-4" class="step-content" style="display: none;">
<div style="text-align: center; margin-bottom: 1.5rem;">
<p style="font-size:1rem; color:var(--gray-600);">{{ $isSpanish ? 'Total a pagar' : 'Total to pay' }}</p>
<h2 id="totalMostrado" style="font-size:2.5rem; color:var(--primary-color); font-weight:800;">{{ $simbolo }}{{ number_format($precioActual, 0, ',', '.') }}</h2>
</div>
<form id="clip-payment-form">
@csrf
<div class="form-group">
<label>{{ $isSpanish ? 'Correo electrónico para el recibo *' : 'Email for receipt *' }}</label>
<input type="email" id="payment_email" class="form-control" required>
</div>
<div class="form-group">
<label>{{ $isSpanish ? 'Teléfono *' : 'Phone *' }}</label>
<input type="tel" id="payment_phone" class="form-control" required>
</div>
<div class="form-group">
<label>{{ $isSpanish ? 'Descripción del pago' : 'Payment description' }}</label>
<input type="text" id="payment_description" class="form-control" value="{{ $vuelo['nombre_vuelo'] }}">
</div>
<div class="form-group">
<label>💳 {{ $isSpanish ? 'Datos de la tarjeta' : 'Card details' }}</label>
<div id="clip-checkout"></div>
<div id="clip-error" class="error-message" style="margin-top:0.5rem;"></div>
</div>
<div>
<span class="discount-toggle" id="toggleCupon">{{ $isSpanish ? '¿Tienes un código de descuento?' : 'Have a discount code?' }}</span>
<div class="discount-input" id="cuponInput" style="display:none;">
<input type="text" id="cuponCode" class="form-control" placeholder="{{ $isSpanish ? 'Ingresa tu código' : 'Enter your code' }}">
<button type="button" id="aplicarCupon" class="btn-secondary" style="width:auto;">{{ $isSpanish ? 'Aplicar' : 'Apply' }}</button>
</div>
</div>
<button type="submit" id="btnPagar" class="btn-pay"><i class="fas fa-lock"></i> {{ $isSpanish ? 'Pagar ahora' : 'Pay now' }}</button>
</form>
<div class="trust-badges">
<div class="trust-badge"><i class="fas fa-shield-alt"></i> {{ $isSpanish ? 'Pago seguro SSL' : 'SSL secure payment' }}</div>
<div class="trust-badge"><i class="fas fa-undo-alt"></i> {{ $isSpanish ? 'Cancelación gratuita (24h)' : 'Free cancellation (24h)' }}</div>
<div class="trust-badge"><i class="fas fa-headset"></i> {{ $isSpanish ? 'Atención 24/7' : '24/7 support' }}</div>
</div>
<button type="button" class="btn-secondary prev-step" data-prev="3" style="margin-top:1rem; width:100%;"><i class="fas fa-arrow-left"></i> {{ $isSpanish ? 'Volver' : 'Back' }}</button>
</div>
<!-- STEP 5: PASAJEROS Y PASE DE ABORDAR -->
<div id="step-5" class="step-content" style="display: none;">
<h3 style="font-size: 1.3rem; margin-bottom: 1.5rem; color: var(--gray-800);">
{{ $isSpanish ? 'Datos de los pasajeros' : 'Passenger details' }}</h3>
<div id="passengers-step5-container"></div>
<div id="boarding-pass-container" style="display: none; margin-top: 2rem;"></div>
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
<button type="button" class="btn-secondary" id="backFromStep5Btn"><i class="fas fa-arrow-left"></i> {{ $isSpanish ? 'Volver' : 'Back' }}</button>
<button type="button" class="btn-primary" id="savePassengersBtn">{{ $isSpanish ? 'Guardar datos' : 'Save passengers' }} <i class="fas fa-save"></i></button>
</div>
<div id="imprimirContainer" style="margin-top: 1rem; text-align: center;"></div>
</div>
<!-- Detalles del viaje e incluye -->
@if (!empty($vuelo['descripcion_larga']))
<section class="section">
<h2 class="section-title">{{ $isSpanish ? 'Detalles del viaje' : 'Trip details' }}</h2>
<div class="description" style="color: var(--gray-600); line-height: 1.6;">
{!! $vuelo['descripcion_larga'] !!}
</div>
</section>
@endif
@if (!empty($incluye))
<section class="section">
<h3 class="section-subtitle"><i class="fas fa-gift"></i> {{ $isSpanish ? '¿Qué incluye tu experiencia?' : 'What\'s included in your experience?' }}</h3>
<ul class="includes-list-modern">
@foreach ($incluye as $item)
<li>
<i class="fas fa-check"></i>
<span>{{ $item['titulo_item'] ?? ($item['nombre'] ?? 'Item') }}</span>
</li>
@endforeach
</ul>
</section>
@endif
</div>
</div>
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="sidebar-card">
<h3 class="card-title">
<i class="fas fa-receipt"></i> {{ $isSpanish ? 'Resumen de reserva' : 'Booking summary' }}
<a class="edit-link" data-edit-step="1"><i class="fas fa-edit"></i></a>
</h3>
<div class="summary-item">
<span>{{ $vuelo['nombre_vuelo'] }}</span>
<span id="resumenPasajeros">{{ $isSpanish ? '1 adulto' : '1 adult' }}</span>
</div>
<div class="summary-item">
<span>{{ $isSpanish ? 'Fecha' : 'Date' }}</span>
<span id="resumenFecha">{{ $isSpanish ? 'No seleccionada' : 'Not selected' }} <a class="edit-link" data-edit-step="1"><i class="fas fa-pencil-alt"></i></a></span>
</div>
<div class="summary-item">
<span>{{ $isSpanish ? 'Adultos' : 'Adults' }}</span>
<span id="resumenAdultos">1 <a class="edit-link" data-edit-step="1"><i class="fas fa-pencil-alt"></i></a></span>
</div>
<div class="summary-item">
<span>{{ $isSpanish ? 'Niños' : 'Children' }}</span>
<span id="resumenNinos">0 <a class="edit-link" data-edit-step="1"><i class="fas fa-pencil-alt"></i></a></span>
</div>
<div class="summary-item subtotal-vuelo">
<span>{{ $isSpanish ? 'Subtotal vuelo' : 'Flight subtotal' }}</span>
<span id="subtotalVuelo">{{ $simbolo }}{{ number_format($precioActual, 0, ',', '.') }}</span>
</div>
<div id="extras-summary" class="summary-extras"></div>
<div class="summary-item" id="descuentoAplicado" style="display:none;">
<span>{{ $isSpanish ? 'Descuento' : 'Discount' }}</span>
<span id="descuentoMonto" style="color: var(--success-color);">-<span></span></span>
</div>
<div class="summary-total">
<span>{{ $isSpanish ? 'Total' : 'Total' }}</span>
<span id="resumenTotal">{{ $simbolo }}{{ number_format($precioActual, 0, ',', '.') }}</span>
</div>
</div>
</aside>
</div>
</div>
</article>
@else
<div class="container">
<div style="background: var(--bg-card); padding:2rem; border-radius:1rem; text-align:center; color: var(--gray-700);">
<i class="fas fa-exclamation-triangle" style="font-size:3rem; color:var(--warning-color); margin-bottom:1rem;"></i>
<h2>{{ $isSpanish ? 'Vuelo no encontrado' : 'Flight not found' }}</h2>
</div>
</div>
@endif
<!-- Botón flotante para cambiar tema (Modo Noche/Día) -->
<button id="theme-toggle" class="theme-toggle-btn" aria-label="Cambiar tema">
<i class="fas fa-moon"></i>
</button>
@endsection
@push('js')
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/es.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.min.js"></script>
<script src="https://sdk.clip.mx/js/clip-sdk.js"></script>
<script>
// ========== VARIABLES PHP EXPORTADAS ==========
const PHP_VARS = {
vuelo: @json($vuelo),
categoria: @json($categoria),
imagenesGaleria: @json($imagenesGaleria),
incluye: @json($incluye),
extrasArray: @json($extrasArray),
serviciosIncluidosIds: @json($serviciosIncluidosIds),
extrasFiltrados: @json($extrasFiltrados),
totalExtrasDisponibles: {{ $totalExtrasDisponibles }},
todosIncluidos: {{ $todosIncluidos ? 'true' : 'false' }},
moneda: '{{ $moneda }}',
simbolo: '{{ $simbolo }}',
precioActual: {{ $precioActual }},
precioOriginal: {{ $precioOriginal }},
precioNino: {{ $precioNino }},
tieneDescuento: {{ $tieneDescuento ? 'true' : 'false' }},
descuentoPorcentaje: {{ $descuentoPorcentaje }},
isSpanish: {{ $isSpanish ? 'true' : 'false' }},
flightId: {{ $vuelo['id_vuelo'] ?? 0 }},
flightName: '{{ addslashes($vuelo['nombre_vuelo'] ?? '') }}',
flightShortDesc: '{{ addslashes($vuelo['descripcion_corta'] ?? '') }}',
flightLongDesc: `{!! addslashes($vuelo['descripcion_larga'] ?? '') !!}`,
flightTime: '{{ $vuelo['hora_actividad'] ?? '06:30' }}',
ratingGlobal: {{ $vuelo['raiting_global_number'] ?? 0 }},
csrfToken: '{{ csrf_token() }}',
clipApiKey: '{{ $clipApiKey }}',
routeCheckoutProcesar: '{{ route("checkout.procesar") }}',
routePaseAbordar: '{{ route("checkout.pase.abordar") }}',
routeCotizaVuelo: '{{ route("checkout.cotiza.vuelo") }}',
highlightedText: '{{ $vuelo["highlighted_text_flight_es"] ?? "" }}',
highlightedBgColor: '{{ $vuelo["highlighted_background_color_badge_flight_es"] ?? "#2563eb" }}'
};
function isServicioIncluidoEnPaquete(serviceId) {
return PHP_VARS.serviciosIncluidosIds.includes(parseInt(serviceId));
}
document.addEventListener('DOMContentLoaded', function () {
// ========== MODO OSCURO / CLARO ==========
const themeToggleBtn = document.getElementById('theme-toggle');
if (themeToggleBtn) {
const themeIcon = themeToggleBtn.querySelector('i');
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) {
document.body.classList.add('dark');
themeIcon.classList.replace('fa-moon', 'fa-sun');
}
themeToggleBtn.addEventListener('click', () => {
document.body.classList.toggle('dark');
const isDark = document.body.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
if (isDark) {
themeIcon.classList.replace('fa-moon', 'fa-sun');
} else {
themeIcon.classList.replace('fa-sun', 'fa-moon');
}
});
}
// ========== CONFIGURACIÓN INICIAL ==========
const precioAdulto = PHP_VARS.precioActual;
const precioNino = PHP_VARS.precioNino;
const simbolo = PHP_VARS.simbolo;
const isSpanish = PHP_VARS.isSpanish;
let adultos = 1, ninios = 0, fechaSeleccionada = '', fechaLibre = false;
let serviciosSeleccionados = [], descuentoCupon = 0, currentStep = 1, reservationId = null;
let passengerDataSaved = false;
let clipCard = null;
let itiResponsable = null;
window.adultos = adultos;
window.ninios = ninios;
window.fechaSeleccionada = fechaSeleccionada;
window.fechaLibre = fechaLibre;
window.serviciosSeleccionados = serviciosSeleccionados;
window.aplicarCuponAuto = function (codigo) {
if (codigo === 'DESCUENTO10') {
const subtotal = (adultos * precioAdulto) + (ninios * precioNino) + getTotalExtras();
descuentoCupon = subtotal * 0.10;
actualizarResumen();
return true;
}
return false;
};
function capitalizeWords(str) {
if (!str) return '';
return str.toLowerCase().replace(/\b\w/g, char => char.toUpperCase());
}
function setupNameCapitalization(inputElement) {
if (!inputElement) return;
inputElement.addEventListener('blur', function () {
const formatted = capitalizeWords(this.value);
if (this.value !== formatted) {
this.value = formatted;
this.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
function generateDayOptions() {
let html = '<option value="">' + (isSpanish ? 'Día' : 'Day') + '</option>';
for (let i = 1; i <= 31; i++) html += `<option value="${i}">${i}</option>`;
return html;
}
function generateMonthOptions() {
const months = isSpanish ? ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
let html = '<option value="">' + (isSpanish ? 'Mes' : 'Month') + '</option>';
months.forEach((m, idx) => html += `<option value="${idx + 1}">${m}</option>`);
return html;
}
function generateYearOptions() {
const currentYear = new Date().getFullYear();
let html = '<option value="">' + (isSpanish ? 'Año' : 'Year') + '</option>';
for (let y = currentYear; y >= 1900; y--) html += `<option value="${y}">${y}</option>`;
return html;
}
function renderPassengersStep5(container, totalAdultos, totalNinos, existingData = []) {
if (!container) return;
const total = totalAdultos + totalNinos;
container.innerHTML = '';
for (let i = 0; i < total; i++) {
const tipo = i < totalAdultos ? (isSpanish ? 'Adulto' : 'Adult') : (isSpanish ? 'Niño' : 'Child');
const idx = i + 1;
const card = document.createElement('div');
card.className = 'passenger-card';
card.style.marginBottom = '1rem';
card.style.padding = '1rem';
card.style.border = '1px solid var(--gray-200)';
card.style.borderRadius = '0.75rem';
card.style.background = 'var(--bg-card)';
card.innerHTML = `
<div class="passenger-title" style="font-weight:700; margin-bottom:0.75rem; color: var(--gray-800);">${tipo} ${idx}</div>
<div class="inline-fields">
<div class="form-group"><label>${isSpanish ? 'Nombres *' : 'First names *'}</label><input type="text" class="form-control passenger-nombres" data-pax="${i}" placeholder="${isSpanish ? 'Nombres' : 'First names'}"></div>
<div class="form-group"><label>${isSpanish ? 'Apellidos *' : 'Last names *'}</label><input type="text" class="form-control passenger-apellidos" data-pax="${i}" placeholder="${isSpanish ? 'Apellidos' : 'Last names'}"></div>
</div>
<div class="inline-fields">
<div class="form-group"><label>${isSpanish ? 'Día *' : 'Day *'}</label><select class="form-control passenger-day" data-pax="${i}">${generateDayOptions()}</select></div>
<div class="form-group"><label>${isSpanish ? 'Mes *' : 'Month *'}</label><select class="form-control passenger-month" data-pax="${i}">${generateMonthOptions()}</select></div>
<div class="form-group"><label>${isSpanish ? 'Año *' : 'Year *'}</label><select class="form-control passenger-year" data-pax="${i}">${generateYearOptions()}</select></div>
</div>
<div class="form-group"><label>${isSpanish ? 'Peso aproximado (kg) *' : 'Approximate weight (kg) *'}</label><input type="number" step="1" class="form-control passenger-peso" data-pax="${i}" placeholder="${isSpanish ? 'Ej: 75' : 'Ex: 75'}"></div>
`;
container.appendChild(card);
}
for (let i = 0; i < total; i++) {
const nombresInput = document.querySelector(`#passengers-step5-container .passenger-nombres[data-pax="${i}"]`);
const apellidosInput = document.querySelector(`#passengers-step5-container .passenger-apellidos[data-pax="${i}"]`);
setupNameCapitalization(nombresInput);
setupNameCapitalization(apellidosInput);
if (existingData[i]) {
const p = existingData[i];
if (nombresInput) nombresInput.value = capitalizeWords(p.nombres || '');
if (apellidosInput) apellidosInput.value = capitalizeWords(p.apellidos || '');
const daySelect = document.querySelector(`#passengers-step5-container .passenger-day[data-pax="${i}"]`);
const monthSelect = document.querySelector(`#passengers-step5-container .passenger-month[data-pax="${i}"]`);
const yearSelect = document.querySelector(`#passengers-step5-container .passenger-year[data-pax="${i}"]`);
const pesoInput = document.querySelector(`#passengers-step5-container .passenger-peso[data-pax="${i}"]`);
if (daySelect && p.fecha_nacimiento) {
const [year, month, day] = p.fecha_nacimiento.split('-');
daySelect.value = parseInt(day, 10);
monthSelect.value = parseInt(month, 10);
yearSelect.value = parseInt(year, 10);
}
if (pesoInput) pesoInput.value = p.peso_kg || '';
}
}
}
function collectPassengerDataFromStep5(totalAdultos, totalNinos) {
const total = totalAdultos + totalNinos;
const pasajeros = [];
for (let i = 0; i < total; i++) {
const nombresRaw = document.querySelector(`#passengers-step5-container .passenger-nombres[data-pax="${i}"]`)?.value.trim() || '';
const apellidosRaw = document.querySelector(`#passengers-step5-container .passenger-apellidos[data-pax="${i}"]`)?.value.trim() || '';
const nombres = capitalizeWords(nombresRaw);
const apellidos = capitalizeWords(apellidosRaw);
const dia = document.querySelector(`#passengers-step5-container .passenger-day[data-pax="${i}"]`)?.value;
const mes = document.querySelector(`#passengers-step5-container .passenger-month[data-pax="${i}"]`)?.value;
const año = document.querySelector(`#passengers-step5-container .passenger-year[data-pax="${i}"]`)?.value;
const peso = document.querySelector(`#passengers-step5-container .passenger-peso[data-pax="${i}"]`)?.value;
if (!nombres || !apellidos || !dia || !mes || !año || !peso) return null;
const fechaNac = `${año}-${mes.padStart(2, '0')}-${dia.padStart(2, '0')}`;
const fechaObj = new Date(fechaNac);
if (isNaN(fechaObj.getTime()) || fechaObj > new Date()) return null;
pasajeros.push({
nombres, apellidos,
fecha_nacimiento: fechaNac,
peso_kg: parseInt(peso),
tipo: i < totalAdultos ? 'adulto' : 'nino'
});
}
return pasajeros;
}
function getSubtotalVuelo() { return (adultos * precioAdulto) + (ninios * precioNino); }
function getTotalExtras() { return serviciosSeleccionados.reduce((sum, s) => sum + (s.cantidad * s.precio), 0); }
function getTotalGeneral() { return Math.max(0, getSubtotalVuelo() + getTotalExtras() - descuentoCupon); }
function actualizarFechaResumen() {
let texto = '';
if (fechaLibre) texto = isSpanish ? 'Por confirmar' : 'To be confirmed';
else if (fechaSeleccionada) {
const date = new Date(fechaSeleccionada);
texto = !isNaN(date.getTime()) ? date.toLocaleDateString(isSpanish ? 'es-MX' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : fechaSeleccionada;
} else texto = isSpanish ? 'No seleccionada' : 'Not selected';
const rf = document.getElementById('resumenFecha');
if (rf) rf.innerHTML = texto + ' <a class="edit-link" data-edit-step="1"><i class="fas fa-pencil-alt"></i></a>';
}
function actualizarResumen() {
const subtotalVuelo = getSubtotalVuelo(), totalExtras = getTotalExtras(), totalGeneral = getTotalGeneral();
const ra = document.getElementById('resumenAdultos'), rn = document.getElementById('resumenNinos'), rp = document.getElementById('resumenPasajeros');
const sv = document.getElementById('subtotalVuelo'), es = document.getElementById('extras-summary'), rt = document.getElementById('resumenTotal'), tm = document.getElementById('totalMostrado');
if (ra) ra.innerHTML = adultos + ' <a class="edit-link" data-edit-step="1"><i class="fas fa-pencil-alt"></i></a>';
if (rn) rn.innerHTML = ninios + ' <a class="edit-link" data-edit-step="1"><i class="fas fa-pencil-alt"></i></a>';
if (rp) {
const adultText = isSpanish ? `adulto${adultos !== 1 ? 's' : ''}` : `adult${adultos !== 1 ? 's' : ''}`;
const childText = isSpanish ? `niño${ninios !== 1 ? 's' : ''}` : `child${ninios !== 1 ? 'ren' : ''}`;
rp.textContent = `${adultos} ${adultText}${ninios > 0 ? `, ${ninios} ${childText}` : ''}`;
}
if (sv) sv.textContent = simbolo + subtotalVuelo.toLocaleString('es-MX');
if (es) {
if (serviciosSeleccionados.length === 0 || serviciosSeleccionados.every(s => s.cantidad === 0))
es.innerHTML = `<div class="extra-item" style="color:var(--gray-400); justify-content:center;">${isSpanish ? 'No hay extras seleccionados' : 'No extras selected'}</div>`;
else {
let html = '';
serviciosSeleccionados.forEach(s => {
if (s.cantidad > 0) html += `<div class="extra-item"><span>${s.cantidad}x ${s.descripcion.substring(0, 30)}${s.descripcion.length > 30 ? '...' : ''}</span><span>${simbolo}${(s.cantidad * s.precio).toLocaleString('es-MX')}</span></div>`;
});
es.innerHTML = html;
}
}
const totalFormateado = simbolo + totalGeneral.toLocaleString('es-MX');
if (rt) rt.textContent = totalFormateado;
if (tm) tm.textContent = totalFormateado;
guardarDatosEnLocalStorage();
}
function syncServiciosDesdeVisores() {
const displays = document.querySelectorAll('#step-3 .service-qty-display');
const nuevos = [];
displays.forEach(span => {
const id = parseInt(span.dataset.serviceId);
const cantidad = parseInt(span.textContent) || 0;
if (!isServicioIncluidoEnPaquete(id) && cantidad > 0) {
const row = span.closest('tr');
if (row) {
const precio = parseFloat(row.dataset.price);
const descripcion = row.dataset.description;
nuevos.push({ id, cantidad, precio, descripcion });
}
}
});
serviciosSeleccionados = nuevos;
window.serviciosSeleccionados = nuevos;
actualizarResumen();
}
function showError(id, mensaje) {
const el = document.getElementById(id);
if (el) { el.textContent = mensaje; el.classList.add('show'); }
}
function clearErrors() { document.querySelectorAll('.error-message').forEach(el => el.classList.remove('show')); }
function validarStep1() {
clearErrors();
let valido = true;
if (!fechaSeleccionada && !fechaLibre) { showError('fecha-error', isSpanish ? 'Selecciona una fecha o marca "Fecha por confirmar"' : 'Select a date or check "Date to be confirmed"'); valido = false; }
if (adultos < 1) { showError('fecha-error', isSpanish ? 'Debe haber al menos 1 adulto' : 'There must be at least 1 adult'); valido = false; }
return valido;
}
function validarStep2() {
clearErrors();
let valido = true;
const nombres = document.getElementById('resp_nombres')?.value.trim();
const apellidos = document.getElementById('resp_apellidos')?.value.trim();
const email = document.getElementById('resp_email')?.value.trim();
const telefonoRaw = document.getElementById('resp_telefono')?.value.trim();
if (!nombres) { showError('resp_nombres-error', isSpanish ? 'Nombres requeridos' : 'First names required'); valido = false; }
if (!apellidos) { showError('resp_apellidos-error', isSpanish ? 'Apellidos requeridos' : 'Last names required'); valido = false; }
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { showError('resp_email-error', isSpanish ? 'Correo inválido' : 'Invalid email'); valido = false; }
let telefonoValido = itiResponsable ? itiResponsable.isValidNumber() : (telefonoRaw?.replace(/\D/g, '').length >= 8);
if (!telefonoRaw || !telefonoValido) { showError('resp_telefono-error', isSpanish ? 'Teléfono inválido' : 'Invalid phone number'); valido = false; }
return valido;
}
function canGoToStep(targetStep) {
if (targetStep < currentStep) return true;
if (targetStep === 2) return validarStep1();
if (targetStep === 3) return validarStep1() && validarStep2();
if (targetStep === 4) return validarStep1() && validarStep2();
if (targetStep === 5) return validarStep1() && validarStep2() && (reservationId !== null);
return true;
}
function inicializarClipEnPaso4() {
if (clipCard) return;
const clipCheckoutDiv = document.getElementById('clip-checkout');
if (clipCheckoutDiv && PHP_VARS.clipApiKey && typeof window.ClipSDK !== 'undefined') {
try {
const clip = new window.ClipSDK(PHP_VARS.clipApiKey);
clipCard = clip.element.create("Card", {
theme: document.body.classList.contains('dark') ? "dark" : "light",
locale: isSpanish ? "es" : "en"
});
clipCard.mount("clip-checkout");
} catch (e) {
console.error('Clip init error:', e);
const errDiv = document.getElementById('clip-error');
if (errDiv) errDiv.textContent = isSpanish ? 'Error al cargar el módulo de pago' : 'Error loading payment module';
}
}
}
function cambiarPaso(step) {
if (!canGoToStep(step)) {
if (step === 2 && !validarStep1()) cambiarPaso(1);
else if ((step === 3 || step === 4) && !validarStep2()) cambiarPaso(2);
return;
}
document.querySelectorAll('.step-content').forEach((content, idx) => { content.style.display = (idx + 1 === step) ? 'block' : 'none'; });
document.querySelectorAll('.step-item').forEach((item, idx) => {
const num = idx + 1;
if (num === step) item.classList.add('active'); else item.classList.remove('active');
if (num < step) item.classList.add('completed'); else item.classList.remove('completed');
});
const gallery = document.getElementById('gallerySection');
if (gallery) gallery.classList.toggle('hide-gallery', step > 1);
if (step === 3) { setupExtraButtons(); syncServiciosDesdeVisores(); }
if (step === 4) {
inicializarClipEnPaso4();
const pe = document.getElementById('payment_email'), pp = document.getElementById('payment_phone');
const re = document.getElementById('resp_email'), rp = document.getElementById('resp_telefono');
if (pe && re) pe.value = re.value;
if (pp && rp) pp.value = rp.value;
}
if (step === 5 && !passengerDataSaved) {
renderPassengersStep5(document.getElementById('passengers-step5-container'), adultos, ninios, []);
}
currentStep = step;
document.querySelector('.main-card')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function handleIncrementDecrement(e) {
const btn = e.currentTarget;
const serviceId = parseInt(btn.dataset.serviceId);
if (isServicioIncluidoEnPaquete(serviceId)) {
Swal.fire({ icon: 'warning', title: isSpanish ? 'Servicio incluido' : 'Service included', text: isSpanish ? 'Este servicio ya viene incluido en tu paquete.' : 'This service is already included in your package.', timer: 2000, showConfirmButton: false });
return;
}
const display = document.querySelector(`.service-qty-display[data-service-id="${serviceId}"]`);
if (!display) return;
let currentQty = parseInt(display.textContent) || 0;
if (btn.classList.contains('btn-increment')) currentQty++;
else if (btn.classList.contains('btn-decrement')) currentQty = Math.max(0, currentQty - 1);
display.textContent = currentQty;
syncServiciosDesdeVisores();
}
function setupExtraButtons() {
document.querySelectorAll('#step-3 .btn-increment, #step-3 .btn-decrement').forEach(btn => {
btn.removeEventListener('click', handleIncrementDecrement);
btn.addEventListener('click', handleIncrementDecrement);
});
}
function getReservationData(cardToken = null) {
return {
id_vuelo: PHP_VARS.flightId,
fecha: fechaLibre ? null : fechaSeleccionada,
adultos, ninios,
responsable: {
nombres: capitalizeWords(document.getElementById('resp_nombres')?.value.trim() || ''),
apellidos: capitalizeWords(document.getElementById('resp_apellidos')?.value.trim() || ''),
email: document.getElementById('resp_email')?.value.trim(),
telefono: itiResponsable ? itiResponsable.getNumber() : document.getElementById('resp_telefono')?.value.trim()
},
servicios: serviciosSeleccionados.filter(s => s.cantidad > 0).map(s => ({ id: s.id, cantidad: s.cantidad })),
total: getTotalGeneral(),
cupon: document.getElementById('cuponCode')?.value || '',
_token: PHP_VARS.csrfToken,
...(cardToken && { card_token: cardToken })
};
}
async function enviarACotizacion(reservationData, paymentResponse) {
try {
const cotizacionPayload = { ...reservationData, reservation_id: reservationId, payment_id: paymentResponse.payment_id || null, payment_status: paymentResponse.payment_status, payment_email: document.getElementById('payment_email')?.value.trim(), payment_phone: document.getElementById('payment_phone')?.value.trim(), payment_description: document.getElementById('payment_description')?.value.trim(), fecha_vuelo: reservationData.fecha ? new Date(reservationData.fecha).toISOString() : null, created_at: new Date().toISOString() };
await axios.post(PHP_VARS.routeCotizaVuelo, cotizacionPayload, { headers: { 'X-CSRF-TOKEN': PHP_VARS.csrfToken } });
} catch (error) { console.error('Error al registrar cotización:', error); }
}
function guardarDatosEnLocalStorage() {
let pasajerosGuardados = [];
if (currentStep === 5 && !passengerDataSaved) {
const total = adultos + ninios;
for (let i = 0; i < total; i++) {
const nombres = document.querySelector(`#passengers-step5-container .passenger-nombres[data-pax="${i}"]`)?.value;
const apellidos = document.querySelector(`#passengers-step5-container .passenger-apellidos[data-pax="${i}"]`)?.value;
const dia = document.querySelector(`#passengers-step5-container .passenger-day[data-pax="${i}"]`)?.value;
const mes = document.querySelector(`#passengers-step5-container .passenger-month[data-pax="${i}"]`)?.value;
const año = document.querySelector(`#passengers-step5-container .passenger-year[data-pax="${i}"]`)?.value;
const peso = document.querySelector(`#passengers-step5-container .passenger-peso[data-pax="${i}"]`)?.value;
if (nombres && apellidos && dia && mes && año && peso) pasajerosGuardados.push({ nombres, apellidos, dia, mes, año, peso });
}
}
const datos = {
step1: { fecha_vuelo: window.fechaSeleccionada || '', fechaLibre: window.fechaLibre || false, adultos: window.adultos || 1, ninios: window.ninios || 0 },
step2: { responsable: { nombres: document.getElementById('resp_nombres')?.value || '', apellidos: document.getElementById('resp_apellidos')?.value || '', email: document.getElementById('resp_email')?.value || '', telefono: document.getElementById('resp_telefono')?.value || '' } },
step3: { extras_seleccionados: window.serviciosSeleccionados || [] },
step4: { payment_email: document.getElementById('payment_email')?.value || '', payment_phone: document.getElementById('payment_phone')?.value || '', payment_description: document.getElementById('payment_description')?.value || '', cupon: document.getElementById('cuponCode')?.value || '' },
step5: { pasajeros: pasajerosGuardados, reservationId: reservationId }
};
localStorage.setItem('datosReservaVuelo', JSON.stringify(datos));
}
function cargarDatosDesdeLocalStorage() {
const datosGuardados = localStorage.getItem('datosReservaVuelo');
if (!datosGuardados) return false;
try {
const datos = JSON.parse(datosGuardados);
if (datos.step1) {
if (datos.step1.fecha_vuelo && document.getElementById('fecha_vuelo')) {
document.getElementById('fecha_vuelo').value = datos.step1.fecha_vuelo;
if (document.getElementById('fecha_vuelo_native')) document.getElementById('fecha_vuelo_native').value = datos.step1.fecha_vuelo;
window.fechaSeleccionada = datos.step1.fecha_vuelo;
fechaSeleccionada = datos.step1.fecha_vuelo;
}
if (datos.step1.fechaLibre !== undefined && document.getElementById('fechaLibreCheckbox')) {
document.getElementById('fechaLibreCheckbox').checked = datos.step1.fechaLibre;
window.fechaLibre = datos.step1.fechaLibre;
fechaLibre = datos.step1.fechaLibre;
const fi = document.getElementById('fecha_vuelo'), fn = document.getElementById('fecha_vuelo_native');
if (window.fechaLibre) { if (fi) fi.disabled = true; if (fn) fn.disabled = true; }
else { if (fi) fi.disabled = false; if (fn) fn.disabled = false; }
}
if (datos.step1.adultos !== undefined && document.getElementById('adultos')) {
document.getElementById('adultos').value = datos.step1.adultos;
window.adultos = datos.step1.adultos;
adultos = datos.step1.adultos;
}
if (datos.step1.ninios !== undefined && document.getElementById('ninios')) {
document.getElementById('ninios').value = datos.step1.ninios;
window.ninios = datos.step1.ninios;
ninios = datos.step1.ninios;
}
}
if (datos.step2 && datos.step2.responsable) {
const resp = datos.step2.responsable;
if (document.getElementById('resp_nombres')) document.getElementById('resp_nombres').value = capitalizeWords(resp.nombres || '');
if (document.getElementById('resp_apellidos')) document.getElementById('resp_apellidos').value = capitalizeWords(resp.apellidos || '');
if (document.getElementById('resp_email')) document.getElementById('resp_email').value = resp.email || '';
if (document.getElementById('resp_telefono')) document.getElementById('resp_telefono').value = resp.telefono || '';
}
if (datos.step3 && datos.step3.extras_seleccionados) window.extrasToLoad = datos.step3.extras_seleccionados;
if (datos.step4) {
if (document.getElementById('payment_email')) document.getElementById('payment_email').value = datos.step4.payment_email || '';
if (document.getElementById('payment_phone')) document.getElementById('payment_phone').value = datos.step4.payment_phone || '';
if (document.getElementById('payment_description') && datos.step4.payment_description) document.getElementById('payment_description').value = datos.step4.payment_description;
if (document.getElementById('cuponCode') && datos.step4.cupon) {
document.getElementById('cuponCode').value = datos.step4.cupon;
setTimeout(() => { if (window.aplicarCuponAuto) window.aplicarCuponAuto(datos.step4.cupon); }, 500);
}
}
if (datos.step5 && datos.step5.pasajeros && datos.step5.pasajeros.length) {
window.passengerDataToLoad = datos.step5.pasajeros;
reservationId = datos.step5.reservationId;
}
setTimeout(() => {
actualizarResumen();
actualizarFechaResumen();
if (currentStep === 3 && window.extrasToLoad) {
setTimeout(() => {
window.extrasToLoad.forEach(extra => {
const display = document.querySelector(`.service-qty-display[data-service-id="${extra.id}"]`);
if (display) display.textContent = extra.cantidad;
});
syncServiciosDesdeVisores();
window.extrasToLoad = null;
}, 200);
}
if (currentStep === 5 && window.passengerDataToLoad) renderPassengersStep5(document.getElementById('passengers-step5-container'), adultos, ninios, window.passengerDataToLoad);
}, 100);
return true;
} catch (e) { return false; }
}
function setupAutoSave() {
setInterval(() => { if (document.visibilityState === 'visible') guardarDatosEnLocalStorage(); }, 30000);
window.addEventListener('beforeunload', () => guardarDatosEnLocalStorage());
const inputs = ['#fecha_vuelo', '#fecha_vuelo_native', '#fechaLibreCheckbox', '#adultos', '#ninios', '#resp_nombres', '#resp_apellidos', '#resp_email', '#resp_telefono', '#payment_email', '#payment_phone', '#cuponCode'];
inputs.forEach(sel => {
const el = document.querySelector(sel);
if (el) {
el.addEventListener('change', () => guardarDatosEnLocalStorage());
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', () => guardarDatosEnLocalStorage());
}
});
}
const respTel = document.getElementById('resp_telefono');
if (respTel) {
itiResponsable = window.intlTelInput(respTel, {
initialCountry: "mx",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js",
preferredCountries: ['mx', 'us', 'es'],
separateDialCode: true
});
}
const clipForm = document.getElementById('clip-payment-form');
if (clipForm) {
clipForm.addEventListener('submit', async function (e) {
e.preventDefault();
if (!validarStep1()) { cambiarPaso(1); return; }
if (!validarStep2()) { cambiarPaso(2); return; }
const paymentEmail = document.getElementById('payment_email');
const paymentPhone = document.getElementById('payment_phone');
if (!paymentEmail.value.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(paymentEmail.value.trim())) {
Swal.fire({ icon: 'error', title: isSpanish ? 'Error' : 'Error', text: isSpanish ? 'Ingresa un correo electrónico válido' : 'Enter a valid email', timer: 2000, showConfirmButton: false });
return;
}
if (!paymentPhone.value.trim()) {
Swal.fire({ icon: 'error', title: isSpanish ? 'Error' : 'Error', text: isSpanish ? 'Ingresa un número de teléfono' : 'Enter a phone number', timer: 2000, showConfirmButton: false });
return;
}
const btnPagar = document.getElementById('btnPagar');
btnPagar.disabled = true;
btnPagar.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> ' + (isSpanish ? 'Procesando pago...' : 'Processing...');
try {
if (!clipCard) throw new Error(isSpanish ? 'Módulo de pago no inicializado' : 'Payment module not initialized');
const cardToken = await clipCard.cardToken();
const reservationData = getReservationData(cardToken.id);
reservationData.payment_email = paymentEmail.value.trim();
reservationData.payment_phone = paymentPhone.value.trim();
reservationData.payment_description = document.getElementById('payment_description')?.value.trim() || '';
const response = await axios.post(PHP_VARS.routeCheckoutProcesar, reservationData);
if (response.data.success && response.data.payment_status === 'approved') {
reservationId = response.data.reservation_id;
await enviarACotizacion(reservationData, response.data);
guardarDatosEnLocalStorage();
passengerDataSaved = false;
cambiarPaso(5);
Swal.fire({ icon: 'success', title: isSpanish ? 'Pago aprobado' : 'Payment approved', text: isSpanish ? 'Ahora completa los datos de los pasajeros' : 'Now complete passenger details', timer: 2000, showConfirmButton: false });
} else {
throw new Error(response.data.message || (isSpanish ? 'Error en el pago' : 'Payment error'));
}
} catch (error) {
Swal.fire({ icon: 'error', title: isSpanish ? 'Error' : 'Error', text: error.message });
} finally {
btnPagar.disabled = false;
btnPagar.innerHTML = '<i class="fas fa-lock"></i> ' + (isSpanish ? 'Pagar ahora' : 'Pay now');
}
});
}
document.getElementById('savePassengersBtn')?.addEventListener('click', async () => {
const pasajeros = collectPassengerDataFromStep5(adultos, ninios);
if (!pasajeros) {
Swal.fire({ icon: 'error', title: isSpanish ? 'Datos incompletos' : 'Incomplete data', text: isSpanish ? 'Por favor complete todos los campos de los pasajeros' : 'Please fill all passenger fields' });
return;
}
Swal.fire({ title: isSpanish ? 'Generando pase de abordar...' : 'Generating boarding pass...', text: isSpanish ? 'Por favor espere, estamos procesando su información.' : 'Please wait, we are processing your information.', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } });
try {
const responsableData = {
nombres: capitalizeWords(document.getElementById('resp_nombres')?.value.trim() || ''),
apellidos: capitalizeWords(document.getElementById('resp_apellidos')?.value.trim() || ''),
email: document.getElementById('resp_email')?.value.trim() || '',
telefono: itiResponsable ? itiResponsable.getNumber() : document.getElementById('resp_telefono')?.value.trim() || ''
};
const payload = {
reservation_id: reservationId,
_token: PHP_VARS.csrfToken,
id_vuelo: PHP_VARS.flightId,
vuelo_nombre: PHP_VARS.flightName,
vuelo_descripcion: PHP_VARS.flightShortDesc,
vuelo_hora: PHP_VARS.flightTime,
fecha_vuelo: fechaLibre ? null : fechaSeleccionada,
fecha_libre: fechaLibre,
adultos, ninios,
responsable: responsableData,
pasajeros: pasajeros,
servicios: serviciosSeleccionados.filter(s => s.cantidad > 0 && !isServicioIncluidoEnPaquete(s.id)).map(s => ({ id: s.id, cantidad: s.cantidad, precio: s.precio, descripcion: s.descripcion })),
subtotal_vuelo: getSubtotalVuelo(),
total_extras: getTotalExtras(),
descuento: descuentoCupon,
total: getTotalGeneral(),
payment_email: document.getElementById('payment_email')?.value.trim() || '',
payment_phone: document.getElementById('payment_phone')?.value.trim() || '',
payment_description: document.getElementById('payment_description')?.value.trim() || '',
moneda: PHP_VARS.moneda,
simbolo: PHP_VARS.simbolo,
categoria: PHP_VARS.categoria?.nombre || '',
incluye: PHP_VARS.incluye || [],
rating: PHP_VARS.ratingGlobal,
highlighted_text: PHP_VARS.highlightedText || '',
highlighted_bg_color: PHP_VARS.highlightedBgColor || ''
};
const pdfResponse = await axios.post(PHP_VARS.routePaseAbordar, payload, { responseType: 'blob', headers: { 'Content-Type': 'application/json', 'Accept': 'application/pdf' } });
Swal.close();
if (pdfResponse.data.type === 'application/json') {
const errorText = await pdfResponse.data.text();
const errorJson = JSON.parse(errorText);
throw new Error(errorJson.message || (isSpanish ? 'Error al generar el pase de abordar' : 'Error generating boarding pass'));
}
const blobUrl = window.URL.createObjectURL(pdfResponse.data);
const link = document.createElement('a');
link.href = blobUrl;
link.setAttribute('download', `pase_abordar_${reservationId}_${Date.now()}.pdf`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(blobUrl);
passengerDataSaved = true;
guardarDatosEnLocalStorage();
const imprimirContainer = document.getElementById('imprimirContainer');
if (imprimirContainer) {
imprimirContainer.innerHTML = `<button class="btn-imprimir" onclick="window.print()"><i class="fas fa-print"></i> ${isSpanish ? 'Imprimir pase de abordar' : 'Print boarding pass'}</button><button class="btn-primary" style="margin-top: 0.5rem; width: 100%;" onclick="window.location.reload()"><i class="fas fa-redo"></i> ${isSpanish ? 'Nueva reserva' : 'New booking'}</button>`;
}
Swal.fire({ icon: 'success', title: isSpanish ? '¡Reserva completada!' : 'Booking completed!', text: isSpanish ? 'El pase de abordar se ha descargado automáticamente. También lo recibirás por correo electrónico.' : 'The boarding pass has been downloaded automatically. You will also receive it by email.', timer: 4000, showConfirmButton: true, confirmButtonText: isSpanish ? 'Entendido' : 'Understood' });
} catch (error) {
Swal.close();
console.error('Error al generar pase de abordar:', error);
let errorMessage = error.message || (isSpanish ? 'No se pudo generar el pase de abordar' : 'Could not generate boarding pass');
if (error.response?.data instanceof Blob && error.response.data.type === 'application/json') {
try {
const text = await error.response.data.text();
const jsonError = JSON.parse(text);
errorMessage = jsonError.message || errorMessage;
} catch (e) { }
}
Swal.fire({ icon: 'error', title: isSpanish ? 'Error' : 'Error', text: errorMessage, confirmButtonText: isSpanish ? 'Intentar de nuevo' : 'Try again', showCancelButton: true, cancelButtonText: isSpanish ? 'Cancelar' : 'Cancel' }).then((result) => { if (result.isConfirmed) document.getElementById('savePassengersBtn')?.click(); });
}
});
document.getElementById('backFromStep5Btn')?.addEventListener('click', () => { cambiarPaso(4); });
let flatpickrInstance = null;
const fechaInput = document.getElementById('fecha_vuelo'), fechaNative = document.getElementById('fecha_vuelo_native');
if (fechaInput) {
flatpickrInstance = flatpickr(fechaInput, {
minDate: "today",
dateFormat: "Y-m-d",
altInput: true,
altFormat: "j F, Y",
locale: isSpanish ? "es" : "default",
disableMobile: true,
onChange: (dates, dateStr) => { fechaSeleccionada = dateStr; window.fechaSeleccionada = dateStr; if (fechaNative) fechaNative.value = dateStr; fechaLibre = false; window.fechaLibre = false; if (document.getElementById('fechaLibreCheckbox')) document.getElementById('fechaLibreCheckbox').checked = false; actualizarFechaResumen(); actualizarResumen(); const fe = document.getElementById('fecha-error'); if (fe) fe.classList.remove('show'); }
});
}
if (fechaNative) {
fechaNative.addEventListener('change', function (e) { fechaSeleccionada = e.target.value; window.fechaSeleccionada = e.target.value; if (flatpickrInstance) flatpickrInstance.setDate(fechaSeleccionada); fechaLibre = false; window.fechaLibre = false; if (document.getElementById('fechaLibreCheckbox')) document.getElementById('fechaLibreCheckbox').checked = false; actualizarFechaResumen(); actualizarResumen(); const fe = document.getElementById('fecha-error'); if (fe) fe.classList.remove('show'); });
}
function detectMobileAndShowInput() { if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && fechaNative && fechaInput) { fechaInput.style.display = 'none'; fechaNative.style.display = 'block'; } }
detectMobileAndShowInput();
const fechaLibreCB = document.getElementById('fechaLibreCheckbox');
if (fechaLibreCB) {
fechaLibreCB.addEventListener('change', (e) => { fechaLibre = e.target.checked; window.fechaLibre = fechaLibre; if (fechaLibre) { if (fechaInput) fechaInput.disabled = true; if (fechaNative) fechaNative.disabled = true; if (flatpickrInstance) flatpickrInstance.clear(); fechaSeleccionada = ''; window.fechaSeleccionada = ''; } else { if (fechaInput) fechaInput.disabled = false; if (fechaNative) fechaNative.disabled = false; } actualizarFechaResumen(); actualizarResumen(); const fe = document.getElementById('fecha-error'); if (fe) fe.classList.remove('show'); });
}
const adultosInput = document.getElementById('adultos'), niniosInput = document.getElementById('ninios');
const minusAdult = document.getElementById('minusAdult'), plusAdult = document.getElementById('plusAdult'), minusChild = document.getElementById('minusChild'), plusChild = document.getElementById('plusChild');
function actualizarContadores() { adultos = parseInt(adultosInput.value) || 1; ninios = parseInt(niniosInput.value) || 0; if (adultos < 1) adultos = 1; if (ninios < 0) ninios = 0; window.adultos = adultos; window.ninios = ninios; actualizarResumen(); actualizarFechaResumen(); }
if (adultosInput) adultosInput.addEventListener('change', actualizarContadores);
if (niniosInput) niniosInput.addEventListener('change', actualizarContadores);
if (minusAdult) minusAdult.addEventListener('click', () => { if (adultos > 1) { adultos--; adultosInput.value = adultos; actualizarContadores(); } });
if (plusAdult) plusAdult.addEventListener('click', () => { if (adultos < 10) { adultos++; adultosInput.value = adultos; actualizarContadores(); } });
if (minusChild) minusChild.addEventListener('click', () => { if (ninios > 0) { ninios--; niniosInput.value = ninios; actualizarContadores(); } });
if (plusChild) plusChild.addEventListener('click', () => { if (ninios < 10) { ninios++; niniosInput.value = ninios; actualizarContadores(); } });
document.querySelectorAll('.next-step').forEach(btn => { btn.addEventListener('click', () => { const nextStep = parseInt(btn.dataset.next); cambiarPaso(nextStep); }); });
document.querySelectorAll('.prev-step').forEach(btn => { const prevStep = parseInt(btn.dataset.prev); if (prevStep) cambiarPaso(prevStep); });
document.querySelectorAll('.step-item').forEach(item => { item.addEventListener('click', () => { const step = parseInt(item.dataset.step); if (step && canGoToStep(step)) cambiarPaso(step); else if (step > 1) Swal.fire({ icon: 'info', title: isSpanish ? 'Completa los pasos anteriores' : 'Complete previous steps', text: isSpanish ? 'Debes llenar la información requerida antes de avanzar.' : 'You must complete the required information before proceeding.', timer: 2000, showConfirmButton: false }); else if (step === 1) cambiarPaso(1); }); });
document.querySelectorAll('.edit-link').forEach(link => { link.addEventListener('click', (e) => { e.stopPropagation(); const step = parseInt(link.dataset.editStep); if (step) cambiarPaso(step); }); });
const toggleCupon = document.getElementById('toggleCupon'), cuponDiv = document.getElementById('cuponInput');
if (toggleCupon && cuponDiv) {
toggleCupon.addEventListener('click', () => { cuponDiv.style.display = cuponDiv.style.display === 'block' ? 'none' : 'block'; });
document.getElementById('aplicarCupon')?.addEventListener('click', () => { const codigo = document.getElementById('cuponCode').value.trim(); if (window.aplicarCuponAuto(codigo)) { Swal.fire({ icon: 'success', title: isSpanish ? 'Cupón aplicado' : 'Coupon applied', text: isSpanish ? '10% de descuento' : '10% discount', timer: 2000, showConfirmButton: false }); } else { Swal.fire({ icon: 'error', title: isSpanish ? 'Cupón inválido' : 'Invalid coupon', text: isSpanish ? 'El código no es válido' : 'The code is invalid', timer: 2000, showConfirmButton: false }); } });
}
setupNameCapitalization(document.getElementById('resp_nombres'));
setupNameCapitalization(document.getElementById('resp_apellidos'));
if (document.querySelector('.main-swiper')) {
const mainSwiper = new Swiper('.main-swiper', { loop: true, pagination: { el: '.swiper-pagination', clickable: true }, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }, autoplay: { delay: 5000, disableOnInteraction: false } });
if (document.querySelector('.thumbs-swiper')) { new Swiper('.thumbs-swiper', { loop: false, spaceBetween: 10, slidesPerView: 'auto', freeMode: true, watchSlidesProgress: true, thumbs: { swiper: mainSwiper } }); }
}
actualizarResumen();
actualizarFechaResumen();
cargarDatosDesdeLocalStorage();
setupAutoSave();
});
</script>
@endpush