Quellcode durchsuchen

(feat) company amélioration

Brin AMONKONAN vor 1 Jahr
Ursprung
Commit
c5ab0b6b0c

+ 30 - 0
app_transport_front/src/services/TripService.ts

@@ -265,5 +265,35 @@ export default {
         pagination: { total: 0, page: 1, limit: 50, totalPages: 1 }
       }
     }
+  },
+
+  async getTripReservations(tripId: number, page: number = 1, limit: number = 50, status?: string): Promise<{ reservations: any[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {
+    try {
+      let url = `${API_URL}/trajets/${tripId}/reservations?page=${page}&limit=${limit}`
+      if (status) {
+        url += `&status=${encodeURIComponent(status)}`
+      }
+      
+      const response = await authenticatedFetch(url)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des réservations')
+      }
+      const result = await response.json()
+      return {
+        reservations: result.data?.data || [],
+        pagination: {
+          total: result.data?.total || 0,
+          page: result.data?.page || 1,
+          limit: result.data?.limit || 50,
+          totalPages: result.data?.totalPages || 1
+        }
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return {
+        reservations: [],
+        pagination: { total: 0, page: 1, limit: 50, totalPages: 1 }
+      }
+    }
   }
 }

+ 371 - 24
app_transport_front/src/views/TripView.vue

@@ -84,11 +84,11 @@
                 <td>{{ formatDateTime(trip.departAt) }}</td>
                 <td>{{ formatDateTime(trip.arriveAt) }}</td>
                 <td>{{ trip.pricePerSeat || 0 }} FCFA</td>
-                <td>{{ trip.availableSeats || 0 }}/{{ trip.totalSeats || 0 }}</td>
-                <td>{{ getDriverName(trip.driverId) }}</td>
+                <td>{{ trip.availableSeats || 0 }}/{{ trip.vehicule?.number_places || 0 }}</td>
+                <td>{{ getDriverName() }}</td>
                 <td>
-                  <span class="status-badge" :class="getStatusClass(trip.status)">
-                    {{ getStatusLabel(trip.status || 'upcoming') }}
+                  <span class="status-badge" :class="getStatusClass(trip)">
+                    {{ getStatusLabel(trip) }}
                   </span>
                 </td>
                 <td class="actions-cell">
@@ -257,11 +257,11 @@
                   </div>
                   <div class="info-item">
                     <div class="info-label">Places disponibles</div>
-                    <div class="info-value">{{ viewingTrip.availableSeats || 0 }}/{{ viewingTrip.totalSeats || 0 }}</div>
+                    <div class="info-value">{{ viewingTrip.availableSeats || 0 }}/{{ viewingTrip.vehicule?.number_places || 0 }}</div>
                   </div>
                   <div class="info-item">
                     <div class="info-label">Chauffeur</div>
-                    <div class="info-value">{{ getDriverName(viewingTrip.driverId) }}</div>
+                    <div class="info-value">{{ getDriverName() }}</div>
                   </div>
                   <div class="info-item">
                     <div class="info-label">Véhicule</div>
@@ -270,28 +270,40 @@
                   <div class="info-item">
                     <div class="info-label">Statut</div>
                     <div class="info-value">
-                      <span class="status-badge" :class="getStatusClass(viewingTrip.status || 'upcoming')">
-                        {{ getStatusLabel(viewingTrip.status || 'upcoming') }}
+                      <span class="status-badge" :class="getStatusClass(viewingTrip)">
+                        {{ getStatusLabel(viewingTrip) }}
                       </span>
                     </div>
                   </div>
                   <div class="info-item" v-if="viewingTrip.actualArrivalDateTime">
                     <div class="info-label">Arrivée réelle</div>
                     <div class="info-value">{{ formatDateTime(viewingTrip.actualArrivalDateTime) }}</div>
+                  </div>
+                  <!-- Ajout de la somme gagnée selon la formule demandée -->
+                  <div class="info-item bg-gradient-to-r from-green-100 to-emerald-100 border-2 border-green-300 shadow-lg rounded-lg p-3 transform hover:scale-105 transition-all duration-200">
+                    <div class="info-label text-green-800 font-semibold flex items-center">
+                      <svg class="w-5 h-5 mr-2 text-green-600" fill="currentColor" viewBox="0 0 20 20">
+                        <path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
+                      </svg>
+                      Somme gagnée 💰
+                    </div>
+                    <div class="info-value text-green-700 font-bold text-lg">{{ (viewingTrip.pricePerSeat || 0) * ((viewingTrip.vehicule?.number_places || 0) - (viewingTrip.availableSeats || 0)) }} FCFA</div>
                   </div>
                     <!-- Ajout des sommes -->
-                  <div class="info-item">
-                    <div class="info-label">Totale des places prises</div>
-                    <div class="info-value">{{ ((viewingTrip.totalSeats || 0) - (viewingTrip.availableSeats || 0)) * (viewingTrip.pricePerSeat || 0) }} FCFA</div>
+                  <div class="info-item cursor-pointer hover:bg-blue-50 transition-colors duration-200" @click="openReservationsModal">
+                    <div class="info-label flex items-center">
+                      <span>Totale des places réservées</span>
+                      <i class="fas fa-eye ml-2 text-blue-500"></i>
+                    </div>
+                    <div class="info-value text-blue-600">{{ ((viewingTrip.vehicule?.number_places || 0) - (viewingTrip.availableSeats || 0)) }} </div>
                   </div>
                   <div class="info-item">
-                    <div class="info-label">Totale des places non occupés</div>
-                    <div class="info-value">{{ (viewingTrip.availableSeats || 0) * (viewingTrip.pricePerSeat || 0) }} FCFA</div>
+                    <div class="info-label">Totale des paiements en ligne</div>
+                    <div class="info-value">{{ totalOnlinePayments }} FCFA</div>
                   </div>
                   <div class="info-item">
-                    <div class="info-label">Totale des places en espèces</div>
-                    <!-- Calcul à faire -->
-                    <div class="info-value">{{ (viewingTrip.availableSeats || 0) * (viewingTrip.pricePerSeat || 0) }} FCFA</div>
+                    <div class="info-label">Totale des paiements en espèces</div>
+                    <div class="info-value">{{ totalCashPayments }} FCFA</div>
                   </div>
                 </div>
 
@@ -309,6 +321,8 @@
                       :cities="stationStore.stations"
                     />
                   </div>
+
+
               </div>
             </div>
             <div class="py-4 px-5 border-t border-gray-200 flex justify-end gap-2.5">
@@ -336,6 +350,137 @@
           </div>
         </div>
       </div>
+
+      <!-- Modal des réservations -->
+      <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" v-if="showReservationsModal">
+        <div class="bg-white rounded-lg w-11/12 max-w-6xl max-h-screen overflow-y-auto shadow-lg">
+          
+          <div class="flex justify-end items-center py-4 px-5 border-b border-gray-200 ">
+          <button class="bg-transparent border-0 m-2 p-2 rounded-full text-xl text-gray-400 cursor-pointer hover:bg-gray-400 hover:text-white " @click="closeReservationsModal">
+            <i class="fas fa-times"></i>
+          </button>
+          </div>
+
+          
+          <div class="p-5">
+            <!-- Filtres et actions -->
+            <div class="flex justify-between items-center mb-4">
+                          <h2 class="text-xl text-gray-800 m-0">
+              <i class="fas fa-list-alt mr-2 text-blue-500"></i>
+              Réservations du voyage
+            </h2>
+              <div class="flex gap-2">
+                <select v-model="reservationFilters.status" @change="loadTripReservations" class="px-3 py-2 border border-gray-300 rounded text-sm">
+                  <option value="">Tous les statuts</option>
+                  <option value="pending">En attente</option>
+                  <option value="confirmed">Confirmée</option>
+                  <option value="cancelled">Annulée</option>
+                  <option value="completed">Terminée</option>
+                </select>
+                <button @click="loadTripReservations" class="px-4 py-2 bg-blue-500 text-white rounded text-sm hover:bg-blue-600 transition-colors">
+                  <i class="fas fa-sync-alt mr-1"></i> Actualiser
+                </button>
+              </div>
+            </div>
+
+            <!-- Loading state -->
+            <div v-if="loadingReservations" class="text-center py-8">
+              <i class="fas fa-spinner fa-spin text-blue-500 text-2xl"></i>
+              <p class="text-gray-600 mt-3">Chargement des réservations...</p>
+            </div>
+
+            <!-- Error state -->
+            <div v-else-if="reservationsError" class="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
+              <i class="fas fa-exclamation-triangle mr-2"></i>
+              {{ reservationsError }}
+            </div>
+
+            <!-- Reservations list -->
+            <div v-else-if="tripReservations.length > 0" class="space-y-4">
+              <div v-for="reservation in tripReservations" :key="reservation.id" 
+                   class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
+                <div class="flex justify-between items-start">
+                  <div class="flex-1">
+                    <div class="flex items-center gap-3 mb-3">
+                      <div class="flex items-center gap-2">
+                        <i class="fas fa-user text-gray-500"></i>
+                        <span class="font-medium text-gray-900 text-lg">
+                          {{ reservation.user?.name || 'N/A' }} {{ reservation.user?.otherName || '' }}
+                        </span>
+                      </div>
+                      <span class="reservation-status-badge" :class="getReservationStatusClass(reservation.status)">
+                        {{ getReservationStatusLabel(reservation.status) }}
+                      </span>
+                    </div>
+                    
+                    <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-gray-600 mb-3">
+                      <div class="flex items-center gap-2">
+                        <i class="fas fa-chair text-blue-500"></i>
+                        <span><strong>Places:</strong> {{ reservation.place.number }}</span>
+                      </div>
+                      <div class="flex items-center gap-2">
+                        <i class="fas fa-money-bill-wave text-green-500"></i>
+                        <span><strong>Total:</strong> {{ reservation.price }} FCFA</span>
+                      </div>
+                      <div class="flex items-center gap-2">
+                        <i class="fas fa-credit-card text-purple-500"></i>
+                        <span><strong>Paiement:</strong> {{ getPaymentMethodLabel(reservation.paymentMethod) }}</span>
+                      </div>
+                      <div class="flex items-center gap-2">
+                        <i class="fas fa-calendar text-gray-500"></i>
+                        <span><strong>Réservé le:</strong> {{ formatDateTime(reservation.dateAt) }}</span>
+                      </div>
+                    </div>
+
+                    <div v-if="reservation.place" class="flex items-center gap-2 text-sm text-gray-600 mb-2">
+                      <i class="fas fa-map-marker-alt text-red-500"></i>
+                      <span><strong>Lieu de prise en charge:</strong> {{ reservation.place.name }}</span>
+                    </div>
+
+                    <div v-if="reservation.notes" class="flex items-start gap-2 text-sm text-gray-600">
+                      <i class="fas fa-sticky-note text-yellow-500 mt-1"></i>
+                      <span><strong>Notes:</strong> {{ reservation.notes }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              <!-- Pagination -->
+              <div v-if="reservationsPagination.totalPages > 1" class="flex justify-center items-center gap-3 mt-6 pt-4 border-t border-gray-200">
+                <button @click="changePage(reservationsPagination.currentPage - 1)" 
+                        :disabled="reservationsPagination.currentPage <= 1"
+                        class="px-4 py-2 border border-gray-300 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors">
+                  <i class="fas fa-chevron-left mr-1"></i> Précédent
+                </button>
+                
+                <span class="text-sm text-gray-600 px-4">
+                  Page {{ reservationsPagination.currentPage }} sur {{ reservationsPagination.totalPages }}
+                  ({{ reservationsPagination.totalItems }} réservations)
+                </span>
+                
+                <button @click="changePage(reservationsPagination.currentPage + 1)" 
+                        :disabled="reservationsPagination.currentPage >= reservationsPagination.totalPages"
+                        class="px-4 py-2 border border-gray-300 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors">
+                  Suivant <i class="fas fa-chevron-right ml-1"></i>
+                </button>
+              </div>
+            </div>
+
+            <!-- Empty state -->
+            <div v-else class="text-center py-12 text-gray-500">
+              <i class="fas fa-calendar-times text-6xl mb-4 text-gray-300"></i>
+              <h3 class="text-lg font-medium text-gray-600 mb-2">Aucune réservation trouvée</h3>
+              <p class="text-gray-500">Ce voyage n'a pas encore de réservations.</p>
+            </div>
+          </div>
+          
+          <div class="py-4 px-5 border-t border-gray-200 flex justify-end">
+            <button class="bg-gray-100 text-gray-600 border-0 rounded py-2.5 px-4 cursor-pointer hover:bg-gray-200 transition-colors" @click="closeReservationsModal">
+              Fermer
+            </button>
+          </div>
+        </div>
+      </div>
     </div>
 </template>
 
@@ -377,11 +522,28 @@ const { success, error } = useToast()
 const editingTrip = ref<Trip | null>(null)
 const viewingTrip = ref<Trip | null>(null)
 const showDeleteConfirm = ref(false)
+const showReservationsModal = ref(false)
 const tripToDelete = ref<Trip | null>(null)
 const searchQuery = ref('')
 const driverFilter = ref('')
 const statusFilter = ref('')
 
+// Variables pour les réservations
+const tripReservations = ref<any[]>([])
+const loadingReservations = ref(false)
+const reservationsError = ref('')
+const reservationFilters = ref({
+  status: '',
+  page: 1,
+  limit: 10
+})
+const reservationsPagination = ref({
+  currentPage: 1,
+  totalPages: 1,
+  totalItems: 0,
+  itemsPerPage: 10
+})
+
 const formData = ref<Trip>({
   id: 0,
   departureCity: 0,
@@ -435,7 +597,7 @@ const filteredTrips = computed(() => {
     const searchLower = searchQuery.value.toLowerCase()
     const departureMatch = getStationName(trip.departStationId).toLowerCase().includes(searchLower)
     const arrivalMatch = getStationName(trip.arriveStationId).toLowerCase().includes(searchLower)
-    const driverMatch = getDriverName(trip.driverId).toLowerCase().includes(searchLower)
+    const driverMatch = getDriverName().toLowerCase().includes(searchLower)
     
     if (searchQuery.value && !departureMatch && !arrivalMatch && !driverMatch) {
       return false
@@ -455,6 +617,26 @@ const filteredTrips = computed(() => {
   })
 })
 
+// Propriétés calculées pour les totaux des paiements
+const totalOnlinePayments = computed(() => {
+  if (!viewingTrip.value || !tripReservations.value.length) return 0
+  
+  return tripReservations.value
+    .filter(reservation => 
+      reservation.paymentMethod === 'CARD' || 
+      reservation.paymentMethod === 'MOBILE'
+    )
+    .reduce((total, reservation) => total + (reservation.price || 0), 0)
+})
+
+const totalCashPayments = computed(() => {
+  if (!viewingTrip.value || !tripReservations.value.length) return 0
+  
+  return tripReservations.value
+    .filter(reservation => reservation.paymentMethod === 'CASH')
+    .reduce((total, reservation) => total + (reservation.price || 0), 0)
+})
+
 // Fonctions utilitaires
 const formatDateTime = (dateTime: string): string => {
   if (!dateTime) return ''
@@ -473,18 +655,24 @@ const getStationName = (stationId: number): string => {
   return station ? station.name : 'Station inconnue'
 }
 
-const getDriverName = (driverId: number): string => {
+const getDriverName = (): string => {
   console.log('drivers:', drivers.value)
-  const driver = drivers.value.find(d => d.id === driverId)
-  return driver ? `${driver.name} ${driver.otherName}` : 'Inconnu'
+  return drivers.value[0] ? `${drivers.value[0].name} ${drivers.value[0].otherName}` : 'Inconnu'
 }
 
 const getVehicleName = (vehicleId: number): string => {
   const vehicle = vehicles.value.find(v => v.id === vehicleId)
-  return vehicle ? `${vehicle.model} ${vehicle.type}` : 'Inconnu'
+  return vehicle ? `${vehicle.model} | ${vehicle.type}` : 'Inconnu'
 }
 
-const getStatusLabel = (status: string): string => {
+const getStatusLabel = (trip: any): string => {
+  // Vérifier d'abord si les inscriptions sont fermées
+  if (trip.registrationClosed) {
+    return 'En cours'
+  }
+  
+  // Sinon, utiliser le statut normal
+  const status = trip.status || 'upcoming'
   switch (status) {
     case 'upcoming': return 'À venir'
     case 'ongoing': return 'En cours'
@@ -494,7 +682,14 @@ const getStatusLabel = (status: string): string => {
   }
 }
 
-const getStatusClass = (status: string): string => {
+const getStatusClass = (trip: any): string => {
+  // Vérifier d'abord si les inscriptions sont fermées
+  if (trip.registrationClosed) {
+    return 'status-ongoing'
+  }
+  
+  // Sinon, utiliser le statut normal
+  const status = trip.status || 'upcoming'
   switch (status) {
     case 'upcoming': return 'status-upcoming'
     case 'ongoing': return 'status-ongoing'
@@ -792,9 +987,131 @@ const handleMessages = () => {
 const handleUserMenu = () => {
   // Gérer l'affichage du menu utilisateur
 }
+
+// Méthodes pour gérer les réservations
+const loadTripReservations = async () => {
+  if (!viewingTrip.value?.id) return
+
+  loadingReservations.value = true
+  reservationsError.value = ''
+
+  try {
+    const result = await TripService.getTripReservations(
+      viewingTrip.value.id,
+      reservationFilters.value.page,
+      reservationFilters.value.limit,
+      reservationFilters.value.status
+    )
+    console.log('Résultat getTripReservations:', result)
+    tripReservations.value = result.reservations
+    reservationsPagination.value = {
+      currentPage: result.pagination.currentPage,
+      totalPages: result.pagination.totalPages,
+      totalItems: result.pagination.totalItems,
+      itemsPerPage: result.pagination.itemsPerPage
+    }
+  } catch (err: any) {
+    console.error('Erreur lors du chargement des réservations:', err)
+    reservationsError.value = err.message || 'Erreur lors du chargement des réservations'
+    tripReservations.value = []
+  } finally {
+    loadingReservations.value = false
+  }
+}
+
+const changePage = (newPage: number) => {
+  if (newPage >= 1 && newPage <= reservationsPagination.value.totalPages) {
+    reservationFilters.value.page = newPage
+    loadTripReservations()
+  }
+}
+
+const getReservationStatusLabel = (status: string): string => {
+  switch (status) {
+    case 'pending': return 'En attente'
+    case 'confirmed': return 'Confirmée'
+    case 'cancelled': return 'Annulée'
+    case 'completed': return 'Terminée'
+    default: return status
+  }
+}
+
+const getReservationStatusClass = (status: string): string => {
+  switch (status) {
+    case 'pending': return 'bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full text-xs'
+    case 'confirmed': return 'bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs'
+    case 'cancelled': return 'bg-red-100 text-red-800 px-2 py-1 rounded-full text-xs'
+    case 'completed': return 'bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs'
+    default: return 'bg-gray-100 text-gray-800 px-2 py-1 rounded-full text-xs'
+  }
+}
+
+const getPaymentMethodLabel = (method: string): string => {
+  switch (method) {
+    case 'cash': return 'Espèces'
+    case 'card': return 'Carte'
+    case 'mobile': return 'Mobile Money'
+    case 'online': return 'En ligne'
+    default: return method || 'Non spécifié'
+  }
+}
+
+const formatDate = (dateString: string): string => {
+  if (!dateString) return 'N/A'
+  const date = new Date(dateString)
+  return date.toLocaleDateString('fr-FR', {
+    day: '2-digit',
+    month: '2-digit',
+    year: 'numeric'
+  })
+}
+
+// Méthodes pour gérer la modal des réservations
+const openReservationsModal = (trip: Trip) => {
+  viewingTrip.value = trip
+  showReservationsModal.value = true
+  // Réinitialiser les filtres et la pagination
+  reservationFilters.value = {
+    status: '',
+    page: 1,
+    limit: 10
+  }
+  reservationsPagination.value = {
+    currentPage: 1,
+    totalPages: 1,
+    totalItems: 0,
+    itemsPerPage: 10
+  }
+  // Charger les réservations
+  loadTripReservations()
+}
+
+const closeReservationsModal = () => {
+  showReservationsModal.value = false
+  viewingTrip.value = null
+  tripReservations.value = []
+  reservationsError.value = ''
+}
+
+// Watcher pour charger les réservations quand un voyage est sélectionné pour visualisation
+watch(() => viewingTrip.value, (newTrip) => {
+  if (newTrip) {
+    // Réinitialiser les filtres et la pagination
+    reservationFilters.value = {
+      status: '',
+      page: 1,
+      limit: 10
+    }
+    loadTripReservations()
+  } else {
+    // Nettoyer les données quand la modal se ferme
+    tripReservations.value = []
+    reservationsError.value = ''
+  }
+})
+
 // Initialisation des données
 onMounted(async () => {
-  console.log('viewingTrip:', viewingTrip.value)
   await loadStations()
   await loadDrivers()
   await loadVehicles()
@@ -1286,6 +1603,36 @@ watch(() => formData.value.vehicleId, (newVehicleId) => {
   border-radius: 5px;
 }
 
+/* Styles pour l'élément cliquable des réservations */
+.info-item.clickable {
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.info-item.clickable:hover {
+  background-color: #f8f9fc;
+  border-color: #4e73df;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.info-item.clickable .info-value {
+  color: #4e73df;
+}
+
+.info-item.clickable:hover .info-value {
+  color: #2e59d9;
+}
+
+/* Styles pour les badges de statut des réservations */
+.reservation-status-badge {
+  display: inline-block;
+  font-size: 11px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
 .trip-notes h4 {
   margin: 0 0 10px 0;
   color: #333;

+ 2 - 0
node_app/src/app.module.ts

@@ -35,6 +35,7 @@ import { DeleteTrajetUseCase } from './application/use-cases/trajet/delete-traje
 import { SearchTrajetsUseCase } from './application/use-cases/trajet/search-trajets.use-case';
 import { FindTripPassengersUseCase } from './application/use-cases/trajet/find-trip-passengers.use-case';
 import { CloseRegistrationUseCase } from './application/use-cases/trajet/close-registration.use-case';
+import { FindReservationsByTripUseCase } from './application/use-cases/reservation/find-reservations-by-trip.use-case';
 import { TrajetMapper } from './infrastructure/mappers/trajet.mapper';
 import { TrajetRepository } from './infrastructure/database/repositories/trajet.repository';
 import { DebugMiddleware } from './middleware/debug.middleware';
@@ -178,6 +179,7 @@ import { CqrsModule } from '@nestjs/cqrs';
     SearchTrajetsUseCase,
     FindTripPassengersUseCase,
     CloseRegistrationUseCase,
+    FindReservationsByTripUseCase,
     TrajetMapper,
     {
       provide: 'TrajetRepository',

+ 69 - 0
node_app/src/application/dto/responses/trip-reservation-response.dto.ts

@@ -0,0 +1,69 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { PlaceResponseDto } from './place-response.dto';
+import { UserResponseDto } from './user-response.dto';
+
+export enum ReservationStatus {
+  PENDING = 'PENDING',
+  CONFIRMED = 'CONFIRMED',
+  CANCELLED = 'CANCELLED',
+  COMPLETED = 'COMPLETED',
+}
+
+export enum PaymentMethod {
+  CARD = 'CARD',
+  MOBILE = 'MOBILE',
+  CASH = 'CASH',
+}
+
+export class TripReservationResponseDto {
+  @ApiProperty({
+    description: 'ID de la réservation',
+    example: 1,
+  })
+  id: number;
+
+  @ApiProperty({
+    description: 'Date de création de la réservation',
+    example: '2024-01-15T10:30:00Z',
+  })
+  dateAt: Date;
+
+  @ApiProperty({
+    description: 'Statut de la réservation',
+    enum: ReservationStatus,
+    example: ReservationStatus.CONFIRMED,
+  })
+  status: ReservationStatus;
+
+  @ApiProperty({
+    description: 'Méthode de paiement utilisée',
+    enum: PaymentMethod,
+    example: PaymentMethod.CASH,
+  })
+  paymentMethod: PaymentMethod;
+
+  @ApiProperty({
+    description: 'Informations de la place réservée',
+    type: () => PlaceResponseDto,
+  })
+  place: PlaceResponseDto;
+
+  @ApiProperty({
+    description: 'Informations de l\'utilisateur qui a fait la réservation',
+    type: () => UserResponseDto,
+  })
+  user: UserResponseDto;
+
+  @ApiProperty({
+    description: 'Prix payé pour cette réservation',
+    example: 2500,
+  })
+  price?: number;
+
+  @ApiProperty({
+    description: 'Notes ou commentaires sur la réservation',
+    example: 'Réservation confirmée par téléphone',
+    required: false,
+  })
+  notes?: string;
+}

+ 44 - 0
node_app/src/application/use-cases/reservation/find-reservations-by-trip.use-case.ts

@@ -0,0 +1,44 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+import { ReservationMapper } from '../../../infrastructure/mappers/reservation.mapper';
+import { PaginatedResponseDto } from '../../dto/responses/paginated-response.dto';
+import { TripReservationResponseDto } from '../../dto/responses/trip-reservation-response.dto';
+
+@Injectable()
+export class FindReservationsByTripUseCase {
+  constructor(
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: ReservationRepository,
+    private readonly reservationMapper: ReservationMapper,
+  ) {}
+
+  async execute(
+    tripId: number,
+    page = 1,
+    limit = 50,
+    status?: string,
+  ): Promise<PaginatedResponseDto<TripReservationResponseDto>> {
+    const result = await this.reservationRepository.findByTripId(
+      tripId,
+      page,
+      limit,
+      status,
+    );
+
+    // Mapper les réservations vers le DTO spécifique
+    const mappedData = result.data.map((reservation) =>
+      this.reservationMapper.toTripReservationResponseDto(reservation),
+    );
+
+    // Calculer le nombre total de pages
+    const totalPages = Math.ceil(result.total / limit);
+
+    return {
+      data: mappedData,
+      total: result.total,
+      page,
+      limit,
+      totalPages,
+    };
+  }
+}

+ 62 - 0
node_app/src/infrastructure/database/repositories/reservation.repository.ts

@@ -227,4 +227,66 @@ export class ReservationRepository implements IBaseRepository<Reservation> {
       total,
     };
   }
+
+  async findByTripId(
+    tripId: number,
+    page = 1,
+    limit = 50,
+    status?: string,
+  ): Promise<{ data: Reservation[]; total: number }> {
+    const skip = (page - 1) * limit;
+
+    // Construction de la condition where
+    const whereCondition: any = { trajetId: tripId };
+    if (status) {
+      whereCondition.status = status;
+    }
+
+    const [data, total] = await Promise.all([
+      this.prisma.reservation.findMany({
+        where: whereCondition,
+        skip,
+        take: limit,
+        include: {
+          place: {
+            include: {
+              vehicule: {
+                include: {
+                  company: true,
+                  driver: true,
+                },
+              },
+            },
+          },
+          user: true,
+          trajet: {
+            include: {
+              vehicule: {
+                include: {
+                  company: true,
+                  driver: true,
+                },
+              },
+              departStation: true,
+              arriveStation: true,
+            },
+          },
+        },
+        orderBy: {
+          dateAt: 'desc', // Tri par date de réservation décroissante
+        },
+      }),
+
+      this.prisma.reservation.count({
+        where: whereCondition,
+      }),
+    ]);
+
+    return {
+      data: data.map((reservation) =>
+        this.reservationMapper.toDomain(reservation),
+      ),
+      total,
+    };
+  }
 }

+ 49 - 0
node_app/src/infrastructure/mappers/reservation.mapper.ts

@@ -5,6 +5,10 @@ import {
   PaymentMethod,
 } from 'src/domain/entities/reservation.entity';
 import { User, UserRole } from 'src/domain/entities/user.entity';
+import {
+  TripReservationResponseDto,
+  ReservationStatus,
+} from 'src/application/dto/responses/trip-reservation-response.dto';
 
 export class ReservationMapper implements IPrismaMapper<any, Reservation> {
   toDomain(prismaReservation: any): Reservation {
@@ -224,4 +228,49 @@ export class ReservationMapper implements IPrismaMapper<any, Reservation> {
         : undefined,
     };
   }
+
+  toTripReservationResponseDto(
+    reservation: Reservation,
+  ): TripReservationResponseDto {
+    const dto = new TripReservationResponseDto();
+    dto.id = reservation.id;
+    dto.dateAt = reservation.dateAt;
+    dto.status = ReservationStatus.CONFIRMED; // Par défaut, peut être ajusté selon la logique métier
+    dto.paymentMethod = reservation.paymentMethod;
+
+    // Place est requis selon le DTO, on fournit une valeur par défaut si null
+    dto.place = reservation.place
+      ? ({
+          id: reservation.place.id,
+          number: reservation.place.number,
+          isAvailable: reservation.place.isAvailable,
+        } as any)
+      : ({
+          id: 0,
+          number: 0,
+          isAvailable: false,
+        } as any);
+
+    // User est requis selon le DTO, on fournit une valeur par défaut si null
+    dto.user = reservation.user
+      ? ({
+          id: reservation.user.id,
+          name: reservation.user.name,
+          otherName: reservation.user.otherName,
+          email: reservation.user.email,
+          telephone: reservation.user.telephone,
+        } as any)
+      : ({
+          id: 0,
+          name: 'Utilisateur inconnu',
+          otherName: '',
+          email: '',
+          telephone: '',
+        } as any);
+
+    dto.price = reservation.trajet?.pricePerSeat || 0;
+    dto.notes = undefined; // Optionnel selon le DTO
+
+    return dto;
+  }
 }

+ 34 - 0
node_app/src/presentation/controllers/trajet.controller.ts

@@ -30,9 +30,11 @@ import { DeleteTrajetUseCase } from '../../application/use-cases/trajet/delete-t
 import { SearchTrajetsUseCase } from '../../application/use-cases/trajet/search-trajets.use-case';
 import { FindTripPassengersUseCase } from '../../application/use-cases/trajet/find-trip-passengers.use-case';
 import { CloseRegistrationUseCase } from '../../application/use-cases/trajet/close-registration.use-case';
+import { FindReservationsByTripUseCase } from '../../application/use-cases/reservation/find-reservations-by-trip.use-case';
 import { PaginationDto } from '../../application/dto/pagination.dto';
 import { PaginatedResponseDto } from '../../application/dto/responses/paginated-response.dto';
 import { TripPassengerResponseDto } from '../../application/dto/responses/trip-passenger-response.dto';
+import { TripReservationResponseDto } from '../../application/dto/responses/trip-reservation-response.dto';
 import { Roles } from '../decorators/roles.decorator';
 import { UserRole } from 'src/domain/entities/user.entity';
 import { TrajetNotFoundException } from '../../application/exceptions/trajet-not-found.exception';
@@ -52,6 +54,7 @@ export class TrajetController {
     private readonly searchTrajetsUseCase: SearchTrajetsUseCase,
     private readonly findTripPassengersUseCase: FindTripPassengersUseCase,
     private readonly closeRegistrationUseCase: CloseRegistrationUseCase,
+    private readonly findReservationsByTripUseCase: FindReservationsByTripUseCase,
   ) {}
 
   @Post()
@@ -236,6 +239,37 @@ export class TrajetController {
     }
   }
 
+  @Get(':id/reservations')
+  @ApiOperation({ summary: "Récupérer les réservations d'un trajet" })
+  @ApiParam({ name: 'id', description: 'ID du trajet', type: 'number' })
+  @ApiResponse({
+    status: 200,
+    description: 'Liste paginée des réservations du trajet',
+    type: PaginatedResponseDto<TripReservationResponseDto>,
+  })
+  @ApiResponse({ status: 404, description: 'Trajet non trouvé' })
+  @Roles(UserRole.ADMIN, UserRole.MANAGER, UserRole.EMPLOYEE, UserRole.DRIVER)
+  async getTripReservations(
+    @Param('id', ParseIntPipe) id: number,
+    @Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
+    @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 50,
+    @Query('status') status?: string,
+  ) {
+    try {
+      return await this.findReservationsByTripUseCase.execute(
+        id,
+        page,
+        limit,
+        status,
+      );
+    } catch (error) {
+      if (error instanceof TrajetNotFoundException) {
+        throw error;
+      }
+      throw error;
+    }
+  }
+
   @Patch(':id/close-registration')
   @ApiOperation({ summary: "Fermer l'enregistrement d'un trajet" })
   @ApiParam({ name: 'id', description: 'ID du trajet', type: 'number' })