|
|
@@ -0,0 +1,1576 @@
|
|
|
+<template>
|
|
|
+ <div class="reservation-page">
|
|
|
+ <!-- Header avec navigation -->
|
|
|
+ <header class="reservation-header">
|
|
|
+ <div class="container">
|
|
|
+ <div class="header-content">
|
|
|
+ <button @click="goBack" class="back-button" :disabled="goBackLoading">
|
|
|
+ <i v-if="!goBackLoading" class="fa fa-arrow-left"></i>
|
|
|
+ <i v-else class="fa fa-spinner fa-spin"></i>
|
|
|
+ {{ goBackLoading ? 'Chargement...' : 'Retour' }}
|
|
|
+ </button>
|
|
|
+ <h1>Réservation de trajet</h1>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="container">
|
|
|
+ <div class="reservation-content">
|
|
|
+ <!-- Informations du trajet -->
|
|
|
+ <div class="trip-info-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h2><i class="fa fa-route"></i> Informations du trajet</h2>
|
|
|
+ </div>
|
|
|
+ <div class="trip-card">
|
|
|
+ <div class="trip-route">
|
|
|
+ <div class="route-point departure">
|
|
|
+ <div class="point-icon">
|
|
|
+ <i class="fa fa-map-marker-alt"></i>
|
|
|
+ </div>
|
|
|
+ <div class="point-info">
|
|
|
+ <h3>{{ departureStation?.name || 'Station de départ' }}</h3>
|
|
|
+ <p>{{ departureStation?.address || 'Adresse non disponible' }}</p>
|
|
|
+ <span class="time">{{ formatTime(trip?.departAt) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="route-line">
|
|
|
+ <div class="line"></div>
|
|
|
+ <div class="duration">{{ calculateDuration() }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="route-point arrival">
|
|
|
+ <div class="point-icon">
|
|
|
+ <i class="fa fa-map-marker-alt"></i>
|
|
|
+ </div>
|
|
|
+ <div class="point-info">
|
|
|
+ <h3>{{ arrivalStation?.name || 'Station d\'arrivée' }}</h3>
|
|
|
+ <p>{{ arrivalStation?.address || 'Adresse non disponible' }}</p>
|
|
|
+ <span class="time">{{ formatTime(trip?.arriveAt) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="trip-details">
|
|
|
+ <div class="detail-item">
|
|
|
+ <i class="fa fa-calendar"></i>
|
|
|
+ <span>{{ formatDate(trip?.departAt) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <i class="fa fa-money-bill-wave"></i>
|
|
|
+ <span>{{ trip?.pricePerSeat || 0 }} CFA / place</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-item">
|
|
|
+ <i class="fa fa-users"></i>
|
|
|
+ <span>{{ trip?.availableSeats || 0 }} places disponibles</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Informations de la compagnie -->
|
|
|
+ <div class="company-info-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h2><i class="fa fa-building"></i> Informations de la compagnie</h2>
|
|
|
+ </div>
|
|
|
+ <div class="company-card">
|
|
|
+ <div class="company-logo">
|
|
|
+ <img v-if="company?.logo" :src="company.logo" :alt="company.name" />
|
|
|
+ <div v-else class="logo-placeholder">
|
|
|
+ <i class="fa fa-building"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="company-details">
|
|
|
+ <h3>{{ company?.name || 'Compagnie de transport' }}</h3>
|
|
|
+ <p v-if="company?.description">{{ company.description }}</p>
|
|
|
+ <div class="company-contact">
|
|
|
+ <div v-if="company?.phone" class="contact-item">
|
|
|
+ <i class="fa fa-phone"></i>
|
|
|
+ <span>{{ company.phone }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="company?.email" class="contact-item">
|
|
|
+ <i class="fa fa-envelope"></i>
|
|
|
+ <span>{{ company.email }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="company?.address" class="contact-item">
|
|
|
+ <i class="fa fa-map-marker-alt"></i>
|
|
|
+ <span>{{ company.address }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Carte des stations -->
|
|
|
+ <div class="map-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h2><i class="fa fa-map"></i> Localisation des stations</h2>
|
|
|
+ </div>
|
|
|
+ <div class="map-container">
|
|
|
+ <div id="map" class="map"></div>
|
|
|
+ <div class="map-legend">
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="legend-marker departure"></div>
|
|
|
+ <span>Station de départ</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="legend-marker arrival"></div>
|
|
|
+ <span>Station d'arrivée</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Formulaire de réservation -->
|
|
|
+ <div class="reservation-form-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h2><i class="fa fa-ticket-alt"></i> Détails de la réservation</h2>
|
|
|
+ </div>
|
|
|
+ <form @submit.prevent="submitReservation" class="reservation-form">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="passenger-count">Nombre de passagers</label>
|
|
|
+ <select id="passenger-count" v-model="reservationData.passengerCount" required>
|
|
|
+ <option value="">Sélectionnez le nombre de passagers</option>
|
|
|
+ <option v-for="n in Math.min(trip?.availableSeats || 1, 10)" :key="n" :value="n">
|
|
|
+ {{ n }} passager{{ n > 1 ? 's' : '' }}
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="passengers-info" v-if="reservationData.passengerCount">
|
|
|
+ <h3>Informations des passagers</h3>
|
|
|
+ <div v-for="(passenger, index) in passengers" :key="index" class="passenger-form">
|
|
|
+ <h4>Passager {{ index + 1 }}</h4>
|
|
|
+ <div class="form-row">
|
|
|
+ <div class="form-group">
|
|
|
+ <label :for="`firstName-${index}`">Prénom</label>
|
|
|
+ <input
|
|
|
+ :id="`firstName-${index}`"
|
|
|
+ type="text"
|
|
|
+ v-model="passenger.firstName"
|
|
|
+ required
|
|
|
+ placeholder="Prénom"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label :for="`lastName-${index}`">Nom</label>
|
|
|
+ <input
|
|
|
+ :id="`lastName-${index}`"
|
|
|
+ type="text"
|
|
|
+ v-model="passenger.lastName"
|
|
|
+ required
|
|
|
+ placeholder="Nom"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="form-row">
|
|
|
+ <div class="form-group">
|
|
|
+ <label :for="`phone-${index}`">Téléphone</label>
|
|
|
+ <input
|
|
|
+ :id="`phone-${index}`"
|
|
|
+ type="tel"
|
|
|
+ v-model="passenger.phone"
|
|
|
+ required
|
|
|
+ placeholder="Numéro de téléphone"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label :for="`email-${index}`">Email (optionnel)</label>
|
|
|
+ <input
|
|
|
+ :id="`email-${index}`"
|
|
|
+ type="email"
|
|
|
+ v-model="passenger.email"
|
|
|
+ placeholder="Adresse email"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Résumé de la commande -->
|
|
|
+ <div class="order-summary">
|
|
|
+ <h3>Résumé de la commande</h3>
|
|
|
+ <div class="summary-item">
|
|
|
+ <span>Prix par place:</span>
|
|
|
+ <span>{{ trip?.pricePerSeat || 0 }} CFA</span>
|
|
|
+ </div>
|
|
|
+ <div class="summary-item">
|
|
|
+ <span>Nombre de places:</span>
|
|
|
+ <span>{{ reservationData.passengerCount || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="summary-item total">
|
|
|
+ <span>Total:</span>
|
|
|
+ <span>{{ calculateTotal() }} CFA</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Moyens de paiement -->
|
|
|
+ <div class="payment-section">
|
|
|
+ <h3><i class="fa fa-credit-card"></i> Moyen de paiement</h3>
|
|
|
+ <div class="payment-methods">
|
|
|
+ <div class="payment-method"
|
|
|
+ :class="{ active: reservationData.paymentMethod === 'card' }"
|
|
|
+ @click="reservationData.paymentMethod = 'card'">
|
|
|
+ <input type="radio" id="card" value="card" v-model="reservationData.paymentMethod" />
|
|
|
+ <label for="card">
|
|
|
+ <i class="fa fa-credit-card"></i>
|
|
|
+ <span>Carte bancaire</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <div class="payment-method"
|
|
|
+ :class="{ active: reservationData.paymentMethod === 'mobile' }"
|
|
|
+ @click="reservationData.paymentMethod = 'mobile'">
|
|
|
+ <input type="radio" id="mobile" value="mobile" v-model="reservationData.paymentMethod" />
|
|
|
+ <label for="mobile">
|
|
|
+ <i class="fa fa-mobile-alt"></i>
|
|
|
+ <span>Mobile Money</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <div class="payment-method"
|
|
|
+ :class="{ active: reservationData.paymentMethod === 'cash' }"
|
|
|
+ @click="reservationData.paymentMethod = 'cash'">
|
|
|
+ <input type="radio" id="cash" value="cash" v-model="reservationData.paymentMethod" />
|
|
|
+ <label for="cash">
|
|
|
+ <i class="fa fa-money-bill-wave"></i>
|
|
|
+ <span>Paiement à bord</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Message d'avertissement pour paiement à bord -->
|
|
|
+ <div v-if="reservationData.paymentMethod === 'cash'" class="payment-warning">
|
|
|
+ <div class="warning-content">
|
|
|
+ <i class="fa fa-exclamation-triangle"></i>
|
|
|
+ <div class="warning-text">
|
|
|
+ <strong>Attention :</strong> Le paiement à bord n'est pas prioritaire.
|
|
|
+ Les réservations avec paiement en ligne (carte bancaire ou Mobile Money)
|
|
|
+ sont traitées en priorité. Vous risquez de perdre votre place si un autre
|
|
|
+ passager réserve en ligne avant vous.
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Formulaire de paiement Mobile Money -->
|
|
|
+ <div v-if="reservationData.paymentMethod === 'mobile'" class="mobile-payment-form">
|
|
|
+ <h3><i class="fa fa-mobile-alt"></i> Choisissez votre opérateur Mobile Money</h3>
|
|
|
+ <div class="mobile-operators">
|
|
|
+ <div class="operator-option"
|
|
|
+ :class="{ active: mobileData.operator === 'orange' }"
|
|
|
+ @click="mobileData.operator = 'orange'">
|
|
|
+ <input type="radio" id="orange" value="orange" v-model="mobileData.operator" />
|
|
|
+ <label for="orange" class="operator-label">
|
|
|
+ <div class="operator-logo orange-logo">
|
|
|
+ <svg viewBox="0 0 200 100" class="logo-svg">
|
|
|
+ <rect width="200" height="100" fill="#FF6600" rx="10"/>
|
|
|
+ <text x="100" y="35" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="16" font-weight="bold">Orange</text>
|
|
|
+ <text x="100" y="55" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="14">Money</text>
|
|
|
+ <path d="M30 70 L50 70 M40 60 L40 80" stroke="white" stroke-width="2" fill="none"/>
|
|
|
+ <circle cx="160" cy="70" r="8" fill="white"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <span class="operator-name">Orange Money</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="operator-option"
|
|
|
+ :class="{ active: mobileData.operator === 'mtn' }"
|
|
|
+ @click="mobileData.operator = 'mtn'">
|
|
|
+ <input type="radio" id="mtn" value="mtn" v-model="mobileData.operator" />
|
|
|
+ <label for="mtn" class="operator-label">
|
|
|
+ <div class="operator-logo mtn-logo">
|
|
|
+ <svg viewBox="0 0 200 100" class="logo-svg">
|
|
|
+ <rect width="200" height="100" fill="#FFCC00" rx="10"/>
|
|
|
+ <rect x="10" y="10" width="180" height="80" fill="white" rx="5"/>
|
|
|
+ <ellipse cx="100" cy="50" rx="60" ry="25" fill="#004B87"/>
|
|
|
+ <text x="100" y="40" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="18" font-weight="bold">MTN</text>
|
|
|
+ <text x="100" y="58" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="10">Mobile Money</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <span class="operator-name">MTN Mobile Money</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="operator-option"
|
|
|
+ :class="{ active: mobileData.operator === 'wave' }"
|
|
|
+ @click="mobileData.operator = 'wave'">
|
|
|
+ <input type="radio" id="wave" value="wave" v-model="mobileData.operator" />
|
|
|
+ <label for="wave" class="operator-label">
|
|
|
+ <div class="operator-logo wave-logo">
|
|
|
+ <svg viewBox="0 0 200 100" class="logo-svg">
|
|
|
+ <circle cx="100" cy="50" r="45" fill="#00BFFF"/>
|
|
|
+ <circle cx="85" cy="35" r="3" fill="white"/>
|
|
|
+ <circle cx="95" cy="35" r="3" fill="white"/>
|
|
|
+ <ellipse cx="90" cy="45" rx="8" ry="6" fill="white"/>
|
|
|
+ <ellipse cx="90" cy="60" rx="15" ry="10" fill="white"/>
|
|
|
+ <path d="M75 65 Q85 70 95 65" stroke="#FF6600" stroke-width="2" fill="none"/>
|
|
|
+ <path d="M105 65 Q115 70 125 65" stroke="#FF6600" stroke-width="2" fill="none"/>
|
|
|
+ <text x="100" y="85" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="14" font-weight="bold">wave</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <span class="operator-name">Wave</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Formulaire de numéro de téléphone pour Mobile Money -->
|
|
|
+ <div v-if="mobileData.operator" class="mobile-form">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="mobile-number">Numéro de téléphone</label>
|
|
|
+ <input
|
|
|
+ id="mobile-number"
|
|
|
+ type="tel"
|
|
|
+ v-model="mobileData.phoneNumber"
|
|
|
+ :placeholder="getPhoneNumberPlaceholder()"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="mobile-info">
|
|
|
+ <i class="fa fa-info-circle"></i>
|
|
|
+ <span>Vous recevrez un SMS de confirmation pour valider le paiement</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Formulaire de paiement par carte -->
|
|
|
+ <div v-if="reservationData.paymentMethod === 'card'" class="card-payment-form">
|
|
|
+ <h3><i class="fa fa-credit-card"></i> Informations de paiement</h3>
|
|
|
+ <div class="card-form">
|
|
|
+ <div class="form-row">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="card-number">Numéro de carte</label>
|
|
|
+ <input
|
|
|
+ id="card-number"
|
|
|
+ type="text"
|
|
|
+ v-model="cardData.number"
|
|
|
+ placeholder="1234 5678 9012 3456"
|
|
|
+ maxlength="19"
|
|
|
+ @input="formatCardNumber"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="card-name">Nom sur la carte</label>
|
|
|
+ <input
|
|
|
+ id="card-name"
|
|
|
+ type="text"
|
|
|
+ v-model="cardData.name"
|
|
|
+ placeholder="JEAN DUPONT"
|
|
|
+ style="text-transform: uppercase;"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="form-row">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="card-expiry">Date d'expiration</label>
|
|
|
+ <input
|
|
|
+ id="card-expiry"
|
|
|
+ type="text"
|
|
|
+ v-model="cardData.expiry"
|
|
|
+ placeholder="MM/AA"
|
|
|
+ maxlength="5"
|
|
|
+ @input="formatExpiryDate"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="card-cvv">CVV</label>
|
|
|
+ <input
|
|
|
+ id="card-cvv"
|
|
|
+ type="text"
|
|
|
+ v-model="cardData.cvv"
|
|
|
+ placeholder="123"
|
|
|
+ maxlength="4"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="security-info">
|
|
|
+ <i class="fa fa-shield-alt"></i>
|
|
|
+ <span>Vos informations de paiement sont sécurisées et cryptées</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Bouton de soumission -->
|
|
|
+ <div class="form-actions">
|
|
|
+ <button type="button" @click="goBack" class="btn-secondary">
|
|
|
+ Annuler
|
|
|
+ </button>
|
|
|
+ <button type="submit" class="btn-primary" :disabled="!isFormValid">
|
|
|
+ <i class="fa fa-check"></i>
|
|
|
+ Confirmer la réservation
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
+import { useToast } from '@/composables/useToast'
|
|
|
+import { useStationStore } from '@/stores/useStationStore'
|
|
|
+import TripService from '@/services/TripService'
|
|
|
+import type { Trip, Station, Company } from '@/types'
|
|
|
+import 'leaflet/dist/leaflet.css'
|
|
|
+// @ts-ignore
|
|
|
+import * as L from 'leaflet'
|
|
|
+import CompanyService from '@/services/CompanyService'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const router = useRouter()
|
|
|
+const { success, error } = useToast()
|
|
|
+const stationStore = useStationStore()
|
|
|
+
|
|
|
+// État réactif
|
|
|
+const loading = ref(true)
|
|
|
+const goBackLoading = ref(false)
|
|
|
+const trip = ref<Trip | null>(null)
|
|
|
+const company = ref<Company | null>(null)
|
|
|
+const departureStation = ref<Station | null>(null)
|
|
|
+const arrivalStation = ref<Station | null>(null)
|
|
|
+const mapInstance = ref<L.Map | null>(null)
|
|
|
+
|
|
|
+// Données de réservation
|
|
|
+const reservationData = ref({
|
|
|
+ passengerCount: '',
|
|
|
+ paymentMethod: ''
|
|
|
+})
|
|
|
+
|
|
|
+// Données de carte bancaire
|
|
|
+const cardData = ref({
|
|
|
+ number: '',
|
|
|
+ name: '',
|
|
|
+ expiry: '',
|
|
|
+ cvv: ''
|
|
|
+})
|
|
|
+
|
|
|
+// Données Mobile Money
|
|
|
+const mobileData = ref({
|
|
|
+ operator: '',
|
|
|
+ phoneNumber: ''
|
|
|
+})
|
|
|
+
|
|
|
+const passengers = ref<Array<{
|
|
|
+ firstName: string
|
|
|
+ lastName: string
|
|
|
+ phone: string
|
|
|
+ email: string
|
|
|
+}>>([]);
|
|
|
+
|
|
|
+// Watchers
|
|
|
+watch(() => reservationData.value.passengerCount, (newCount) => {
|
|
|
+ const count = parseInt(newCount)
|
|
|
+ if (count > 0) {
|
|
|
+ passengers.value = Array.from({ length: count }, () => ({
|
|
|
+ firstName: '',
|
|
|
+ lastName: '',
|
|
|
+ phone: '',
|
|
|
+ email: ''
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ passengers.value = []
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// Computed
|
|
|
+const isFormValid = computed(() => {
|
|
|
+ if (!reservationData.value.passengerCount || !reservationData.value.paymentMethod) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ const passengersValid = passengers.value.every(passenger =>
|
|
|
+ passenger.firstName.trim() &&
|
|
|
+ passenger.lastName.trim() &&
|
|
|
+ passenger.phone.trim()
|
|
|
+ )
|
|
|
+
|
|
|
+ // Validation supplémentaire pour le paiement par carte
|
|
|
+ if (reservationData.value.paymentMethod === 'card') {
|
|
|
+ const cardValid = cardData.value.number.replace(/\s/g, '').length >= 13 &&
|
|
|
+ cardData.value.name.trim() &&
|
|
|
+ cardData.value.expiry.length === 5 &&
|
|
|
+ cardData.value.cvv.length >= 3
|
|
|
+ return passengersValid && cardValid
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validation supplémentaire pour Mobile Money
|
|
|
+ if (reservationData.value.paymentMethod === 'mobile') {
|
|
|
+ const mobileValid = mobileData.value.operator &&
|
|
|
+ mobileData.value.phoneNumber.trim().length >= 8
|
|
|
+ return passengersValid && mobileValid
|
|
|
+ }
|
|
|
+
|
|
|
+ return passengersValid
|
|
|
+})
|
|
|
+
|
|
|
+// Méthodes
|
|
|
+const goBack = () => {
|
|
|
+ goBackLoading.value = true
|
|
|
+ router.go(-1)
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (dateString: string | undefined) => {
|
|
|
+ if (!dateString) return 'Date non disponible'
|
|
|
+ const date = new Date(dateString)
|
|
|
+ return date.toLocaleDateString('fr-FR', {
|
|
|
+ day: '2-digit',
|
|
|
+ month: '2-digit',
|
|
|
+ year: 'numeric'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const formatTime = (dateString: string | undefined) => {
|
|
|
+ if (!dateString) return 'Heure non disponible'
|
|
|
+ const date = new Date(dateString)
|
|
|
+ return date.toLocaleTimeString('fr-FR', {
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const calculateDuration = () => {
|
|
|
+ if (!trip.value?.departAt || !trip.value?.arriveAt) return 'Durée inconnue'
|
|
|
+
|
|
|
+ const departure = new Date(trip.value.departAt)
|
|
|
+ const arrival = new Date(trip.value.arriveAt)
|
|
|
+ const duration = arrival.getTime() - departure.getTime()
|
|
|
+
|
|
|
+ const hours = Math.floor(duration / (1000 * 60 * 60))
|
|
|
+ const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60))
|
|
|
+
|
|
|
+ return `${hours}h${minutes.toString().padStart(2, '0')}`
|
|
|
+}
|
|
|
+
|
|
|
+const calculateTotal = () => {
|
|
|
+ const count = parseInt(reservationData.value.passengerCount) || 0
|
|
|
+ const price = trip.value?.pricePerSeat || 0
|
|
|
+ return count * price
|
|
|
+}
|
|
|
+
|
|
|
+// Fonctions de formatage pour la carte
|
|
|
+const formatCardNumber = (event: Event) => {
|
|
|
+ const input = event.target as HTMLInputElement
|
|
|
+ let value = input.value.replace(/\s/g, '').replace(/\D/g, '')
|
|
|
+ value = value.replace(/(\d{4})(?=\d)/g, '$1 ')
|
|
|
+ cardData.value.number = value
|
|
|
+}
|
|
|
+
|
|
|
+const formatExpiryDate = (event: Event) => {
|
|
|
+ const input = event.target as HTMLInputElement
|
|
|
+ let value = input.value.replace(/\D/g, '')
|
|
|
+ if (value.length >= 2) {
|
|
|
+ value = value.substring(0, 2) + '/' + value.substring(2, 4)
|
|
|
+ }
|
|
|
+ cardData.value.expiry = value
|
|
|
+}
|
|
|
+
|
|
|
+const getPhoneNumberPlaceholder = () => {
|
|
|
+ switch (mobileData.value.operator) {
|
|
|
+ case 'orange':
|
|
|
+ return '07 XX XX XX XX ou 01 XX XX XX XX'
|
|
|
+ case 'mtn':
|
|
|
+ return '05 XX XX XX XX ou 02 XX XX XX XX'
|
|
|
+ case 'wave':
|
|
|
+ return '01 XX XX XX XX'
|
|
|
+ default:
|
|
|
+ return 'Numéro de téléphone'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadTripData = async () => {
|
|
|
+ try {
|
|
|
+ loading.value = true
|
|
|
+ const tripId = route.params.id as string
|
|
|
+
|
|
|
+ if (!tripId) {
|
|
|
+ error('ID du trajet manquant')
|
|
|
+ router.push('/')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Charger les stations si pas déjà fait
|
|
|
+ if (stationStore.stations.length === 0) {
|
|
|
+ await stationStore.fetchStations()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Charger les détails du trajet
|
|
|
+ const tripResponse = await TripService.getTripById(parseInt(tripId))
|
|
|
+ trip.value = tripResponse.data
|
|
|
+ console.log('Trip', tripResponse.data)
|
|
|
+ // Trouver les stations de départ et d'arrivée
|
|
|
+ if (trip.value) {
|
|
|
+ departureStation.value = stationStore.stations.find(
|
|
|
+ station => station.id === trip.value!.departStationId
|
|
|
+ ) || null
|
|
|
+
|
|
|
+ arrivalStation.value = stationStore.stations.find(
|
|
|
+ station => station.id === trip.value!.arriveStationId
|
|
|
+ ) || null
|
|
|
+ console.log('trip.value.companyId', trip.value.companyId)
|
|
|
+ // Charger les informations de la compagnie si disponible
|
|
|
+ if (trip.value.companyId) {
|
|
|
+ // Ici vous devriez avoir un service pour charger les détails de la compagnie
|
|
|
+ const response = await CompanyService.getCompany(trip.value.companyId)
|
|
|
+ company.value = response?.data
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Initialiser la carte
|
|
|
+ initializeMap()
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Erreur lors du chargement des données:', err)
|
|
|
+ error('Erreur lors du chargement des informations du trajet')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const initializeMap = async () => {
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ if (!departureStation.value || !arrivalStation.value) {
|
|
|
+ console.warn('Stations de départ ou d\'arrivée manquantes')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Supprimer l'instance de carte existante si elle existe
|
|
|
+ if (mapInstance.value) {
|
|
|
+ mapInstance.value.remove()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Coordonnées par défaut pour les stations (vous devriez avoir ces données dans votre base)
|
|
|
+ const defaultCoords = {
|
|
|
+ [departureStation.value.name]: { lat: 5.348, lng: -4.007 }, // Abidjan par défaut
|
|
|
+ [arrivalStation.value.name]: { lat: 7.683, lng: -5.030 } // Bouaké par défaut
|
|
|
+ }
|
|
|
+
|
|
|
+ const departureCoords = {
|
|
|
+ lat: departureStation.value.lat || defaultCoords[departureStation.value.name]?.lat || 5.348,
|
|
|
+ lng: departureStation.value.long || defaultCoords[departureStation.value.name]?.lng || -4.007
|
|
|
+ }
|
|
|
+
|
|
|
+ const arrivalCoords = {
|
|
|
+ lat: arrivalStation.value.lat|| defaultCoords[arrivalStation.value.name]?.lat || 7.683,
|
|
|
+ lng: arrivalStation.value.long|| defaultCoords[arrivalStation.value.name]?.lng || -5.030
|
|
|
+ }
|
|
|
+
|
|
|
+ // Créer la carte
|
|
|
+ mapInstance.value = L.map('map').setView([departureCoords.lat, departureCoords.lng], 7)
|
|
|
+
|
|
|
+ // Ajouter les tuiles OpenStreetMap
|
|
|
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
|
+ attribution: '© OpenStreetMap contributors'
|
|
|
+ }).addTo(mapInstance.value)
|
|
|
+
|
|
|
+ // Créer des icônes personnalisées
|
|
|
+ const departureIcon = L.divIcon({
|
|
|
+ html: '<i class="fa fa-map-marker-alt" style="color: #4CAF50; font-size: 24px;"></i>',
|
|
|
+ iconSize: [30, 30],
|
|
|
+ className: 'custom-div-icon'
|
|
|
+ })
|
|
|
+
|
|
|
+ const arrivalIcon = L.divIcon({
|
|
|
+ html: '<i class="fa fa-map-marker-alt" style="color: #f44336; font-size: 24px;"></i>',
|
|
|
+ iconSize: [30, 30],
|
|
|
+ className: 'custom-div-icon'
|
|
|
+ })
|
|
|
+
|
|
|
+ // Ajouter les marqueurs
|
|
|
+ L.marker([departureCoords.lat, departureCoords.lng], { icon: departureIcon })
|
|
|
+ .addTo(mapInstance.value)
|
|
|
+ .bindPopup(`<strong>Départ:</strong> ${departureStation.value.name}`)
|
|
|
+
|
|
|
+ L.marker([arrivalCoords.lat, arrivalCoords.lng], { icon: arrivalIcon })
|
|
|
+ .addTo(mapInstance.value)
|
|
|
+ .bindPopup(`<strong>Arrivée:</strong> ${arrivalStation.value.name}`)
|
|
|
+
|
|
|
+ // Ajouter une ligne entre les deux points
|
|
|
+ L.polyline([
|
|
|
+ [departureCoords.lat, departureCoords.lng],
|
|
|
+ [arrivalCoords.lat, arrivalCoords.lng]
|
|
|
+ ], {
|
|
|
+ color: '#2196F3',
|
|
|
+ weight: 3,
|
|
|
+ opacity: 0.7
|
|
|
+ }).addTo(mapInstance.value)
|
|
|
+
|
|
|
+ // Ajuster la vue pour inclure les deux marqueurs
|
|
|
+ mapInstance.value.fitBounds([
|
|
|
+ [departureCoords.lat, departureCoords.lng],
|
|
|
+ [arrivalCoords.lat, arrivalCoords.lng]
|
|
|
+ ], { padding: [20, 20] })
|
|
|
+}
|
|
|
+
|
|
|
+const submitReservation = async () => {
|
|
|
+ try {
|
|
|
+ if (!isFormValid.value) {
|
|
|
+ error('Veuillez remplir tous les champs obligatoires')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const reservationPayload = {
|
|
|
+ tripId: trip.value?.id,
|
|
|
+ passengers: passengers.value,
|
|
|
+ paymentMethod: reservationData.value.paymentMethod,
|
|
|
+ totalAmount: calculateTotal(),
|
|
|
+ ...(reservationData.value.paymentMethod === 'card' && {
|
|
|
+ cardInfo: {
|
|
|
+ number: cardData.value.number.replace(/\s/g, ''),
|
|
|
+ name: cardData.value.name,
|
|
|
+ expiry: cardData.value.expiry,
|
|
|
+ cvv: cardData.value.cvv
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('Données de réservation:', reservationPayload)
|
|
|
+
|
|
|
+ // Ici vous devriez appeler votre service de réservation
|
|
|
+ // await ReservationService.createReservation(reservationPayload)
|
|
|
+
|
|
|
+ success('Réservation confirmée avec succès!')
|
|
|
+
|
|
|
+ // Rediriger vers une page de confirmation ou le dashboard
|
|
|
+ //router.push({ name: 'home' })
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Erreur lors de la réservation:', err)
|
|
|
+ error('Erreur lors de la confirmation de la réservation')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadTripData()
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ if (mapInstance.value) {
|
|
|
+ mapInstance.value.remove()
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* Styles pour Mobile Money */
|
|
|
+.mobile-payment-form {
|
|
|
+ margin-top: 2rem;
|
|
|
+ padding: 1.5rem;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ background-color: #f9fafb;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-operators {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+ gap: 1rem;
|
|
|
+ margin: 1rem 0;
|
|
|
+}
|
|
|
+
|
|
|
+.operator-option {
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.operator-option input[type="radio"] {
|
|
|
+ position: absolute;
|
|
|
+ opacity: 0;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.operator-label {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 1rem;
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
+ border-radius: 0.75rem;
|
|
|
+ background-color: white;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.operator-option:hover .operator-label {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.operator-option.active .operator-label {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ background-color: #eff6ff;
|
|
|
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25);
|
|
|
+}
|
|
|
+
|
|
|
+.operator-logo {
|
|
|
+ width: 120px;
|
|
|
+ height: 60px;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.logo-svg {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.operator-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #374151;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-form {
|
|
|
+ margin-top: 1.5rem;
|
|
|
+ padding: 1rem;
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+ margin-top: 0.75rem;
|
|
|
+ padding: 0.75rem;
|
|
|
+ background-color: #eff6ff;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ color: #1e40af;
|
|
|
+ font-size: 0.875rem;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-info i {
|
|
|
+ color: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+/* Responsive design pour mobile */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .mobile-operators {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .operator-logo {
|
|
|
+ width: 100px;
|
|
|
+ height: 50px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Styles existants... */
|
|
|
+.reservation-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.reservation-header {
|
|
|
+ background: linear-gradient(135deg, #4b8ef1 0%, #7a6ad8 100%);
|
|
|
+ color: white;
|
|
|
+ padding: 20px 0;
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.header-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.back-button {
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border: none;
|
|
|
+ color: white;
|
|
|
+ padding: 10px 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.back-button:hover:not(:disabled) {
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.back-button:disabled {
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+
|
|
|
+.back-button:disabled .fa-spinner {
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ from {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.container {
|
|
|
+ max-width: 1200px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 0 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.reservation-content {
|
|
|
+ padding: 30px 0;
|
|
|
+ display: grid;
|
|
|
+ gap: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-header {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-header h2 {
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-header h2 i {
|
|
|
+ color: #4b8ef1;
|
|
|
+}
|
|
|
+
|
|
|
+/* Trip Info Styles */
|
|
|
+.trip-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 25px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.trip-route {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 25px;
|
|
|
+}
|
|
|
+
|
|
|
+.route-point {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-icon {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.departure .point-icon {
|
|
|
+ background: #28a745;
|
|
|
+}
|
|
|
+
|
|
|
+.arrival .point-icon {
|
|
|
+ background: #dc3545;
|
|
|
+}
|
|
|
+
|
|
|
+.point-info h3 {
|
|
|
+ margin: 0 0 5px 0;
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-info p {
|
|
|
+ margin: 0 0 5px 0;
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.time {
|
|
|
+ background: #e9ecef;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #495057;
|
|
|
+}
|
|
|
+
|
|
|
+.route-line {
|
|
|
+ flex: 0 0 100px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ margin: 0 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.line {
|
|
|
+ width: 2px;
|
|
|
+ height: 40px;
|
|
|
+ background: linear-gradient(to bottom, #28a745, #dc3545);
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.duration {
|
|
|
+ background: #4b8ef1;
|
|
|
+ color: white;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.trip-details {
|
|
|
+ display: flex;
|
|
|
+ gap: 30px;
|
|
|
+ padding-top: 20px;
|
|
|
+ border-top: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #6c757d;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-item i {
|
|
|
+ color: #4b8ef1;
|
|
|
+}
|
|
|
+
|
|
|
+/* Company Info Styles */
|
|
|
+.company-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 25px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.company-logo {
|
|
|
+ flex: 0 0 80px;
|
|
|
+}
|
|
|
+
|
|
|
+.company-logo img {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 8px;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.logo-placeholder {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ background: #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.company-details h3 {
|
|
|
+ margin: 0 0 10px 0;
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.company-details p {
|
|
|
+ margin: 0 0 15px 0;
|
|
|
+ color: #6c757d;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.company-contact {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.contact-item i {
|
|
|
+ color: #4b8ef1;
|
|
|
+ width: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Map Styles */
|
|
|
+.map-container {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 25px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.map {
|
|
|
+ width: 100%;
|
|
|
+ height: 300px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-bottom: 15px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-div-icon {
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.leaflet-popup-content-wrapper {
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.leaflet-popup-content {
|
|
|
+ margin: 8px 12px;
|
|
|
+ font-family: inherit;
|
|
|
+}
|
|
|
+
|
|
|
+.static-map {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 20px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.map-marker {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ z-index: 2;
|
|
|
+}
|
|
|
+
|
|
|
+.departure-marker i {
|
|
|
+ color: #28a745;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.arrival-marker i {
|
|
|
+ color: #dc3545;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-marker span {
|
|
|
+ background: white;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.map-route {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 20%;
|
|
|
+ right: 20%;
|
|
|
+ height: 2px;
|
|
|
+ background: linear-gradient(to right, #28a745, #dc3545);
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.map-legend {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #6c757d;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-marker {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-marker.departure {
|
|
|
+ background: #28a745;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-marker.arrival {
|
|
|
+ background: #dc3545;
|
|
|
+}
|
|
|
+
|
|
|
+/* Form Styles */
|
|
|
+.reservation-form-section {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 25px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.reservation-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 25px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #2c3e50;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input,
|
|
|
+.form-group select {
|
|
|
+ padding: 12px;
|
|
|
+ border: 2px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ transition: border-color 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input:focus,
|
|
|
+.form-group select:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #4b8ef1;
|
|
|
+}
|
|
|
+
|
|
|
+.form-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.passengers-info {
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.passengers-info h3 {
|
|
|
+ margin: 0 0 20px 0;
|
|
|
+ color: #2c3e50;
|
|
|
+}
|
|
|
+
|
|
|
+.passenger-form {
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ padding-bottom: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.passenger-form:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ margin-bottom: 0;
|
|
|
+ padding-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.passenger-form h4 {
|
|
|
+ margin: 0 0 15px 0;
|
|
|
+ color: #495057;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-summary {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-summary h3 {
|
|
|
+ margin: 0 0 15px 0;
|
|
|
+ color: #2c3e50;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: #6c757d;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-item.total {
|
|
|
+ border-top: 1px solid #dee2e6;
|
|
|
+ padding-top: 10px;
|
|
|
+ margin-top: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-section h3 {
|
|
|
+ margin: 0 0 15px 0;
|
|
|
+ color: #2c3e50;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-methods {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-method {
|
|
|
+ border: 2px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 15px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-method.active {
|
|
|
+ border-color: #4b8ef1;
|
|
|
+ background: #f8f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-method input {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-method label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.payment-method i {
|
|
|
+ color: #4b8ef1;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding-top: 20px;
|
|
|
+ border-top: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-secondary,
|
|
|
+.btn-primary {
|
|
|
+ padding: 12px 24px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-secondary {
|
|
|
+ background: #6c757d;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-secondary:hover {
|
|
|
+ background: #5a6268;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary {
|
|
|
+ background: linear-gradient(135deg, #4b8ef1 0%, #7a6ad8 100%);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary:hover:not(:disabled) {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 15px rgba(75, 142, 241, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary:disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+/* Payment Warning Styles */
|
|
|
+.payment-warning {
|
|
|
+ margin-top: 15px;
|
|
|
+ padding: 15px;
|
|
|
+ background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
|
|
+ border: 1px solid #ffeaa7;
|
|
|
+ border-radius: 8px;
|
|
|
+ animation: slideIn 0.3s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.warning-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.warning-content i {
|
|
|
+ color: #856404;
|
|
|
+ font-size: 18px;
|
|
|
+ margin-top: 2px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.warning-text {
|
|
|
+ color: #856404;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.warning-text strong {
|
|
|
+ color: #6c5400;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes slideIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-10px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Card Payment Form Styles */
|
|
|
+.card-payment-form {
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 25px;
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
|
+ border: 2px solid #e3f2fd;
|
|
|
+ animation: slideIn 0.3s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.card-payment-form h3 {
|
|
|
+ color: #2c3e50;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-payment-form h3 i {
|
|
|
+ color: #4b8ef1;
|
|
|
+}
|
|
|
+
|
|
|
+.card-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-form .form-group input {
|
|
|
+ border: 2px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px 15px;
|
|
|
+ font-size: 16px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ background: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.card-form .form-group input:focus {
|
|
|
+ border-color: #4b8ef1;
|
|
|
+ background: white;
|
|
|
+ box-shadow: 0 0 0 3px rgba(75, 142, 241, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.card-form .form-group input[placeholder] {
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+}
|
|
|
+
|
|
|
+.card-form .form-group input#card-name {
|
|
|
+ font-family: inherit;
|
|
|
+}
|
|
|
+
|
|
|
+.security-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 12px 15px;
|
|
|
+ background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%);
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #2e7d32;
|
|
|
+ font-size: 14px;
|
|
|
+ border: 1px solid #c8e6c9;
|
|
|
+}
|
|
|
+
|
|
|
+.security-info i {
|
|
|
+ color: #4caf50;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Responsive */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .container {
|
|
|
+ padding: 0 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .trip-route {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .route-line {
|
|
|
+ flex-direction: row;
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .line {
|
|
|
+ width: 40px;
|
|
|
+ height: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .company-card {
|
|
|
+ flex-direction: column;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-row {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .payment-methods {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-actions {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .trip-details {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|