Jelajahi Sumber

(feat) fermer les enrégistrements

Brin AMONKONAN 1 tahun lalu
induk
melakukan
f0e8202812
22 mengubah file dengan 1318 tambahan dan 211 penghapusan
  1. 65 12
      app_transport_front/src/components/employee/QRScanner.vue
  2. 216 86
      app_transport_front/src/components/employee/TripDetails.vue
  3. 207 0
      app_transport_front/src/docs/QRCODE_SCAN_FIX.md
  4. 48 6
      app_transport_front/src/services/EmployeeService.ts
  5. 35 1
      app_transport_front/src/services/TripService.ts
  6. 299 80
      app_transport_front/src/views/EmployeeDashboard.vue
  7. 2 0
      node_app/prisma/migrations/20250920103348_add_registration_closed_to_trajet/migration.sql
  8. 12 11
      node_app/prisma/schema.prisma
  9. 8 4
      node_app/src/app.module.ts
  10. 22 0
      node_app/src/application/dto/qrcode.dto.ts
  11. 3 0
      node_app/src/application/dto/responses/trajet-response.dto.ts
  12. 36 0
      node_app/src/application/dto/responses/trip-passenger-response.dto.ts
  13. 17 1
      node_app/src/application/use-cases/reservation/create-reservation.use-case.ts
  14. 30 0
      node_app/src/application/use-cases/trajet/close-registration.use-case.ts
  15. 54 0
      node_app/src/application/use-cases/trajet/find-trip-passengers.use-case.ts
  16. 1 0
      node_app/src/domain/entities/trajet.entity.ts
  17. 97 0
      node_app/src/infrastructure/database/repositories/trajet.repository.ts
  18. 3 0
      node_app/src/infrastructure/mappers/trajet.mapper.ts
  19. 1 8
      node_app/src/presentation/controllers/qrcode.controller.ts
  20. 65 2
      node_app/src/presentation/controllers/trajet.controller.ts
  21. 6 0
      node_app/src/presentation/modules/reservation.module.ts
  22. 91 0
      node_app/test-close-registration.js

+ 65 - 12
app_transport_front/src/components/employee/QRScanner.vue

@@ -44,7 +44,7 @@
             </div>
             <div class="text-right">
               <p class="text-sm text-blue-700">Passagers validés</p>
-              <p class="text-2xl font-bold text-blue-900">{{ validatedCount }}/{{ trip.passengers }}</p>
+              <p class="text-2xl font-bold text-blue-900">{{ displayStats.validated }}/{{ displayStats.total }}</p>
             </div>
           </div>
         </div>
@@ -250,7 +250,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, onUnmounted, computed } from 'vue'
+import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
 import EmployeeService, { type TicketValidationResult } from '@/services/EmployeeService'
 
 // Props
@@ -266,8 +266,16 @@ interface Trip {
   status: string
 }
 
+interface TicketStats {
+  total: number
+  validated: number
+  pending: number
+  cancelled: number
+}
+
 interface Props {
   trip?: Trip
+  ticketStats?: TicketStats
 }
 
 const props = defineProps<Props>()
@@ -285,7 +293,6 @@ const cameraError = ref('')
 const manualCode = ref('')
 const showValidationModal = ref(false)
 const validationResult = ref({ success: false, message: '' })
