|
|
@@ -0,0 +1,433 @@
|
|
|
+<template>
|
|
|
+ <div v-if="isOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
|
+ <div class="bg-white rounded-lg p-6 w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto">
|
|
|
+ <!-- Header -->
|
|
|
+ <div class="flex justify-between items-center mb-6">
|
|
|
+ <h2 class="text-xl font-bold text-gray-800">Confirmation de réservation</h2>
|
|
|
+ <button @click="closeModal" class="text-gray-500 hover:text-gray-700">
|
|
|
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Progress Steps -->
|
|
|
+ <div class="mb-6">
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div :class="stepClasses(1)" class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium">
|
|
|
+ 1
|
|
|
+ </div>
|
|
|
+ <span class="ml-2 text-sm font-medium" :class="currentStep >= 1 ? 'text-blue-600' : 'text-gray-500'">Utilisateur</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 h-1 mx-4 bg-gray-200 rounded">
|
|
|
+ <div class="h-1 bg-blue-600 rounded transition-all duration-300" :style="{ width: currentStep >= 2 ? '100%' : '0%' }"></div>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div :class="stepClasses(2)" class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium">
|
|
|
+ 2
|
|
|
+ </div>
|
|
|
+ <span class="ml-2 text-sm font-medium" :class="currentStep >= 2 ? 'text-blue-600' : 'text-gray-500'">Réservation</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 h-1 mx-4 bg-gray-200 rounded">
|
|
|
+ <div class="h-1 bg-blue-600 rounded transition-all duration-300" :style="{ width: currentStep >= 3 ? '100%' : '0%' }"></div>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div :class="stepClasses(3)" class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium">
|
|
|
+ 3
|
|
|
+ </div>
|
|
|
+ <span class="ml-2 text-sm font-medium" :class="currentStep >= 3 ? 'text-blue-600' : 'text-gray-500'">Paiement</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Step Content -->
|
|
|
+ <div class="min-h-[200px]">
|
|
|
+ <!-- Step 1: User Creation -->
|
|
|
+ <div v-if="currentStep === 1" class="space-y-4">
|
|
|
+ <h3 class="text-lg font-semibold mb-4">Création du compte utilisateur</h3>
|
|
|
+
|
|
|
+ <div v-if="passengers.length > 1" class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 mb-2">
|
|
|
+ Sélectionnez le passager principal :
|
|
|
+ </label>
|
|
|
+ <select v-model="selectedMainPassenger" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
|
|
+ <option v-for="(passenger, index) in passengers" :key="index" :value="index">
|
|
|
+ {{ passenger.name }} {{ passenger.otherName }}
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="bg-gray-50 p-4 rounded-md">
|
|
|
+ <h4 class="font-medium mb-2">Informations du compte :</h4>
|
|
|
+ <p><strong>Nom :</strong> {{ mainPassenger?.name }} {{ mainPassenger?.otherName }}</p>
|
|
|
+ <p><strong>Téléphone :</strong> {{ mainPassenger?.phone }}</p>
|
|
|
+ <p><strong>Email :</strong> {{ generatedEmail }}</p>
|
|
|
+ <p><strong>Mot de passe :</strong> {{ generatedPassword }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="step1Error" class="text-red-600 text-sm">
|
|
|
+ {{ step1Error }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Step 2: Reservation Creation -->
|
|
|
+ <div v-if="currentStep === 2" class="space-y-4">
|
|
|
+ <h3 class="text-lg font-semibold mb-4">Création de la réservation</h3>
|
|
|
+
|
|
|
+ <div class="bg-gray-50 p-4 rounded-md">
|
|
|
+ <h4 class="font-medium mb-2">Détails de la réservation :</h4>
|
|
|
+ <p><strong>Trajet :</strong> {{ tripDetails }}</p>
|
|
|
+ <p><strong>Nombre de passagers :</strong> {{ passengers.length }}</p>
|
|
|
+ <p><strong>Places :</strong> {{ selectedSeats.join(', ') }}</p>
|
|
|
+ <p><strong>Total :</strong> {{ formatPrice(totalAmount) }} FCFA</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="step2Error" class="text-red-600 text-sm">
|
|
|
+ {{ step2Error }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Step 3: Payment -->
|
|
|
+ <div v-if="currentStep === 3" class="space-y-4">
|
|
|
+ <h3 class="text-lg font-semibold mb-4">Paiement</h3>
|
|
|
+
|
|
|
+ <div class="bg-gray-50 p-4 rounded-md">
|
|
|
+ <h4 class="font-medium mb-2">Récapitulatif du paiement :</h4>
|
|
|
+ <p><strong>Réservation ID :</strong> {{ createdReservationId }}</p>
|
|
|
+ <p><strong>Montant :</strong> {{ formatPrice(totalAmount) }} FCFA</p>
|
|
|
+ <p><strong>Méthode :</strong> Mobile Money</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="step3Error" class="text-red-600 text-sm">
|
|
|
+ {{ step3Error }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="paymentSuccess" class="bg-green-50 border border-green-200 rounded-md p-4">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <svg class="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
|
+ </svg>
|
|
|
+ <span class="text-green-800 font-medium">Paiement effectué avec succès !</span>
|
|
|
+ </div>
|
|
|
+ <p class="text-green-700 text-sm mt-2">Votre réservation a été confirmée.</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Loading State -->
|
|
|
+ <div v-if="isLoading" class="flex items-center justify-center py-8">
|
|
|
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
|
+ <span class="ml-2 text-gray-600">{{ loadingMessage }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Action Buttons -->
|
|
|
+ <div class="flex justify-between mt-6">
|
|
|
+ <button
|
|
|
+ v-if="currentStep > 1 && !paymentSuccess"
|
|
|
+ @click="previousStep"
|
|
|
+ :disabled="isLoading"
|
|
|
+ class="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50"
|
|
|
+ >
|
|
|
+ Précédent
|
|
|
+ </button>
|
|
|
+ <div v-else></div>
|
|
|
+
|
|
|
+ <button
|
|
|
+ v-if="!paymentSuccess"
|
|
|
+ @click="nextStep"
|
|
|
+ :disabled="isLoading || !canProceed"
|
|
|
+ class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ >
|
|
|
+ {{ currentStep === 3 ? 'Payer' : 'Suivant' }}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button
|
|
|
+ v-if="paymentSuccess"
|
|
|
+ @click="closeModal"
|
|
|
+ class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
|
|
|
+ >
|
|
|
+ Fermer
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, watch } from 'vue'
|
|
|
+import UserService from '@/services/UserService'
|
|
|
+import ReservationService from '@/services/ReservationService'
|
|
|
+import TransactionService from '@/services/TransactionService'
|
|
|
+import type { User, Reservation } from '@/types'
|
|
|
+import { Role } from '@/Enum/Role'
|
|
|
+
|
|
|
+interface Passenger {
|
|
|
+ name: string
|
|
|
+ otherName: string
|
|
|
+ phone: string
|
|
|
+ email?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ isOpen: boolean
|
|
|
+ passengers: Passenger[]
|
|
|
+ selectedSeats: number[]
|
|
|
+ totalAmount: number
|
|
|
+ tripId: number
|
|
|
+ tripDetails: string
|
|
|
+}
|
|
|
+
|
|
|
+interface Emits {
|
|
|
+ close: []
|
|
|
+ success: [reservationId: number]
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps<Props>()
|
|
|
+const emit = defineEmits<Emits>()
|
|
|
+
|
|
|
+// State
|
|
|
+const currentStep = ref(1)
|
|
|
+const isLoading = ref(false)
|
|
|
+const loadingMessage = ref('')
|
|
|
+const selectedMainPassenger = ref(0)
|
|
|
+const createdUserId = ref<number | null>(null)
|
|
|
+const createdReservationId = ref<number | null>(null)
|
|
|
+const paymentSuccess = ref(false)
|
|
|
+
|
|
|
+// Error states
|
|
|
+const step1Error = ref('')
|
|
|
+const step2Error = ref('')
|
|
|
+const step3Error = ref('')
|
|
|
+
|
|
|
+// Computed
|
|
|
+const mainPassenger = computed(() => props.passengers[selectedMainPassenger.value])
|
|
|
+
|
|
|
+const generatedPassword = computed(() => {
|
|
|
+ console.log('Computed generatedPassword called', mainPassenger.value)
|
|
|
+ if (!mainPassenger.value) return ''
|
|
|
+ const phone = mainPassenger.value.phone
|
|
|
+ const lastTwoDigits = phone.slice(-2)
|
|
|
+ return `${mainPassenger.value.name.toLowerCase()}.${mainPassenger.value.otherName.toLowerCase()}${lastTwoDigits}`
|
|
|
+})
|
|
|
+
|
|
|
+const generatedEmail = computed(() => {
|
|
|
+ console.log('Computed generatedEmail called', mainPassenger.value)
|
|
|
+ if (!mainPassenger.value) return ''
|
|
|
+ if (mainPassenger.value.email) return mainPassenger.value.email
|
|
|
+ const phone = mainPassenger.value.phone
|
|
|
+ const lastTwoDigits = phone.slice(-2)
|
|
|
+ return `${mainPassenger.value.name.toLowerCase()}.${mainPassenger.value.otherName.toLowerCase()}${lastTwoDigits}@transport.ci`
|
|
|
+})
|
|
|
+
|
|
|
+const canProceed = computed(() => {
|
|
|
+ if (currentStep.value === 1) {
|
|
|
+ return mainPassenger.value && mainPassenger.value.name && mainPassenger.value.otherName && mainPassenger.value.phone
|
|
|
+ }
|
|
|
+ if (currentStep.value === 2) {
|
|
|
+ return createdUserId.value !== null
|
|
|
+ }
|
|
|
+ if (currentStep.value === 3) {
|
|
|
+ return createdReservationId.value !== null
|
|
|
+ }
|
|
|
+ return false
|
|
|
+})
|
|
|
+
|
|
|
+// Methods
|
|
|
+const stepClasses = (step: number) => {
|
|
|
+ if (currentStep.value > step) {
|
|
|
+ return 'bg-green-600 text-white'
|
|
|
+ } else if (currentStep.value === step) {
|
|
|
+ return 'bg-blue-600 text-white'
|
|
|
+ } else {
|
|
|
+ return 'bg-gray-300 text-gray-600'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const formatPrice = (price: number) => {
|
|
|
+ return new Intl.NumberFormat('fr-FR').format(price)
|
|
|
+}
|
|
|
+
|
|
|
+const closeModal = () => {
|
|
|
+ // Reset state
|
|
|
+ currentStep.value = 1
|
|
|
+ isLoading.value = false
|
|
|
+ selectedMainPassenger.value = 0
|
|
|
+ createdUserId.value = null
|
|
|
+ createdReservationId.value = null
|
|
|
+ paymentSuccess.value = false
|
|
|
+ step1Error.value = ''
|
|
|
+ step2Error.value = ''
|
|
|
+ step3Error.value = ''
|
|
|
+
|
|
|
+ emit('close')
|
|
|
+}
|
|
|
+
|
|
|
+const previousStep = () => {
|
|
|
+ if (currentStep.value > 1) {
|
|
|
+ currentStep.value--
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const nextStep = async () => {
|
|
|
+ if (!canProceed.value) return
|
|
|
+
|
|
|
+ if (currentStep.value === 1) {
|
|
|
+ await createUser()
|
|
|
+ } else if (currentStep.value === 2) {
|
|
|
+ await createReservation()
|
|
|
+ } else if (currentStep.value === 3) {
|
|
|
+ await processPayment()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const createUser = async () => {
|
|
|
+ isLoading.value = true
|
|
|
+ loadingMessage.value = 'Création du compte utilisateur...'
|
|
|
+ step1Error.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const userData: Omit<User, 'id'> = {
|
|
|
+ name: `${mainPassenger.value.name} ${mainPassenger.value.otherName}`,
|
|
|
+ email: generatedEmail.value,
|
|
|
+ password: generatedPassword.value,
|
|
|
+ telephone: mainPassenger.value.phone,
|
|
|
+ role: Role.User
|
|
|
+ }
|
|
|
+
|
|
|
+ delete userData.role
|
|
|
+ const createdUser = await UserService.createUser(userData)
|
|
|
+
|
|
|
+ if (createdUser && createdUser.id) {
|
|
|
+ createdUserId.value = createdUser.id
|
|
|
+ currentStep.value = 2
|
|
|
+ } else {
|
|
|
+ step1Error.value = 'Erreur lors de la création du compte utilisateur'
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Erreur création utilisateur:', error)
|
|
|
+ step1Error.value = 'Erreur lors de la création du compte utilisateur'
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const createReservation = async () => {
|
|
|
+ isLoading.value = true
|
|
|
+ loadingMessage.value = 'Création de la réservation...'
|
|
|
+ step2Error.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Créer une réservation pour chaque passager/place
|
|
|
+ const reservations = []
|
|
|
+
|
|
|
+ for (let i = 0; i < props.passengers.length; i++) {
|
|
|
+ const reservationData: Omit<Reservation, 'id'> = {
|
|
|
+ dateAt: new Date().toISOString(),
|
|
|
+ placeId: props.selectedSeats[i],
|
|
|
+ userId: createdUserId.value!,
|
|
|
+ trajetId: props.tripId
|
|
|
+ }
|
|
|
+
|
|
|
+ const reservation = await ReservationService.createReservation(reservationData)
|
|
|
+ console.log('Réservations créées 1:', reservations)
|
|
|
+ if (reservation) {
|
|
|
+ reservations.push(reservation)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.log('Réservations créées:', reservations) // Ajout de cette ligne pour débogage
|
|
|
+ if (reservations.length > 0) {
|
|
|
+ // Utiliser la première réservation comme référence principale
|
|
|
+ console.log('Je suis dedans', reservations)
|
|
|
+ createdReservationId.value = reservations[0].data.id
|
|
|
+ currentStep.value = 3
|
|
|
+ } else {
|
|
|
+ step2Error.value = 'Erreur lors de la création de la réservation'
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Erreur création réservation:', error)
|
|
|
+ step2Error.value = 'Erreur lors de la création de la réservation'
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const processPayment = async () => {
|
|
|
+ isLoading.value = true
|
|
|
+ loadingMessage.value = 'Traitement du paiement...'
|
|
|
+ step3Error.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Vérifier que nous avons un ID de réservation valide
|
|
|
+ if (!createdReservationId.value || typeof createdReservationId.value !== 'number') {
|
|
|
+ throw new Error('ID de réservation manquant ou invalide')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Données pour CinetPay selon la documentation
|
|
|
+ const cinetPayData = {
|
|
|
+ apikey: "174173934865f8799156f094.97916229", // À remplacer par votre vraie clé API
|
|
|
+ site_id: "105897483", // À remplacer par votre site ID
|
|
|
+ transaction_id: `TRANSPORT_${createdReservationId.value}_${Date.now()}`, // ID unique de transaction
|
|
|
+ amount: props.totalAmount,
|
|
|
+ currency: "XOF",
|
|
|
+ description: `Réservation transport - ${props.passengers.length} place(s)`,
|
|
|
+ customer_id: createdUserId.value?.toString() || "guest",
|
|
|
+ customer_name: mainPassenger.value.name || "",
|
|
|
+ customer_surname: mainPassenger.value.otherName || "",
|
|
|
+ customer_email: mainPassenger.value.email || generatedEmail.value,
|
|
|
+ customer_phone_number: mainPassenger.value.phone,
|
|
|
+ customer_address: "Côte d'Ivoire",
|
|
|
+ customer_city: "Abidjan",
|
|
|
+ customer_country: "CI",
|
|
|
+ customer_state: "CI",
|
|
|
+ customer_zip_code: "00225",
|
|
|
+ notify_url: `${window.location.origin}/api/payments/cinetpay/notify`,
|
|
|
+ return_url: `${window.location.origin}/payment/success?reservation=${createdReservationId.value}`,
|
|
|
+ channels: "MOBILE_MONEY", // Uniquement mobile money
|
|
|
+ metadata: JSON.stringify({ reservationId: createdReservationId.value }),
|
|
|
+ lang: "FR"
|
|
|
+ }
|
|
|
+
|
|
|
+ // Appel à l'API CinetPay pour générer l'URL de paiement
|
|
|
+ const paymentResult = await fetch('https://api-checkout.cinetpay.com/v2/payment', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(cinetPayData)
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!paymentResult.ok) {
|
|
|
+ throw new Error(`Erreur HTTP CinetPay: ${paymentResult.status}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await paymentResult.json()
|
|
|
+
|
|
|
+ // Vérifier la réponse de CinetPay
|
|
|
+ if (result.code === '201' && result.data && result.data.payment_url) {
|
|
|
+ // Rediriger vers l'URL de paiement CinetPay
|
|
|
+ window.location.href = result.data.payment_url
|
|
|
+ } else {
|
|
|
+ throw new Error(result.message || 'Erreur lors de la génération du lien de paiement')
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Erreur paiement CinetPay:', error)
|
|
|
+ step3Error.value = error instanceof Error ? error.message : 'Erreur lors du traitement du paiement'
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Watch for modal opening to reset state
|
|
|
+watch(() => props.isOpen, (newValue) => {
|
|
|
+ if (newValue) {
|
|
|
+ currentStep.value = 1
|
|
|
+ selectedMainPassenger.value = 0
|
|
|
+ createdUserId.value = null
|
|
|
+ createdReservationId.value = null
|
|
|
+ paymentSuccess.value = false
|
|
|
+ step1Error.value = ''
|
|
|
+ step2Error.value = ''
|
|
|
+ step3Error.value = ''
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|