-const validatedCount = ref(0)
 
 interface ScanRecord {
   id: number
@@ -312,6 +319,31 @@ const failedScans = computed(() =>
   scanHistory.value.filter(scan => !scan.success).length
 )
 
+// Computed pour les statistiques d'affichage
+const displayStats = computed(() => {
+  console.log('🎯 Calcul displayStats:', {
+    ticketStats: props.ticketStats,
+    trip: props.trip
+  })
+  
+  if (props.ticketStats && props.ticketStats.total > 0) {
+    return {
+      validated: props.ticketStats.validated,
+      total: props.ticketStats.total
+    }
+  } else if (props.trip) {
+    return {
+      validated: props.ticketStats?.validated || 0,
+      total: props.trip.passengers
+    }
+  }
+  
+  return {
+    validated: 0,
+    total: 0
+  }
+})
+
 // Méthodes
 const toggleCamera = () => {
   if (isCameraActive.value) {
@@ -388,7 +420,7 @@ const validateQRCode = async (qrCode: string) => {
   try {
     // Utilisation de l'API réelle pour valider le ticket
     const response = await validateTicketAPI(qrCode)
-    
+    console.log('Response from API ga:', response)
     const scanRecord: ScanRecord = {
       id: Date.now(),
       timestamp: new Date().toISOString(),
@@ -399,7 +431,6 @@ const validateQRCode = async (qrCode: string) => {
     if (response.success) {
       scanRecord.passengerName = response.passengerName
       scanRecord.seatNumber = response.seatNumber
-      validatedCount.value++
       
       validationResult.value = {
         success: true,
@@ -408,10 +439,10 @@ const validateQRCode = async (qrCode: string) => {
       
       emit('ticket-validated', response)
     } else {
-      scanRecord.error = response.error
+      scanRecord.error = response.error || 'Erreur inconnue'
       validationResult.value = {
         success: false,
-        message: response.error
+        message: response.error || 'Erreur inconnue'
       }
     }
     
@@ -465,18 +496,21 @@ const closeValidationModal = () => {
 const validateTicketAPI = async (qrCode: string) => {
   try {
     const result = await EmployeeService.validateTicket(qrCode, props.trip?.id)
-    
     if (result.success) {
+      // La structure de réponse du backend est différente
+      // result contient { success: true, message: string, passenger?: object }
+      // mais pas de result.data.qrcode
       return {
         success: true,
-        passengerName: `${result.passenger.firstName} ${result.passenger.lastName}`,
-        seatNumber: result.passenger.seatNumber,
-        ticketId: qrCode
+        passengerName: result.passenger?.name || 'Passager inconnu',
+        seatNumber: result.passenger?.seatNumber || 'N/A',
+        ticketId: qrCode,
+        reservation: result.passenger
       }
     } else {
       return {
         success: false,
-        error: result.message || 'Ticket invalide'
+        error: result.message || result.error || 'Ticket invalide'
       }
     }
   } catch (error) {
@@ -512,9 +546,28 @@ const formatTime = (timestamp: string) => {
   })
 }
 
+// Watchers pour debug
+watch(() => props.ticketStats, (newStats, oldStats) => {
+  console.log('🔄 QRScanner - ticketStats changé:', {
+    ancien: oldStats,
+    nouveau: newStats
+  })
+}, { deep: true, immediate: true })
+
+watch(() => props.trip, (newTrip, oldTrip) => {
+  console.log('🔄 QRScanner - trip changé:', {
+    ancien: oldTrip,
+    nouveau: newTrip
+  })
+}, { deep: true, immediate: true })
+
 // Lifecycle
 onMounted(() => {
   // Vérifier la disponibilité de la caméra
+  console.log('🎬 QRScanner monté avec props:', props)
+  console.log('📊 Stats reçues:', props.ticketStats)
+  console.log('🚌 Trip reçu:', props.trip)
+  
   if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
     cameraError.value = 'Votre navigateur ne supporte pas l\'accès à la caméra'
   }

+ 216 - 86
app_transport_front/src/components/employee/TripDetails.vue

@@ -1,5 +1,17 @@
 <template>
-  <div class="space-y-6">
+  <div class="space-y-6" :class="{ 'opacity-50': tripDetails?.registrationClosed }">
+
+    <!-- Bannière d'information pour trajet fermé -->
+    <div v-if="tripDetails?.registrationClosed" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md mb-4">
+      <div class="flex items-center">
+        <i class="fas fa-lock mr-3 text-red-500"></i>
+        <div>
+          <p class="font-semibold">Enregistrement fermé</p>
+          <p class="text-sm">Les enregistrements pour ce trajet sont fermés. Aucune nouvelle réservation ne peut être effectuée.</p>
+        </div>
+      </div>
+    </div>
+
     <!-- En-tête avec navigation -->
     <div class="bg-white shadow rounded-lg">
       <div class="px-4 py-5 sm:p-6">
@@ -15,14 +27,25 @@
           <div class="flex space-x-3">
             <button
               @click="$emit('scan-tickets', trip)"
-              class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
+              :disabled="tripDetails?.registrationClosed"
+              class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400"
             >
               <i class="fas fa-qrcode mr-2"></i>
               Scanner les tickets
             </button>
+            <button
+              v-if="!tripDetails?.registrationClosed && tripDetails?.status !== 'completed' && tripDetails?.status !== 'cancelled'"
+              @click="closeRegistration"
+              :disabled="isClosingRegistration"
+              class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              <i class="fas fa-lock mr-2" :class="{ 'fa-spin': isClosingRegistration }"></i>
+              {{ isClosingRegistration ? 'Fermeture...' : 'Fin d\'enregistrement' }}
+            </button>
             <button
               @click="refreshTripData"
-              class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+              :disabled="tripDetails?.registrationClosed"
+              class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400"
             >
               <i class="fas fa-sync-alt mr-2"></i>
               Actualiser
@@ -30,11 +53,26 @@
           </div>
         </div>
 
+        <!-- Messages d'erreur et de succès -->
+        <div v-if="errorMessage" class="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded-md">
+          <div class="flex items-center">
+            <i class="fas fa-exclamation-triangle mr-2"></i>
+            {{ errorMessage }}
+          </div>
+        </div>
+        
+        <div v-if="successMessage" class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded-md">
+          <div class="flex items-center">
+            <i class="fas fa-check-circle mr-2"></i>
+            {{ successMessage }}
+          </div>
+        </div>
+
         <!-- Informations principales du trajet -->
         <div v-if="tripDetails" class="border-b border-gray-200 pb-6">
           <div class="flex items-center justify-between mb-4">
             <h1 class="text-2xl font-bold text-gray-900">
-              {{ tripDetails.route.departureStation.city }} → {{ tripDetails.route.arrivalStation.city }}
+              {{ tripDetails.departure }} → {{ tripDetails.arrival }}
             </h1>
             <span 
               :class="getStatusBadgeClass(tripDetails.status)"
@@ -85,7 +123,7 @@
                 <i class="fas fa-bus text-yellow-500 text-xl mr-3"></i>
                 <div>
                   <p class="text-sm font-medium text-gray-500">Véhicule</p>
-                  <p class="text-lg font-semibold text-gray-900">{{ tripDetails.vehicle.model }} - {{ tripDetails.vehicle.licensePlate }}</p>
+                  <p class="text-lg font-semibold text-gray-900">{{ tripDetails.vehicle?.model || 'N/A' }} - {{ tripDetails.vehicle?.licensePlate || 'N/A' }}</p>
                 </div>
               </div>
             </div>
@@ -109,7 +147,7 @@
     </div>
 
     <!-- Statistiques des tickets -->
-    <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
+    <div class="grid grid-cols-1 md:grid-cols-4 gap-6" :class="{ 'pointer-events-none': tripDetails?.registrationClosed }">
       <div class="bg-white overflow-hidden shadow rounded-lg">
         <div class="p-5">
           <div class="flex items-center">
@@ -119,7 +157,7 @@
             <div class="ml-5 w-0 flex-1">
               <dl>
                 <dt class="text-sm font-medium text-gray-500 truncate">Total tickets</dt>
-                <dd class="text-lg font-medium text-gray-900">{{ ticketStats.total }}</dd>
+                <dd class="text-lg font-medium text-gray-900">{{ statistics.total }}</dd>
               </dl>
             </div>
           </div>
@@ -135,7 +173,7 @@
             <div class="ml-5 w-0 flex-1">
               <dl>
                 <dt class="text-sm font-medium text-gray-500 truncate">Validés</dt>
-                <dd class="text-lg font-medium text-gray-900">{{ ticketStats.validated }}</dd>
+                <dd class="text-lg font-medium text-gray-900">{{ statistics.validated }}</dd>
               </dl>
             </div>
           </div>
@@ -151,7 +189,7 @@
             <div class="ml-5 w-0 flex-1">
               <dl>
                 <dt class="text-sm font-medium text-gray-500 truncate">En attente</dt>
-                <dd class="text-lg font-medium text-gray-900">{{ ticketStats.pending }}</dd>
+                <dd class="text-lg font-medium text-gray-900">{{ statistics.pending }}</dd>
               </dl>
             </div>
           </div>
@@ -167,7 +205,7 @@
             <div class="ml-5 w-0 flex-1">
               <dl>
                 <dt class="text-sm font-medium text-gray-500 truncate">Annulés</dt>
-                <dd class="text-lg font-medium text-gray-900">{{ ticketStats.cancelled }}</dd>
+                <dd class="text-lg font-medium text-gray-900">{{ statistics.cancelled }}</dd>
               </dl>
             </div>
           </div>
@@ -176,7 +214,7 @@
     </div>
 
     <!-- Liste des passagers -->
-    <div class="bg-white shadow rounded-lg">
+    <div class="bg-white shadow rounded-lg" :class="{ 'pointer-events-none': tripDetails?.registrationClosed }">
       <div class="px-4 py-5 sm:p-6">
         <div class="flex items-center justify-between mb-6">
           <h3 class="text-lg leading-6 font-medium text-gray-900">
@@ -255,14 +293,14 @@
                 </td>
                 <td class="px-6 py-4 whitespace-nowrap">
                   <span 
-                    :class="getTicketStatusClass(passenger.ticketStatus)"
+                    :class="getTicketStatusClass(passenger.ticketStatus || passenger.status)"
                     class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
                   >
                     <div 
-                      :class="getTicketStatusColor(passenger.ticketStatus)"
+                      :class="getTicketStatusColor(passenger.ticketStatus || passenger.status)"
                       class="w-1.5 h-1.5 rounded-full mr-1.5"
                     ></div>
-                    {{ getTicketStatusText(passenger.ticketStatus) }}
+                    {{ getTicketStatusText(passenger.ticketStatus || passenger.status) }}
                   </span>
                 </td>
                 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@@ -296,107 +334,125 @@
 
 <script setup lang="ts">
 import { ref, computed, onMounted, watch } from 'vue'
-import EmployeeService, { type TripDetails, type TripPassenger } from '@/services/EmployeeService'
+import EmployeeService, { type AssignedTrip, type TripPassenger } from '@/services/EmployeeService'
 
 // Props
 const props = defineProps<{
-  tripId: number
+  trip: AssignedTrip
 }>()
 
-// État réactif
+// Reactive data
 const isLoading = ref(false)
+const isClosingRegistration = ref(false)
+const errorMessage = ref('')
+const successMessage = ref('')
 const searchQuery = ref('')
 const statusFilter = ref('all')
 const currentPage = ref(1)
 const itemsPerPage = ref(10)
-const tripDetails = ref<TripDetails | null>(null)
+const tripDetails = ref<AssignedTrip | null>(null)
 const passengers = ref<TripPassenger[]>([])
 const totalPassengers = ref(0)
 const totalPages = ref(1)
 const passengerFilter = ref('')
 const passengerSearch = ref('')
 
-// Computed
-const paginatedPassengers = computed(() => {
-  return passengers.value
-})
-
+// Computed properties
 const statistics = computed(() => {
-  if (!tripDetails.value) return { total: 0, validated: 0, pending: 0, validationRate: 0 }
+  if (!tripDetails.value) return { 
+    total: 0, 
+    validated: 0, 
+    pending: 0, 
+    cancelled: 0,
+    validationRate: 0 
+  }
   
   const total = passengers.value.length
-  const validated = passengers.value.filter(p => p.status === 'validated').length
-  const pending = passengers.value.filter(p => p.status === 'pending').length
+  const validated = passengers.value.filter(p => 
+    p.ticketStatus === 'validated' || p.status === 'validated'
+  ).length
+  const pending = passengers.value.filter(p => 
+    p.ticketStatus === 'valid' || p.ticketStatus === 'pending' || p.status === 'pending'
+  ).length
+  const cancelled = passengers.value.filter(p => 
+    p.ticketStatus === 'cancelled' || p.status === 'cancelled'
+  ).length
   
   return {
     total,
     validated,
     pending,
+    cancelled,
     validationRate: total > 0 ? Math.round((validated / total) * 100) : 0
   }
 })
 
-const ticketStats = computed(() => {
-  const total = passengers.value.length
-  const validated = passengers.value.filter(p => p.ticketStatus === 'validated').length
-  const pending = passengers.value.filter(p => p.ticketStatus === 'pending').length
-  const cancelled = passengers.value.filter(p => p.ticketStatus === 'cancelled').length
-  
-  return { total, validated, pending, cancelled }
-})
-
 const filteredPassengers = computed(() => {
   let filtered = passengers.value
 
   // Filtre par statut
   if (passengerFilter.value) {
-    filtered = filtered.filter(p => p.ticketStatus === passengerFilter.value)
+    filtered = filtered.filter(p => (p.ticketStatus || p.status) === passengerFilter.value)
   }
 
   // Filtre par recherche
   if (passengerSearch.value) {
     const query = passengerSearch.value.toLowerCase()
-    filtered = filtered.filter(p => 
-      p.name.toLowerCase().includes(query) ||
-      p.email.toLowerCase().includes(query) ||
-      p.seatNumber.toLowerCase().includes(query)
-    )
+    filtered = filtered.filter(p => {
+      const fullName = `${p.firstName || p.name || ''} ${p.lastName || p.otherName || ''}`.toLowerCase()
+      const email = (p.email || '').toLowerCase()
+      const seatNumber = (p.seatNumber || '').toLowerCase()
+      const ticketNumber = (p.ticketNumber || '').toLowerCase()
+      
+      return fullName.includes(query) ||
+             email.includes(query) ||
+             seatNumber.includes(query) ||
+             ticketNumber.includes(query)
+    })
   }
 
   return filtered
 })
 
-// Méthodes
+// Methods
+const createTripDetailsObject = (tripData: any): AssignedTrip => ({
+  id: tripData.id,
+  departure: tripData.departure,
+  arrival: tripData.arrival,
+  departureTime: tripData.departureTime,
+  arrivalTime: tripData.arrivalTime,
+  date: tripData.date,
+  status: tripData.status,
+  registrationClosed: tripData.registrationClosed || false,
+  vehicle: tripData.vehicle,
+  company: tripData.company,
+  route: tripData.route,
+  driver: tripData.driver,
+  totalSeats: tripData.capacity || tripData.vehicle?.capacity || 0,
+  occupiedSeats: tripData.passengers || 0,
+  revenue: 0
+})
+
 const loadTripDetails = async () => {
-  isLoading.value = true
+  if (!props.trip) {
+    console.warn('props.trip n\'est pas encore disponible')
+    return
+  }
+  
+  // Initialiser immédiatement avec les données du prop
+  tripDetails.value = createTripDetailsObject(props.trip)
+  
   try {
-    const details = await EmployeeService.getTripDetails(props.tripId)
-    tripDetails.value = details
+    const tripData = await EmployeeService.getTripDetails(props.trip.id)
+    
+    if (tripData) {
+      tripDetails.value = createTripDetailsObject(tripData)
+      console.log('🔍 DEBUG - tripDetails après chargement API:', tripDetails.value)
+      await loadPassengers()
+    }
   } catch (error) {
     console.error('Erreur lors du chargement des détails du trajet:', error)
-    // Fallback avec des données de démonstration
-    tripDetails.value = {
-      id: props.tripId,
-      departure: 'Abidjan',
-      arrival: 'Bouaké',
-      departureTime: '08:00',
-      arrivalTime: '11:30',
-      date: '2024-01-15',
-      status: 'in_progress',
-      vehicle: { id: 1, licensePlate: 'CI-001-AB', model: 'Mercedes Sprinter', capacity: 50 },
-      company: { id: 1, name: 'Transport Express' },
-      route: {
-        id: 1,
-        departureStation: { id: 1, name: 'Gare d\'Abidjan', city: 'Abidjan' },
-        arrivalStation: { id: 2, name: 'Gare de Bouaké', city: 'Bouaké' }
-      },
-      driver: { id: 1, firstName: 'Jean', lastName: 'Kouassi', phone: '+225 07 12 34 56 78' },
-      totalSeats: 50,
-      occupiedSeats: 45,
-      revenue: 225000
-    }
-  } finally {
-    isLoading.value = false
+    // tripDetails.value est déjà initialisé avec les données du prop
   }
 }
 
@@ -443,6 +499,7 @@ const getStatusText = (status: string) => {
 const getTicketStatusColor = (status: string) => {
   const colors = {
     'validated': 'bg-green-500',
+    'valid': 'bg-blue-500',
     'pending': 'bg-yellow-500',
     'cancelled': 'bg-red-500'
   }
@@ -452,6 +509,7 @@ const getTicketStatusColor = (status: string) => {
 const getTicketStatusClass = (status: string) => {
   const classes = {
     'validated': 'bg-green-100 text-green-800',
+    'valid': 'bg-blue-100 text-blue-800',
     'pending': 'bg-yellow-100 text-yellow-800',
     'cancelled': 'bg-red-100 text-red-800'
   }
@@ -461,6 +519,7 @@ const getTicketStatusClass = (status: string) => {
 const getTicketStatusText = (status: string) => {
   const texts = {
     'validated': 'Validé',
+    'valid': 'Valide',
     'pending': 'En attente',
     'cancelled': 'Annulé'
   }
@@ -508,43 +567,62 @@ const viewPassengerDetails = (passenger: TripPassenger) => {
 
 const refreshTripData = async () => {
   try {
-    // Recharger les données du trajet depuis l'API
     console.log('Actualisation des données du trajet...')
+    await loadTripDetails()
+    await loadPassengers()
   } catch (error) {
     console.error('Erreur lors de l\'actualisation:', error)
   }
 }
 
-// Lifecycle
-onMounted(() => {
-  loadTripDetails()
-  loadPassengers()
-})
-
-// Watchers
-watch([searchQuery, statusFilter], () => {
-  applyFilters()
-})
+const closeRegistration = async () => {
+  if (!tripDetails.value?.id) {
+    console.error('Aucun trajet sélectionné');
+    return;
+  }
 
-watch(() => props.tripId, () => {
-  loadTripDetails()
-  loadPassengers()
-})
+  try {
+    const response = await EmployeeService.closeRegistration(tripDetails.value.id);
+    
+    if (response.success) {
+      // Mettre à jour les données locales
+      tripDetails.value.registrationClosed = true;
+      
+      // Recharger les données pour s'assurer de la cohérence
+      await loadTripDetails();
+      
+      // Afficher un message de succès
+      console.log('Enregistrement fermé avec succès');
+    } else {
+      console.error('Erreur lors de la fermeture de l\'enregistrement:', response.message);
+    }
+  } catch (error) {
+    console.error('Erreur lors de la fermeture de l\'enregistrement:', error);
+  }
+};
 
 const loadPassengers = async () => {
+  if (!props.trip) {
+    console.warn('props.trip n\'est pas encore disponible pour loadPassengers')
+    return
+  }
+  
   try {
     const status = statusFilter.value === 'all' ? undefined : statusFilter.value
     const search = searchQuery.value || undefined
 
     const result = await EmployeeService.getTripPassengers(
-      props.tripId,
+      props.trip.id,
       currentPage.value,
       itemsPerPage.value,
       status,
       search
     )
     
-    passengers.value = result.passengers
+    passengers.value = result.passengers.map(passenger => ({
+      ...passenger,
+      status: passenger.status || passenger.ticketStatus || 'pending'
+    }))
     totalPassengers.value = result.pagination.total
     totalPages.value = result.pagination.totalPages
   } catch (error) {
@@ -559,15 +637,67 @@ const loadPassengers = async () => {
         email: 'marie.kone@email.com',
         seatNumber: 'A1',
         ticketNumber: 'TK001234',
+        ticketStatus: 'validated',
         status: 'validated',
         validatedAt: '2024-01-15T07:45:00Z',
         qrCode: 'QR001234'
+      },
+      {
+        id: 2,
+        firstName: 'Jean',
+        lastName: 'Kouassi',
+        phone: '+225 07 23 45 67 89',
+        email: 'jean.kouassi@email.com',
+        seatNumber: 'A2',
+        ticketNumber: 'TK001235',
+        ticketStatus: 'valid',
+        status: 'pending',
+        validatedAt: null,
+        qrCode: 'QR001235'
+      },
+      {
+        id: 3,
+        firstName: 'Fatou',
+        lastName: 'Traoré',
+        phone: '+225 07 34 56 78 90',
+        email: 'fatou.traore@email.com',
+        seatNumber: 'B1',
+        ticketNumber: 'TK001236',
+        ticketStatus: 'valid',
+        status: 'pending',
+        validatedAt: null,
+        qrCode: 'QR001236'
+      },
+      {
+        id: 4,
+        firstName: 'Amadou',
+        lastName: 'Diallo',
+        phone: '+225 07 45 67 89 01',
+        email: 'amadou.diallo@email.com',
+        seatNumber: 'B2',
+        ticketNumber: 'TK001237',
+        ticketStatus: 'cancelled',
+        status: 'cancelled',
+        validatedAt: null,
+        qrCode: 'QR001237'
       }
     ]
-    totalPassengers.value = 1
+    totalPassengers.value = 4
     totalPages.value = 1
   }
 }
+
+// Watchers
+onMounted(() => {
+  loadTripDetails()
+})
+
+watch(() => props.trip, (newTrip) => {
+  if (newTrip) {
+    loadTripDetails()
+    loadPassengers()
+  }
+}, { immediate: true })
 </script>
 
 <style scoped>

+ 207 - 0
app_transport_front/src/docs/QRCODE_SCAN_FIX.md

@@ -0,0 +1,207 @@
+# 🔧 Correction de l'erreur QR Code Scan API
+
+## 🚨 **Problème identifié**
+
+L'endpoint `/qrcode/scan` retournait des erreurs de validation :
+
+**Erreur 1 :**
+```json
+{
+  "message": [
+    "property qrCode should not exist",
+    "property tripId should not exist"
+  ],
+  "error": "Bad Request",
+  "statusCode": 400
+}
+```
+
+**Erreur 2 (après première correction) :**
+```json
+{
+  "message": [
+    "property value should not exist"
+  ],
+  "error": "Bad Request",
+  "statusCode": 400
+}
+```
+
+## 🔍 **Analyse du problème**
+
+### **Cause racine :**
+1. **Mauvaise structure de payload** : Le frontend envoyait `qrCode`, `tripId`, `validatedBy` au lieu de `value`
+2. **DTO sans validation** : Le `ScanQrcodeDto` n'avait pas de décorateurs de validation
+3. **Configuration stricte** : Le backend utilise `ValidationPipe` avec `whitelist: true` et `forbidNonWhitelisted: true`
+
+### **Configuration ValidationPipe (main.ts) :**
+```typescript
+app.useGlobalPipes(
+  new ValidationPipe({
+    whitelist: true,           // ✅ Filtre les propriétés sans décorateurs
+    transform: true,
+    forbidNonWhitelisted: true // ❌ Rejette les propriétés non autorisées
+  }),
+);
+```
+
+### **Structure attendue par l'API (Backend) :**
+```typescript
+// ScanQrcodeDto dans qrcode.controller.ts (AVANT correction)
+export class ScanQrcodeDto {
+  value: string; // ❌ Pas de décorateurs de validation
+}
+```
+
+### **Structure envoyée par le Frontend (AVANT correction) :**
+```typescript
+// EmployeeService.ts - validateTicket()
+body: JSON.stringify({
+  qrCode,        // ❌ Propriété non attendue
+  tripId,        // ❌ Propriété non attendue  
+  validatedBy: employeeId  // ❌ Propriété non attendue
+})
+```
+
+## ✅ **Solution appliquée**
+
+### **1. Correction du DTO Backend (qrcode.controller.ts)**
+
+**Avant :**
+```typescript
+export class ScanQrcodeDto {
+  value: string; // ❌ Pas de décorateurs de validation
+}
+```
+
+**Après :**
+```typescript
+export class ScanQrcodeDto {
+  @ApiProperty({
+    description: 'Valeur du QR code à scanner',
+    example: 'QR_abc123-def456-789_123',
+  })
+  @IsNotEmpty()
+  @IsString()
+  value: string; // ✅ Avec décorateurs de validation
+}
+```
+
+### **2. Correction du service frontend**
+
+**Avant :**
+```typescript
+async validateTicket(qrCode: string, tripId: number, employeeId: number): Promise<TicketValidationResult> {
+  try {
+    const response = await authenticatedFetch(`${API_URL}/qrcode/scan`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        qrCode,
+        tripId,
+        validatedBy: employeeId
+      })
+    })
+```
+
+**Après :**
+```typescript
+async validateTicket(qrCode: string, tripId?: number, employeeId?: number): Promise<TicketValidationResult> {
+  try {
+    const response = await authenticatedFetch(`${API_URL}/qrcode/scan`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        value: qrCode  // ✅ Seule propriété attendue
+      })
+    })
+```
+
+### **3. Modifications apportées**
+
+**Backend :**
+1. **Ajout des décorateurs de validation** : `@IsNotEmpty()` et `@IsString()` sur la propriété `value`
+2. **Documentation Swagger** : `@ApiProperty()` avec description et exemple
+3. **Import des validateurs** : Ajout de `IsString`, `IsNotEmpty`, `IsNumber` depuis `class-validator`
+
+**Frontend :**
+1. **Paramètres optionnels :** `tripId` et `employeeId` sont maintenant optionnels car ils ne sont plus utilisés dans l'appel API
+2. **Payload simplifié :** Envoi seulement de `{ value: qrCode }` comme attendu par l'API
+3. **Compatibilité maintenue :** L'appel existant dans `QRScanner.vue` continue de fonctionner
+
+## 🔄 **Flux de validation QR Code**
+
+### **Frontend → Backend :**
+```typescript
+// Frontend envoie
+{
+  "value": "QR_abc123_456"
+}
+
+// Backend traite avec ScanQrcodeUseCase
+// 1. Parse la valeur QR
+// 2. Trouve le QR code en base
+// 3. Vérifie s'il n'est pas déjà scanné
+// 4. Marque comme scanné
+// 5. Retourne le résultat
+```
+
+### **Réponse attendue :**
+```typescript
+{
+  "qrcode": {
+    "id": "abc123",
+    "reservationId": 456,
+    "value": "QR_abc123_456",
+    "scannedAt": "2024-01-15T10:30:00Z",
+    "reservation": { ... }
+  },
+  "message": "QR code scanné avec succès"
+}
+```
+
+## 🛡️ **Validation côté Backend**
+
+L'endpoint utilise le `ScanQrcodeUseCase` qui :
+
+1. **Valide le format :** `Qrcode.parseValue(request.value)`
+2. **Recherche en base :** `qrcodeRepository.findByValue(request.value)`
+3. **Vérifie l'état :** `qrcode.isScanned()`
+4. **Met à jour :** `qrcode.markAsScanned()`
+
+## 📝 **Format des QR Codes**
+
+Les QR codes suivent le format : `QR_{id}_{reservationId}`
+
+**Exemple :** `QR_abc123-def456-789_123`
+
+## ✅ **Résultat**
+
+- ✅ **Erreur 1 corrigée** : Mauvaise structure de payload (qrCode → value)
+- ✅ **Erreur 2 corrigée** : DTO sans décorateurs de validation
+- ✅ L'endpoint `/qrcode/scan` fonctionne correctement
+- ✅ La compatibilité avec le composant `QRScanner` est maintenue
+- ✅ Les paramètres inutilisés sont maintenant optionnels
+- ✅ Validation stricte respectée (whitelist + forbidNonWhitelisted)
+
+## 🚀 **Test de l'endpoint**
+
+```bash
+# Test avec curl
+curl -X POST http://localhost:3001/api/qrcode/scan \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer YOUR_TOKEN" \
+  -d '{"value": "QR_abc123_456"}'
+```
+
+## 📋 **Checklist de validation**
+
+- [x] DTO conforme à la structure attendue
+- [x] Paramètres optionnels pour la rétrocompatibilité
+- [x] Gestion d'erreurs maintenue
+- [x] Documentation mise à jour
+- [x] Tests fonctionnels validés

+ 48 - 6
app_transport_front/src/services/EmployeeService.ts

@@ -12,6 +12,7 @@ export interface AssignedTrip {
   passengers: number
   capacity: number
   status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
+  registrationClosed?: boolean
   vehicle: {
     id: number
     licensePlate: string
@@ -200,17 +201,15 @@ export default {
   /**
    * Valider un ticket via QR code
    */
-  async validateTicket(qrCode: string, tripId: number, employeeId: number): Promise<TicketValidationResult> {
+  async validateTicket(qrCode: string, tripId?: number, employeeId?: number): Promise<TicketValidationResult> {
     try {
-      const response = await authenticatedFetch(`${API_URL}/tickets/validate`, {
+      const response = await authenticatedFetch(`${API_URL}/qrcode/scan`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json'
         },
         body: JSON.stringify({
-          qrCode,
-          tripId,
-          validatedBy: employeeId
+          value: qrCode
         })
       })
       
@@ -224,10 +223,25 @@ export default {
         }
       }
       
+      // La réponse du backend a la structure : { qrcode: {...}, message: "..." }
+      // Extraire les informations du passager depuis l'objet qrcode
+      const qrcode = result.qrcode
+      let passenger = undefined
+      
+      if (qrcode && qrcode.reservation) {
+        passenger = {
+          id: qrcode.reservation.user?.id,
+          name: `${qrcode.reservation.user?.name || ''} ${qrcode.reservation.user?.otherName || ''}`.trim(),
+          otherName: qrcode.reservation.user?.otherName,
+          seatNumber: qrcode.reservation.place?.number || 'N/A',
+          ticketId: qrCode
+        }
+      }
+      
       return {
         success: true,
         message: result.message || 'Ticket validé avec succès',
-        passenger: result.data?.passenger || result.passenger
+        passenger: passenger
       }
     } catch (error) {
       console.error('Erreur lors de la validation du ticket:', error)
@@ -403,5 +417,33 @@ export default {
         message: 'Erreur de connexion au serveur'
       }
     }
+  },
+
+  /**
+   * Fermer l'enregistrement d'un trajet
+   */
+  async closeRegistration(tripId: number): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/trajets/${tripId}/close-registration`, {
+        method: 'PATCH',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({})
+      })
+      
+      const result = await response.json()
+      
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Enregistrement fermé avec succès' : 'Erreur lors de la fermeture de l\'enregistrement')
+      }
+    } catch (error) {
+      console.error('Erreur lors de la fermeture de l\'enregistrement:', error)
+      return {
+        success: false,
+        message: 'Erreur de connexion au serveur'
+      }
+    }
   }
 }

+ 35 - 1
app_transport_front/src/services/TripService.ts

@@ -1,7 +1,7 @@
 import type { Trip } from '@/types'
 import { authenticatedFetch } from '@/services/authService'
 
-const API_URL = 'http://localhost:3001/api'
+const API_URL = (import.meta as any).env.VITE_API_BASE_URL
 
 export default {
   async getTrips(page: number = 1, limit: number = 10): Promise<{ trips: Trip[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {
@@ -231,5 +231,39 @@ export default {
         pagination: { total: 0, page: 1, limit: 10, totalPages: 1 }
       }
     }
+  },
+
+  async getTripPassengers(tripId: number, page: number = 1, limit: number = 50, search?: string, status?: string): Promise<{ passengers: any[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {
+    try {
+      let url = `${API_URL}/trajets/${tripId}/passengers?page=${page}&limit=${limit}`
+      if (search) {
+        url += `&search=${encodeURIComponent(search)}`
+      }
+      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 passagers du trajet')
+      }
+      const result = await response.json()
+      console.log('📦 Résultat de l\'API getTripPassengers:', result)
+      return {
+        passengers: 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 {
+        passengers: [],
+        pagination: { total: 0, page: 1, limit: 50, totalPages: 1 }
+      }
+    }
   }
 }

+ 299 - 80
app_transport_front/src/views/EmployeeDashboard.vue

@@ -138,7 +138,7 @@
           <div class="px-4 py-5 sm:p-6">
             <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
               <i class="fas fa-calendar-day mr-2 text-blue-600"></i>
-              Trajets d'aujourd'hui
+              Trajets d'aujourd'hui 
             </h3>
             <div v-if="todayTrips.length === 0" class="text-center py-8">
               <i class="fas fa-calendar-times text-gray-400 text-4xl mb-4"></i>
@@ -148,30 +148,73 @@
               <div
                 v-for="trip in todayTrips"
                 :key="trip.id"
-                class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer"
+                :class="[
+                  'border rounded-lg p-4 transition-all duration-200',
+                  trip.registrationClosed 
+                    ? 'border-gray-300 bg-gray-50 opacity-60 cursor-pointer' 
+                    : 'border-gray-200 hover:shadow-md cursor-pointer'
+                ]"
                 @click="selectTrip(trip)"
               >
                 <div class="flex items-center justify-between">
                   <div class="flex-1">
                     <div class="flex items-center space-x-4">
                       <div class="flex-shrink-0">
-                        <div class="w-3 h-3 bg-green-500 rounded-full"></div>
+                        <div 
+                          :class="[
+                            'w-3 h-3 rounded-full',
+                            trip.registrationClosed ? 'bg-gray-400' : 'bg-green-500'
+                          ]"
+                        ></div>
                       </div>
                       <div class="flex-1 min-w-0">
-                        <p class="text-sm font-medium text-gray-900 truncate">
+                        <p 
+                          :class="[
+                            'text-sm font-medium truncate',
+                            trip.registrationClosed ? 'text-gray-500' : 'text-gray-900'
+                          ]"
+                        >
                           {{ trip.departure }} → {{ trip.arrival }}
+                          <span v-if="trip.registrationClosed" class="ml-2 text-xs font-normal text-red-500">
+                            (Enregistrement fermé)
+                          </span>
                         </p>
-                        <p class="text-sm text-gray-500">
+                        <p 
+                          :class="[
+                            'text-sm',
+                            trip.registrationClosed ? 'text-gray-400' : 'text-gray-500'
+                          ]"
+                        >
                           {{ formatTime(trip.departureTime) }} - {{ formatTime(trip.arrivalTime) }}
                         </p>
+                        <p v-if="trip.vehicule" 
+                          :class="[
+                            'text-xs mt-1',
+                            trip.registrationClosed ? 'text-gray-400' : 'text-gray-400'
+                          ]"
+                        >
+                          {{ trip.vehicule.type }} - {{ trip.vehicule.immatriculation }}
+                        </p>
                       </div>
                     </div>
                   </div>
                   <div class="flex items-center space-x-4">
-                    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
-                      {{ trip.passengers }}/{{ trip.capacity }} passagers
+                    <span 
+                      :class="[
+                        'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
+                        trip.registrationClosed 
+                          ? 'bg-gray-200 text-gray-600' 
+                          : 'bg-blue-100 text-blue-800'
+                      ]"
+                    >
+                      {{ trip.passengers }}/{{ trip.vehicule?.number_places || trip.capacity || 'N/A' }} passagers
                     </span>
-                    <i class="fas fa-chevron-right text-gray-400"></i>
+                    <i 
+                      :class="[
+                        'fas fa-chevron-right',
+                        trip.registrationClosed ? 'text-gray-300' : 'text-gray-400'
+                      ]"
+                    ></i>
                   </div>
                 </div>
               </div>
@@ -202,6 +245,7 @@
       <div v-if="activeTab === 'scanner'">
         <QRScanner 
           :trip="selectedTrip"
+          :ticketStats="ticketStats"
           @ticket-validated="onTicketValidated"
           @back="activeTab = selectedTrip ? 'trip-details' : 'trips'"
         />
@@ -252,9 +296,18 @@ const userStore = useUserStore()
 
 // État réactif
 const activeTab = ref('overview')
-const selectedTrip = ref(null)
+const selectedTrip = ref<AssignedTrip | null>(null)
 const showValidationModal = ref(false)
 const isLoading = ref(false)
+const selectedTripPassengers = ref<any[]>([])
+
+// Interface pour les statistiques des tickets
+interface TicketStats {
+  total: number
+  validated: number
+  pending: number
+  cancelled: number
+}
 
 // Données simulées (à remplacer par des appels API)
 const stats = ref({
@@ -262,25 +315,12 @@ const stats = ref({
   scannedTickets: 23,
   todayPassengers: 45,
 })
-
-const tripsDay = ref<AssignedTrip[] | any>([])
-
 // Trajets de l'entreprise (remplace les données statiques)
-const todayTrips = ref([])
-
-const allTrips = ref([
-  ...todayTrips.value,
-  {
-    id: 3,
-    departure: 'Abidjan',
-    arrival: 'Yamoussoukro',
-    departureTime: '09:00',
-    arrivalTime: '12:00',
-    passengers: 20,
-    capacity: 30,
-    status: 'scheduled',
-    date: '2024-01-16'
-  }
+const todayTrips = ref<AssignedTrip[]>([])
+const companyTrips = ref<AssignedTrip[]>([])
+
+const allTrips = ref<AssignedTrip[]>([
+  ...todayTrips.value
 ])
 
 // Onglets de navigation
@@ -299,18 +339,46 @@ const nextTripTime = computed(() => {
   return 'Aucun'
 })
 
+const ticketStats = computed((): TicketStats => {
+  const total = selectedTripPassengers.value.length
+  const validated = selectedTripPassengers.value.filter(p => p.ticketStatus === 'validated').length
+  const pending = selectedTripPassengers.value.filter(p => p.ticketStatus === 'valid' || p.ticketStatus === 'pending').length
+  const cancelled = selectedTripPassengers.value.filter(p => p.ticketStatus === 'cancelled').length
+  
+  console.log('📊 Calcul des stats tickets:', { 
+    total, 
+    validated, 
+    pending, 
+    cancelled,
+    passengersData: selectedTripPassengers.value,
+    passengersCount: selectedTripPassengers.value.length,
+    statusBreakdown: selectedTripPassengers.value.map(p => ({ name: p.name, status: p.ticketStatus }))
+  })
+  
+  return { total, validated, pending, cancelled }
+})
+
 // Méthodes
 const loadDashboardData = async () => {
+  console.log('🚀 Début du chargement des données du dashboard')
+  console.log('👤 Utilisateur connecté:', userStore.user)
+  console.log('🏢 ID de l\'entreprise:', userStore.companyId)
+  
   isLoading.value = true
   try {
     // Chargement des données réelles via le service
+    console.log('📊 Chargement des trajets de l\'entreprise d\'abord...')
+    await loadCompanyTrips()
+    
+    console.log('📊 Chargement des trajets assignés et statistiques...')
     await Promise.all([
       loadTrips(),
-      loadCompanyTrips(),
       loadStatistics()
     ])
+    console.log('✅ Toutes les données chargées avec succès')
+    console.log('📋 Trajets du jour final:', todayTrips.value)
   } catch (error) {
-    console.error('Erreur lors du chargement des données:', error)
+    console.error('❌ Erreur lors du chargement des données:', error)
   } finally {
     isLoading.value = false
   }
@@ -329,27 +397,63 @@ const loadCompanyTrips = async () => {
 
     const result = await TripService.getTripsByCompany(companyId, 1, 10)
     
-    // Transformer les données pour correspondre au format attendu
-    todayTrips.value = result.trips.map(trip => ({
-      id: trip.id,
-      departure: trip.departStation?.name || 'Station inconnue',
-      arrival: trip.arriveStation?.name || 'Station inconnue',
-      departureTime: new Date(trip.departAt).toLocaleTimeString('fr-FR', { 
-        hour: '2-digit', 
-        minute: '2-digit' 
-      }),
-      arrivalTime: new Date(trip.arriveAt).toLocaleTimeString('fr-FR', { 
-        hour: '2-digit', 
-        minute: '2-digit' 
-      }),
-      passengers: trip.vehicule?.capacity - trip.availableSeats || 0,
-      capacity: trip.vehicule?.capacity || 0,
-      status: 'scheduled',
-      pricePerSeat: trip.pricePerSeat,
-      availableSeats: trip.availableSeats
-    }))
-    
-    console.log(`${result.trips.length} trajets chargés pour l'entreprise ${companyId}`)
+    // Récupérer le nombre de passagers pour chaque trajet
+    const tripsWithPassengers = await Promise.all(
+      result.trips.map(async (trip) => {
+        try {
+          // Récupérer les passagers du trajet pour obtenir le nombre réel
+          const passengersResult = await TripService.getTripPassengers(trip.id, 1, 1)
+          const actualPassengers = passengersResult.pagination.total
+          
+          return {
+            id: trip.id,
+            departure: trip.departStation?.name || 'Station inconnue',
+            arrival: trip.arriveStation?.name || 'Station inconnue',
+            departureTime: new Date(trip.departAt).toLocaleTimeString('fr-FR', { 
+              hour: '2-digit', 
+              minute: '2-digit' 
+            }),
+            arrivalTime: new Date(trip.arriveAt).toLocaleTimeString('fr-FR', { 
+              hour: '2-digit', 
+              minute: '2-digit' 
+            }),
+            passengers: actualPassengers, // Nombre réel de réservations
+            capacity: trip.vehicule?.number_places || 0,
+            vehicule: trip.vehicule, // Ajouter l'objet véhicule complet
+            status: 'scheduled',
+            pricePerSeat: trip.pricePerSeat,
+            availableSeats: trip.availableSeats,
+            registrationClosed: trip.registrationClosed || false // Ajouter registrationClosed
+          }
+        } catch (error) {
+          console.error(`Erreur lors de la récupération des passagers pour le trajet ${trip.id}:`, error)
+          // Fallback avec le calcul précédent en cas d'erreur
+          return {
+            id: trip.id,
+            departure: trip.departStation?.name || 'Station inconnue',
+            arrival: trip.arriveStation?.name || 'Station inconnue',
+            departureTime: new Date(trip.departAt).toLocaleTimeString('fr-FR', { 
+              hour: '2-digit', 
+              minute: '2-digit' 
+            }),
+            arrivalTime: new Date(trip.arriveAt).toLocaleTimeString('fr-FR', { 
+              hour: '2-digit', 
+              minute: '2-digit' 
+            }),
+            passengers: Math.max(0, (trip.vehicule?.number_places || 0) - trip.availableSeats),
+            capacity: trip.vehicule?.number_places || 0,
+            vehicule: trip.vehicule, // Ajouter l'objet véhicule complet
+            status: 'scheduled',
+            pricePerSeat: trip.pricePerSeat,
+            availableSeats: trip.availableSeats,
+            registrationClosed: trip.registrationClosed || false // Ajouter registrationClosed
+          }
+        }
+      })
+    )
+     
+     companyTrips.value = tripsWithPassengers
+     console.log(`${result.trips.length} trajets chargés pour l'entreprise ${companyId}`)
   } catch (error) {
     console.error('Erreur lors du chargement des trajets de l\'entreprise:', error)
     // Fallback avec des données de démonstration en cas d'erreur
@@ -368,44 +472,136 @@ const loadStatistics = async () => {
 
 const loadTrips = async () => {
   try {
+    console.log('🚛 Début du chargement des trajets assignés')
     const userId = userStore.userId
+    console.log('👤 ID utilisateur:', userId)
+    
     if (!userId) {
-      console.error('ID utilisateur non disponible')
+      console.error('❌ ID utilisateur non disponible')
       return
     }
 
+    const today = new Date().toISOString().split('T')[0]
+    console.log('📅 Date du jour:', today)
+    
+    console.log('🔄 Appel API EmployeeService.getAssignedTrips...')
     const result = await EmployeeService.getAssignedTrips(
       userId,
       1, // page
       10, // limit
       undefined, // status filter
-      new Date().toISOString().split('T')[0] // today's date
+      today // today's date
     )
     
-    trips.value = result.trips
-  } catch (error) {
-    console.error('Erreur lors du chargement des trajets:', error)
-    // Fallback avec des données de démonstration en cas d'erreur
-    trips.value = [
-      {
-        id: 1,
-        departure: 'Abidjan',
-        arrival: 'Bouaké',
-        departureTime: '08:00',
-        arrivalTime: '11:30',
-        date: new Date().toISOString().split('T')[0],
-        passengers: 45,
-        capacity: 50,
-        status: 'in_progress',
-        vehicle: { id: 1, licensePlate: 'CI-001-AB', model: 'Mercedes Sprinter', capacity: 50 },
-        company: { id: 1, name: 'Transport Express' },
-        route: {
+    console.log('📦 Résultat de l\'API getAssignedTrips:', result)
+    console.log('📦 Nombre de trajets assignés trouvés:', result.trips?.length || 0)
+    console.log('📦 Détails des trajets assignés:', result.trips)
+    
+    // Récupérer le nombre de passagers pour chaque trajet assigné
+    const tripsWithPassengers = await Promise.all(
+      result.trips.map(async (trip) => {
+        try {
+          // Récupérer les passagers du trajet pour obtenir le nombre réel
+          const passengersResult = await TripService.getTripPassengers(trip.id, 1, 1)
+          const actualPassengers = passengersResult.pagination.total
+          
+          return {
+            ...trip,
+            passengers: actualPassengers, // Remplacer par le nombre réel de réservations
+            registrationClosed: trip.registrationClosed || false // S'assurer que registrationClosed est présent
+          }
+        } catch (error) {
+          console.error(`Erreur lors de la récupération des passagers pour le trajet ${trip.id}:`, error)
+          // Garder les données originales en cas d'erreur avec registrationClosed
+          return {
+            ...trip,
+            registrationClosed: trip.registrationClosed || false
+          }
+        }
+      })
+    )
+    
+    // Utiliser todayTrips au lieu de trips non déclarée
+    todayTrips.value = tripsWithPassengers
+    // Mettre à jour aussi allTrips pour la cohérence
+    allTrips.value = [...tripsWithPassengers]
+    
+    console.log('✅ Trajets assignés chargés:', tripsWithPassengers.length)
+    console.log('📋 todayTrips.value:', todayTrips.value)
+    
+    // Si aucun trajet assigné, utiliser les trajets de l'entreprise comme fallback
+    if (tripsWithPassengers.length === 0 && companyTrips.value.length > 0) {
+      console.log('🔄 Aucun trajet assigné trouvé, utilisation des trajets de l\'entreprise comme fallback')
+      todayTrips.value = companyTrips.value
+      allTrips.value = [...companyTrips.value]
+    }
+    
+    // Ajouter temporairement des données de test avec registrationClosed pour démonstration
+    if (todayTrips.value.length === 0) {
+      console.log('🧪 Ajout de données de test avec registrationClosed')
+      todayTrips.value = [
+        {
           id: 1,
-          departureStation: { id: 1, name: 'Gare d\'Abidjan', city: 'Abidjan' },
-          arrivalStation: { id: 2, name: 'Gare de Bouaké', city: 'Bouaké' }
+          departure: 'Abidjan',
+          arrival: 'Yamoussoukro',
+          departureTime: '08:00',
+          arrivalTime: '10:30',
+          passengers: 25,
+          capacity: 30,
+          registrationClosed: false,
+          vehicule: { type: 'Bus', immatriculation: 'AB-123-CI', number_places: 30 },
+          status: 'scheduled',
+          vehicle: { id: 1, licensePlate: 'AB-123-CI', model: 'Mercedes', capacity: 30 },
+          company: { id: 1, name: 'Transport Express' },
+          route: {
+            id: 1,
+            departureStation: { id: 1, name: 'Abidjan', city: 'Abidjan' },
+            arrivalStation: { id: 2, name: 'Yamoussoukro', city: 'Yamoussoukro' }
+          }
+        },
+        {
+          id: 2,
+          departure: 'Bouaké',
+          arrival: 'Korhogo',
+          departureTime: '14:00',
+          arrivalTime: '17:00',
+          passengers: 18,
+          capacity: 25,
+          registrationClosed: true, // Trajet avec enregistrement fermé
+          vehicule: { type: 'Minibus', immatriculation: 'BK-456-CI', number_places: 25 },
+          status: 'scheduled',
+          vehicle: { id: 2, licensePlate: 'BK-456-CI', model: 'Toyota', capacity: 25 },
+          company: { id: 1, name: 'Transport Express' },
+          route: {
+            id: 2,
+            departureStation: { id: 3, name: 'Bouaké', city: 'Bouaké' },
+            arrivalStation: { id: 4, name: 'Korhogo', city: 'Korhogo' }
+          }
+        },
+        {
+          id: 3,
+          departure: 'San Pedro',
+          arrival: 'Abidjan',
+          departureTime: '16:30',
+          arrivalTime: '19:45',
+          passengers: 12,
+          capacity: 20,
+          registrationClosed: true, // Autre trajet avec enregistrement fermé
+          vehicule: { type: 'Bus', immatriculation: 'SP-789-CI', number_places: 20 },
+          status: 'scheduled',
+          vehicle: { id: 3, licensePlate: 'SP-789-CI', model: 'Iveco', capacity: 20 },
+          company: { id: 1, name: 'Transport Express' },
+          route: {
+            id: 3,
+            departureStation: { id: 5, name: 'San Pedro', city: 'San Pedro' },
+            arrivalStation: { id: 1, name: 'Abidjan', city: 'Abidjan' }
+          }
         }
-      }
-    ]
+      ]
+      allTrips.value = [...todayTrips.value]
+    }
+  } catch (error) {
+    console.error('❌ Erreur lors du chargement des trajets:', error)
   }
 }
 
@@ -416,36 +612,59 @@ const logout = () => {
   router.push('/')
 }
 
-const selectTrip = (trip: any) => {
+const selectTrip = async (trip: AssignedTrip) => {
   selectedTrip.value = trip
   activeTab.value = 'trip-details'
+  
+  // Charger les passengers détaillés pour ce trip
+  await loadTripPassengers(trip.id)
 }
 
-const openScanner = (trip?: any) => {
+const loadTripPassengers = async (tripId: number) => {
+  try {
+    const response = await EmployeeService.getTripPassengers(tripId)
+    selectedTripPassengers.value = response.data || []
+    console.log('✅ Passengers chargés pour le trip:', tripId, selectedTripPassengers.value.length)
+    console.log('📋 Détails des passengers:', selectedTripPassengers.value)
+    console.log('🔍 Structure de la réponse:', response)
+  } catch (error) {
+    console.error('❌ Erreur lors du chargement des passengers:', error)
+    selectedTripPassengers.value = []
+  }
+}
+
+const openScanner = async (trip?: AssignedTrip) => {
+  console.log('🚀 openScanner appelé avec trip:', trip)
   if (trip) {
     selectedTrip.value = trip
+    console.log('📍 Trip sélectionné:', selectedTrip.value)
+    // Charger les passengers pour ce trip
+    await loadTripPassengers(trip.id)
+  } else {
+    console.log('⚠️ Aucun trip fourni à openScanner')
   }
   activeTab.value = 'scanner'
+  console.log('📱 Tab actif changé vers scanner')
 }
 
-const onTicketValidated = (ticketData: any) => {
+const onTicketValidated = async (ticketData: any) => {
   stats.value.scannedTickets++
   showValidationModal.value = true
   
-  // Mettre à jour les statistiques du trajet
+  // Recharger les passengers pour mettre à jour ticketStats
   if (selectedTrip.value) {
-    // Logique de mise à jour des passagers validés
+    await loadTripPassengers(selectedTrip.value.id)
   }
 }
 
 const formatTime = (time: string) => {
   return time
 }
-
 // Lifecycle
 onMounted(() => {
   // Charger les données depuis l'API
   loadDashboardData()
+  console.log('🚀 EmployeeDashboard mounted', todayTrips.value)
 })
 
 const loadEmployeeData = async () => {

+ 2 - 0
node_app/prisma/migrations/20250920103348_add_registration_closed_to_trajet/migration.sql

@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE `Trajet` ADD COLUMN `registrationClosed` BOOLEAN NOT NULL DEFAULT false;

+ 12 - 11
node_app/prisma/schema.prisma

@@ -117,17 +117,18 @@ model Place {
 }
 
 model Trajet {
-  id              Int           @id @default(autoincrement())
-  departAt        DateTime
-  arriveAt        DateTime
-  // depart       String // Ancien champ
-  // arrive       String // Ancien champ
-  departStationId Int           // Nouveau champ pour l'ID de la station de départ
-  arriveStationId Int           // Nouveau champ pour l'ID de la station d'arrivée
-  vehiculeId      Int
-  companyId       Int?          // Relation directe avec Company (optionnel temporairement)
-  employeeId      Int?          // ID de l'employé responsable du trajet
-  pricePerSeat    Float         @default(0.0) // Prix par place
+  id                 Int           @id @default(autoincrement())
+  departAt           DateTime
+  arriveAt           DateTime
+  // depart          String // Ancien champ
+  // arrive          String // Ancien champ
+  departStationId    Int           // Nouveau champ pour l'ID de la station de départ
+  arriveStationId    Int           // Nouveau champ pour l'ID de la station d'arrivée
+  vehiculeId         Int
+  companyId          Int?          // Relation directe avec Company (optionnel temporairement)
+  employeeId         Int?          // ID de l'employé responsable du trajet
+  pricePerSeat       Float         @default(0.0) // Prix par place
+  registrationClosed Boolean       @default(false) // Indique si l'enregistrement est fermé
   reservations    Reservation[] @relation("TrajetReservations")
   vehicule        Vehicule      @relation("VehiculeTrajets", fields: [vehiculeId], references: [id])
   company         Company?      @relation("CompanyTrajets", fields: [companyId], references: [id]) // Relation directe avec Company

+ 8 - 4
node_app/src/app.module.ts

@@ -33,6 +33,8 @@ import { FindTrajetByIdUseCase } from './application/use-cases/trajet/find-traje
 import { FindTrajetsByCompanyUseCase } from './application/use-cases/trajet/find-trajets-by-company.use-case';
 import { DeleteTrajetUseCase } from './application/use-cases/trajet/delete-trajet.use-case'; // Ajoutez cette ligne
 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 { TrajetMapper } from './infrastructure/mappers/trajet.mapper';
 import { TrajetRepository } from './infrastructure/database/repositories/trajet.repository';
 import { DebugMiddleware } from './middleware/debug.middleware';
@@ -99,10 +101,10 @@ import { CqrsModule } from '@nestjs/cqrs';
     PlaceController,
     VehiculeController,
     TrajetController,
-    AuthController,           // Nouveau
-    StationController,           // Nouveau
-    TransactionController,       // Nouveau
-    StationCompanyController,   // Nouveau
+    AuthController,
+    StationController,
+    TransactionController,
+    StationCompanyController,
   ],
   providers: [
     // Configuration du AuthGuard global
@@ -174,6 +176,8 @@ import { CqrsModule } from '@nestjs/cqrs';
     FindTrajetsByCompanyUseCase,
     DeleteTrajetUseCase,
     SearchTrajetsUseCase,
+    FindTripPassengersUseCase,
+    CloseRegistrationUseCase,
     TrajetMapper,
     {
       provide: 'TrajetRepository',

+ 22 - 0
node_app/src/application/dto/qrcode.dto.ts

@@ -0,0 +1,22 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
+
+export class GenerateQrcodeDto {
+  @ApiProperty({
+    description: 'ID de la réservation pour laquelle générer le QR code',
+    example: 1,
+  })
+  @IsNotEmpty()
+  @IsNumber()
+  reservationId: number;
+}
+
+export class ScanQrcodeDto {
+  @ApiProperty({
+    description: 'Valeur du QR code à scanner',
+    example: 'QR_abc123-def456-789_123',
+  })
+  @IsNotEmpty()
+  @IsString()
+  value: string;
+}

+ 3 - 0
node_app/src/application/dto/responses/trajet-response.dto.ts

@@ -49,4 +49,7 @@ export class TrajetResponseDto {
 
   @ApiProperty({ description: "Nombre de places disponibles dans le véhicule pour ce trajet" })
   availableSeats: number;
+
+  @ApiProperty({ description: "Indique si l'enregistrement pour ce trajet est fermé" })
+  registrationClosed: boolean;
 }

+ 36 - 0
node_app/src/application/dto/responses/trip-passenger-response.dto.ts

@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class TripPassengerResponseDto {
+  @ApiProperty({ description: 'ID de la réservation' })
+  id: number;
+
+  @ApiProperty({ description: 'Prénom du passager' })
+  firstName: string;
+
+  @ApiProperty({ description: 'Nom du passager' })
+  lastName: string;
+
+  @ApiProperty({ description: 'Email du passager' })
+  email: string;
+
+  @ApiProperty({ description: 'Téléphone du passager' })
+  phone: string;
+
+  @ApiProperty({ description: 'Numéro de place' })
+  seatNumber: string;
+
+  @ApiProperty({ description: 'Numéro de ticket' })
+  ticketNumber: string;
+
+  @ApiProperty({ description: 'Statut de la réservation', enum: ['pending', 'validated', 'cancelled'] })
+  status: string;
+
+  @ApiProperty({ description: 'Date de validation du ticket', required: false })
+  validatedAt?: Date;
+
+  @ApiProperty({ description: 'Code QR du ticket', required: false })
+  qrCode?: string;
+
+  @ApiProperty({ description: 'Date de réservation' })
+  reservationDate: Date;
+}

+ 17 - 1
node_app/src/application/use-cases/reservation/create-reservation.use-case.ts

@@ -1,20 +1,36 @@
-import { Injectable, Inject } from '@nestjs/common';
+import { Injectable, Inject, BadRequestException } from '@nestjs/common';
 import { EventBus } from '@nestjs/cqrs';
 import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+import { TrajetRepository } from '../../../infrastructure/database/repositories/trajet.repository';
 import { CreateReservationDto } from '../../dto/create-reservation.dto';
 import { ReservationMapper } from 'src/infrastructure/mappers/reservation.mapper';
 import { ReservationCreatedEvent } from '../../../domain/events/reservation-created.event';
+import { TrajetNotFoundException } from '../../exceptions/trajet-not-found.exception';
 
 @Injectable()
 export class CreateReservationUseCase {
   constructor(
     @Inject('ReservationRepository')
     private readonly reservationRepository: ReservationRepository,
+    @Inject('TrajetRepository')
+    private readonly trajetRepository: TrajetRepository,
     private readonly reservationMapper: ReservationMapper,
     private readonly eventBus: EventBus,
   ) {}
 
   async execute(createReservationDto: CreateReservationDto) {
+    // Vérifier si le trajet existe et si l'enregistrement est ouvert
+    const trajet = await this.trajetRepository.findById(createReservationDto.trajetId);
+    if (!trajet) {
+      throw new TrajetNotFoundException(createReservationDto.trajetId);
+    }
+
+    if (trajet.registrationClosed) {
+      throw new BadRequestException(
+        `L'enregistrement pour le trajet ${createReservationDto.trajetId} est fermé. Aucune nouvelle réservation n'est autorisée.`
+      );
+    }
+
     const createReservation =
       this.reservationMapper.fromDtoToDomain(createReservationDto);
     const reservation =

+ 30 - 0
node_app/src/application/use-cases/trajet/close-registration.use-case.ts

@@ -0,0 +1,30 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { TrajetRepository } from '../../../infrastructure/database/repositories/trajet.repository';
+import { TrajetNotFoundException } from '../../exceptions/trajet-not-found.exception';
+
+@Injectable()
+export class CloseRegistrationUseCase {
+  constructor(
+    @Inject('TrajetRepository')
+    private readonly trajetRepository: TrajetRepository,
+  ) {}
+
+  async execute(id: number) {
+    const trajet = await this.trajetRepository.findById(id);
+    if (!trajet) {
+      throw new TrajetNotFoundException(id);
+    }
+
+    // Mettre à jour le champ registrationClosed à true
+    const updatedTrajet = await this.trajetRepository.update(id, {
+      ...trajet,
+      registrationClosed: true,
+    });
+
+    return {
+      success: true,
+      message: `L'enregistrement pour le trajet ${id} a été fermé avec succès`,
+      trajet: updatedTrajet,
+    };
+  }
+}

+ 54 - 0
node_app/src/application/use-cases/trajet/find-trip-passengers.use-case.ts

@@ -0,0 +1,54 @@
+import { Injectable, Inject, NotFoundException } from '@nestjs/common';
+import { TrajetRepository } from '../../../infrastructure/database/repositories/trajet.repository';
+import { TripPassengerResponseDto } from '../../dto/responses/trip-passenger-response.dto';
+import { PaginatedResponseDto } from '../../dto/responses/paginated-response.dto';
+
+@Injectable()
+export class FindTripPassengersUseCase {
+  constructor(
+    @Inject('TrajetRepository')
+    private readonly trajetRepository: TrajetRepository,
+  ) {}
+
+  async execute(
+    tripId: number,
+    page: number = 1,
+    limit: number = 50,
+    search?: string,
+    status?: string,
+  ): Promise<PaginatedResponseDto<TripPassengerResponseDto>> {
+    // Vérifier que le trajet existe
+    const trip = await this.trajetRepository.findById(tripId);
+    if (!trip) {
+      throw new NotFoundException(`Trajet avec l'ID ${tripId} non trouvé`);
+    }
+
+    // Récupérer les passagers avec pagination
+    const { data, total } = await this.trajetRepository.findPassengersByTripId(
+      tripId,
+      page,
+      limit,
+      search,
+      status,
+    );
+
+    // Mapper les données vers le DTO de réponse
+    const passengerDtos: TripPassengerResponseDto[] = data.map(
+      (reservation: any) => ({
+        id: reservation.id,
+        firstName: reservation.user?.name || '',
+        lastName: reservation.user?.otherName || '',
+        email: reservation.user?.email || '',
+        phone: reservation.user?.telephone || '',
+        seatNumber: `${reservation.place?.number || 'N/A'}`,
+        ticketNumber: reservation.qrcode?.value || `TICKET-${reservation.id}`,
+        status: reservation.qrcode?.scannedAt ? 'validated' : 'pending',
+        validatedAt: reservation.qrcode?.scannedAt || undefined,
+        qrCode: reservation.qrcode?.value || undefined,
+        reservationDate: reservation.dateAt,
+      }),
+    );
+
+    return new PaginatedResponseDto(passengerDtos, total, page, limit);
+  }
+}

+ 1 - 0
node_app/src/domain/entities/trajet.entity.ts

@@ -13,6 +13,7 @@ export class Trajet {
   companyId?: number; // Relation directe avec Company (optionnel)
   employeeId?: number; // ID de l'employé responsable du trajet
   pricePerSeat: number; // Prix par place
+  registrationClosed: boolean; // Indique si l'enregistrement est fermé
   // Relations
   vehicule?: Vehicule; // L'objet Vehicule peut être chargé optionnellement
   employee?: User; // Employé responsable du trajet

+ 97 - 0
node_app/src/infrastructure/database/repositories/trajet.repository.ts

@@ -80,6 +80,7 @@ export class TrajetRepository implements IBaseRepository<Trajet> {
         companyId: entity.companyId,
         vehiculeId: entity.vehiculeId,
         pricePerSeat: entity.pricePerSeat,
+        registrationClosed: entity.registrationClosed,
       },
       include: {
         vehicule: {
@@ -365,4 +366,100 @@ export class TrajetRepository implements IBaseRepository<Trajet> {
   delete(id: number): Promise<Trajet | null> {
     throw new Error('Method not implemented.' + id);
   }
+
+  /**
+   * Récupérer les passagers d'un trajet avec pagination et filtres
+   */
+  async findPassengersByTripId(
+    tripId: number,
+    page: number = 1,
+    limit: number = 50,
+    search?: string,
+    status?: string
+  ): Promise<{
+    data: any[];
+    total: number;
+  }> {
+    const skip = (page - 1) * limit;
+
+    // Construire les conditions de recherche
+    const whereConditions: any = {
+      trajetId: tripId,
+    };
+
+    // Filtrer par statut si spécifié
+    if (status && status !== 'all') {
+      // Le statut peut être basé sur la présence d'un QR code scanné
+      if (status === 'validated') {
+        whereConditions.qrcode = {
+          scannedAt: {
+            not: null,
+          },
+        };
+      } else if (status === 'pending') {
+        whereConditions.qrcode = {
+          OR: [
+            { scannedAt: null },
+            { scannedAt: undefined },
+          ],
+        };
+      }
+    }
+
+    // Filtrer par recherche si spécifiée
+    if (search) {
+      whereConditions.user = {
+        OR: [
+          {
+            name: {
+              contains: search,
+              mode: 'insensitive',
+            },
+          },
+          {
+            otherName: {
+              contains: search,
+              mode: 'insensitive',
+            },
+          },
+          {
+            email: {
+              contains: search,
+              mode: 'insensitive',
+            },
+          },
+          {
+            telephone: {
+              contains: search,
+              mode: 'insensitive',
+            },
+          },
+        ],
+      };
+    }
+
+    const [data, total] = await Promise.all([
+      this.prisma.reservation.findMany({
+        where: whereConditions,
+        skip,
+        take: limit,
+        include: {
+          user: true,
+          place: true,
+          qrcode: true,
+        },
+        orderBy: {
+          dateAt: 'desc',
+        },
+      }),
+      this.prisma.reservation.count({
+        where: whereConditions,
+      }),
+    ]);
+
+    return {
+      data,
+      total,
+    };
+  }
 }

+ 3 - 0
node_app/src/infrastructure/mappers/trajet.mapper.ts

@@ -79,6 +79,8 @@ export class TrajetMapper
       companyId: prismaTrajet.companyId ?? undefined,
       employeeId: prismaTrajet.employeeId ?? undefined,
       pricePerSeat: prismaTrajet.pricePerSeat,
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+      registrationClosed: prismaTrajet.registrationClosed || false,
       departStation: departStationDomain,
       arriveStation: arriveStationDomain,
       vehicule: vehiculeDomain,
@@ -118,6 +120,7 @@ export class TrajetMapper
     response.companyId = trajet.companyId;
     response.employeeId = trajet.employeeId;
     response.pricePerSeat = trajet.pricePerSeat;
+    response.registrationClosed = trajet.registrationClosed;
     response.availableSeats = trajet.availableSeats || 0;
 
     // Si TrajetResponseDto doit inclure des détails sur les stations ou le véhicule

+ 1 - 8
node_app/src/presentation/controllers/qrcode.controller.ts

@@ -12,14 +12,7 @@ import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/s
 import { GenerateQrcodeUseCase } from '../../application/use-cases/qrcode/generate-qrcode.use-case';
 import { FindQrcodeUseCase } from '../../application/use-cases/qrcode/find-qrcode.use-case';
 import { ScanQrcodeUseCase } from '../../application/use-cases/qrcode/scan-qrcode.use-case';
-
-export class GenerateQrcodeDto {
-  reservationId: number;
-}
-
-export class ScanQrcodeDto {
-  value: string;
-}
+import { GenerateQrcodeDto, ScanQrcodeDto } from '../../application/dto/qrcode.dto';
 
 @ApiTags('QR Codes')
 @Controller('qrcode')

+ 65 - 2
node_app/src/presentation/controllers/trajet.controller.ts

@@ -28,8 +28,11 @@ import { FindTrajetByIdUseCase } from '../../application/use-cases/trajet/find-t
 import { FindTrajetsByCompanyUseCase } from '../../application/use-cases/trajet/find-trajets-by-company.use-case';
 import { DeleteTrajetUseCase } from '../../application/use-cases/trajet/delete-trajet.use-case'; // Assurez-vous que ce cas d'utilisation existe
 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 { 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 { Roles } from '../decorators/roles.decorator';
 import { UserRole } from 'src/domain/entities/user.entity';
 import { TrajetNotFoundException } from '../../application/exceptions/trajet-not-found.exception';
@@ -47,6 +50,8 @@ export class TrajetController {
     private readonly deleteTrajetUseCase: DeleteTrajetUseCase, // Assurez-vous que ce cas d'utilisation est injecté
     private readonly findTrajetsByCompanyUseCase: FindTrajetsByCompanyUseCase, // Nouveau cas d'utilisation pour les trajets par compagnie
     private readonly searchTrajetsUseCase: SearchTrajetsUseCase,
+    private readonly findTripPassengersUseCase: FindTripPassengersUseCase,
+    private readonly closeRegistrationUseCase: CloseRegistrationUseCase,
   ) {}
 
   @Post()
@@ -100,8 +105,12 @@ export class TrajetController {
   }
 
   @Get('company/:companyId')
-  @ApiOperation({ summary: 'Récupérer les trajets d\'une compagnie' })
-  @ApiParam({ name: 'companyId', description: 'ID de la compagnie', type: 'number' })
+  @ApiOperation({ summary: "Récupérer les trajets d'une compagnie" })
+  @ApiParam({
+    name: 'companyId',
+    description: 'ID de la compagnie',
+    type: 'number',
+  })
   @ApiResponse({
     status: 200,
     description: 'Liste paginée des trajets de la compagnie',
@@ -194,6 +203,60 @@ export class TrajetController {
     }
   }
 
+  @Get(':id/passengers')
+  @ApiOperation({ summary: "Récupérer les passagers d'un trajet" })
+  @ApiParam({ name: 'id', description: 'ID du trajet', type: 'number' })
+  @ApiResponse({
+    status: 200,
+    description: 'Liste paginée des passagers du trajet',
+    type: PaginatedResponseDto<TripPassengerResponseDto>,
+  })
+  @ApiResponse({ status: 404, description: 'Trajet non trouvé' })
+  @Roles(UserRole.ADMIN, UserRole.MANAGER, UserRole.EMPLOYEE)
+  async getTripPassengers(
+    @Param('id', ParseIntPipe) id: number,
+    @Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
+    @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 50,
+    @Query('search') search?: string,
+    @Query('status') status?: string,
+  ) {
+    try {
+      return await this.findTripPassengersUseCase.execute(
+        id,
+        page,
+        limit,
+        search,
+        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' })
+  @ApiResponse({
+    status: 200,
+    description: "L'enregistrement du trajet a été fermé avec succès",
+  })
+  @ApiResponse({ status: 404, description: 'Trajet non trouvé' })
+  @Roles(UserRole.ADMIN, UserRole.MANAGER, UserRole.EMPLOYEE, UserRole.DRIVER)
+  async closeRegistration(@Param('id', ParseIntPipe) id: number) {
+    try {
+      const result = await this.closeRegistrationUseCase.execute(id);
+      return result;
+    } catch (error) {
+      if (error instanceof TrajetNotFoundException) {
+        throw error;
+      }
+      throw error;
+    }
+  }
+
   @Delete(':id')
   @ApiOperation({ summary: 'Supprimer un trajet' })
   @ApiParam({ name: 'id', description: 'ID du trajet', type: 'number' })

+ 6 - 0
node_app/src/presentation/modules/reservation.module.ts

@@ -7,6 +7,7 @@ import { UpdateReservationUseCase } from '../../application/use-cases/reservatio
 import { DeleteReservationUseCase } from '../../application/use-cases/reservation/delete-reservation.use-case';
 import { FindReservationsByUserUseCase } from '../../application/use-cases/reservation/find-reservations-by-user.use-case';
 import { ReservationRepository } from '../../infrastructure/database/repositories/reservation.repository';
+import { TrajetRepository } from '../../infrastructure/database/repositories/trajet.repository';
 import { ReservationMapper } from '../../infrastructure/mappers/reservation.mapper';
 import { PrismaModule } from '../../infrastructure/database/prisma.module';
 import { ServicesModule } from '../../infrastructure/services/services.module';
@@ -32,6 +33,10 @@ import { CqrsModule } from '@nestjs/cqrs';
       provide: 'ReservationRepository',
       useClass: ReservationRepository,
     },
+    {
+      provide: 'TrajetRepository',
+      useClass: TrajetRepository,
+    },
     ReservationMapper,
     UserMapper,
     PlaceMapper,
@@ -47,6 +52,7 @@ import { CqrsModule } from '@nestjs/cqrs';
     DeleteReservationUseCase,
     FindReservationsByUserUseCase,
     'ReservationRepository',
+    'TrajetRepository',
     ReservationMapper,
     UserMapper,
     PlaceMapper,

+ 91 - 0
node_app/test-close-registration.js

@@ -0,0 +1,91 @@
+const axios = require('axios');
+
+const BASE_URL = 'http://localhost:3000';
+
+// Configuration des headers avec un token d'authentification (à adapter selon votre système)
+const headers = {
+  'Content-Type': 'application/json',
+  // 'Authorization': 'Bearer YOUR_TOKEN_HERE' // Décommentez et ajoutez votre token si nécessaire
+};
+
+async function testCloseRegistration() {
+  try {
+    console.log('🚀 Test de la fonctionnalité de fermeture d\'enregistrement\n');
+
+    // 1. Récupérer la liste des trajets
+    console.log('1. Récupération de la liste des trajets...');
+    const trajetsResponse = await axios.get(`${BASE_URL}/trajets`, { headers });
+    const trajets = trajetsResponse.data;
+    
+    if (trajets.length === 0) {
+      console.log('❌ Aucun trajet trouvé. Créez d\'abord un trajet pour tester.');
+      return;
+    }
+
+    const trajet = trajets[0];
+    console.log(`✅ Trajet trouvé: ID ${trajet.id}, Enregistrement fermé: ${trajet.registrationClosed}`);
+
+    // 2. Tenter de créer une réservation avant fermeture
+    console.log('\n2. Test de création de réservation avant fermeture...');
+    try {
+      const reservationData = {
+        trajetId: trajet.id,
+        userId: 1, // À adapter selon vos données
+        placeId: 1 // À adapter selon vos données
+      };
+
+      const reservationResponse = await axios.post(`${BASE_URL}/reservations`, reservationData, { headers });
+      console.log('✅ Réservation créée avec succès avant fermeture');
+    } catch (error) {
+      console.log(`⚠️ Erreur lors de la création de réservation: ${error.response?.data?.message || error.message}`);
+    }
+
+    // 3. Fermer l'enregistrement du trajet
+    console.log('\n3. Fermeture de l\'enregistrement du trajet...');
+    try {
+      const closeResponse = await axios.patch(`${BASE_URL}/trajets/${trajet.id}/close-registration`, {}, { headers });
+      console.log('✅ Enregistrement fermé avec succès');
+      console.log('Réponse:', closeResponse.data);
+    } catch (error) {
+      console.log(`❌ Erreur lors de la fermeture: ${error.response?.data?.message || error.message}`);
+      return;
+    }
+
+    // 4. Vérifier que le trajet est marqué comme fermé
+    console.log('\n4. Vérification du statut du trajet...');
+    const updatedTrajetResponse = await axios.get(`${BASE_URL}/trajets/${trajet.id}`, { headers });
+    const updatedTrajet = updatedTrajetResponse.data;
+    console.log(`✅ Statut mis à jour: Enregistrement fermé: ${updatedTrajet.registrationClosed}`);
+
+    // 5. Tenter de créer une réservation après fermeture
+    console.log('\n5. Test de création de réservation après fermeture...');
+    try {
+      const reservationData = {
+        trajetId: trajet.id,
+        userId: 2, // Différent utilisateur
+        placeId: 2 // Différente place
+      };
+
+      const reservationResponse = await axios.post(`${BASE_URL}/reservations`, reservationData, { headers });
+      console.log('❌ ERREUR: La réservation ne devrait pas être possible après fermeture!');
+    } catch (error) {
+      if (error.response?.status === 400 && error.response?.data?.message?.includes('fermé')) {
+        console.log('✅ Réservation correctement bloquée après fermeture');
+        console.log(`Message d'erreur: ${error.response.data.message}`);
+      } else {
+        console.log(`⚠️ Erreur inattendue: ${error.response?.data?.message || error.message}`);
+      }
+    }
+
+    console.log('\n🎉 Test terminé avec succès!');
+
+  } catch (error) {
+    console.error('❌ Erreur générale:', error.message);
+    if (error.response) {
+      console.error('Détails de l\'erreur:', error.response.data);
+    }
+  }
+}
+
+// Exécuter le test
+testCloseRegistration();