Quellcode durchsuchen

(feat) espace employée

Brin AMONKONAN vor 1 Jahr
Ursprung
Commit
6712f1d666

+ 28 - 0
app_transport_front/package-lock.json

@@ -23,6 +23,7 @@
         "vue-router": "^4.5.1"
       },
       "devDependencies": {
+        "@types/qrcode": "^1.5.5",
         "@vitejs/plugin-vue": "^5.0.3",
         "autoprefixer": "^10.4.21",
         "leaflet": "^1.9.4",
@@ -1053,12 +1054,32 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/node": {
+      "version": "24.5.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
+      "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.12.0"
+      }
+    },
     "node_modules/@types/pako": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz",
       "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==",
       "license": "MIT"
     },
+    "node_modules/@types/qrcode": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+      "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/trusted-types": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -3807,6 +3828,13 @@
         "@types/estree": "^1.0.0"
       }
     },
+    "node_modules/undici-types": {
+      "version": "7.12.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
+      "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/unimport": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",

+ 1 - 0
app_transport_front/package.json

@@ -23,6 +23,7 @@
     "vue-router": "^4.5.1"
   },
   "devDependencies": {
+    "@types/qrcode": "^1.5.5",
     "@vitejs/plugin-vue": "^5.0.3",
     "autoprefixer": "^10.4.21",
     "leaflet": "^1.9.4",

+ 592 - 0
app_transport_front/src/components/employee/QRScanner.vue

@@ -0,0 +1,592 @@
+<template>
+  <div class="space-y-6">
+    <!-- En-tête -->
+    <div class="bg-white shadow rounded-lg">
+      <div class="px-4 py-5 sm:p-6">
+        <div class="flex items-center justify-between mb-4">
+          <button
+            @click="$emit('back')"
+            class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+          >
+            <i class="fas fa-arrow-left mr-2"></i>
+            Retour
+          </button>
+          
+          <h1 class="text-xl font-bold text-gray-900">
+            <i class="fas fa-qrcode mr-2 text-green-600"></i>
+            Scanner QR Code
+          </h1>
+          
+          <button
+            @click="toggleCamera"
+            :class="[
+              isCameraActive 
+                ? 'bg-red-600 hover:bg-red-700 text-white' 
+                : 'bg-green-600 hover:bg-green-700 text-white',
+              'inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2'
+            ]"
+          >
+            <i :class="isCameraActive ? 'fas fa-stop' : 'fas fa-play'" class="mr-2"></i>
+            {{ isCameraActive ? 'Arrêter' : 'Démarrer' }}
+          </button>
+        </div>
+
+        <!-- Informations du trajet -->
+        <div v-if="trip" class="bg-blue-50 rounded-lg p-4">
+          <div class="flex items-center justify-between">
+            <div>
+              <h3 class="text-lg font-medium text-blue-900">
+                {{ trip.departure }} → {{ trip.arrival }}
+              </h3>
+              <p class="text-sm text-blue-700">
+                {{ formatDate(trip.date) }} • {{ trip.departureTime }} - {{ trip.arrivalTime }}
+              </p>
+            </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>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Scanner -->
+    <div class="bg-white shadow rounded-lg">
+      <div class="px-4 py-5 sm:p-6">
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+          <!-- Zone de scan -->
+          <div class="space-y-4">
+            <h3 class="text-lg font-medium text-gray-900">
+              <i class="fas fa-camera mr-2 text-blue-600"></i>
+              Scanner avec la caméra
+            </h3>
+            
+            <!-- Caméra -->
+            <div class="relative bg-gray-900 rounded-lg overflow-hidden" style="aspect-ratio: 1;">
+              <video
+                ref="videoElement"
+                v-show="isCameraActive && !cameraError"
+                class="w-full h-full object-cover"
+                autoplay
+                muted
+                playsinline
+              ></video>
+              
+              <!-- Overlay de scan -->
+              <div v-if="isCameraActive && !cameraError" class="absolute inset-0 flex items-center justify-center">
+                <div class="w-64 h-64 border-2 border-green-500 rounded-lg relative">
+                  <div class="absolute top-0 left-0 w-8 h-8 border-t-4 border-l-4 border-green-500"></div>
+                  <div class="absolute top-0 right-0 w-8 h-8 border-t-4 border-r-4 border-green-500"></div>
+                  <div class="absolute bottom-0 left-0 w-8 h-8 border-b-4 border-l-4 border-green-500"></div>
+                  <div class="absolute bottom-0 right-0 w-8 h-8 border-b-4 border-r-4 border-green-500"></div>
+                  
+                  <!-- Ligne de scan animée -->
+                  <div class="absolute inset-x-0 top-0 h-0.5 bg-green-500 animate-scan"></div>
+                </div>
+              </div>
+              
+              <!-- Message d'erreur caméra -->
+              <div v-if="cameraError" class="absolute inset-0 flex items-center justify-center bg-gray-800">
+                <div class="text-center text-white">
+                  <i class="fas fa-exclamation-triangle text-4xl mb-4 text-yellow-500"></i>
+                  <p class="text-lg font-medium mb-2">Erreur d'accès à la caméra</p>
+                  <p class="text-sm text-gray-300">{{ cameraError }}</p>
+                  <button
+                    @click="initCamera"
+                    class="mt-4 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"
+                  >
+                    <i class="fas fa-redo mr-2"></i>
+                    Réessayer
+                  </button>
+                </div>
+              </div>
+              
+              <!-- Message caméra inactive -->
+              <div v-if="!isCameraActive && !cameraError" class="absolute inset-0 flex items-center justify-center bg-gray-800">
+                <div class="text-center text-white">
+                  <i class="fas fa-camera text-4xl mb-4 text-gray-400"></i>
+                  <p class="text-lg font-medium mb-2">Caméra inactive</p>
+                  <p class="text-sm text-gray-300">Cliquez sur "Démarrer" pour activer la caméra</p>
+                </div>
+              </div>
+            </div>
+
+            <!-- Saisie manuelle -->
+            <div class="border-t pt-4">
+              <h4 class="text-md font-medium text-gray-900 mb-3">
+                <i class="fas fa-keyboard mr-2 text-purple-600"></i>
+                Saisie manuelle
+              </h4>
+              <div class="flex space-x-2">
+                <input
+                  v-model="manualCode"
+                  type="text"
+                  placeholder="Entrez le code QR manuellement"
+                  class="flex-1 block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+                  @keyup.enter="validateManualCode"
+                />
+                <button
+                  @click="validateManualCode"
+                  :disabled="!manualCode.trim()"
+                  class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
+                >
+                  <i class="fas fa-check mr-2"></i>
+                  Valider
+                </button>
+              </div>
+            </div>
+          </div>
+
+          <!-- Historique des scans -->
+          <div class="space-y-4">
+            <h3 class="text-lg font-medium text-gray-900">
+              <i class="fas fa-history mr-2 text-green-600"></i>
+              Historique des validations
+            </h3>
+            
+            <div class="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
+              <div v-if="scanHistory.length === 0" class="text-center py-8">
+                <i class="fas fa-clipboard-list text-gray-400 text-3xl mb-2"></i>
+                <p class="text-gray-500">Aucune validation pour le moment</p>
+              </div>
+              
+              <div v-else class="space-y-3">
+                <div
+                  v-for="scan in scanHistory"
+                  :key="scan.id"
+                  :class="[
+                    'p-3 rounded-lg border-l-4',
+                    scan.success 
+                      ? 'bg-green-50 border-green-500' 
+                      : 'bg-red-50 border-red-500'
+                  ]"
+                >
+                  <div class="flex items-center justify-between">
+                    <div class="flex-1">
+                      <div class="flex items-center space-x-2">
+                        <i 
+                          :class="scan.success ? 'fas fa-check-circle text-green-500' : 'fas fa-times-circle text-red-500'"
+                        ></i>
+                        <span class="font-medium text-gray-900">
+                          {{ scan.passengerName || 'Passager inconnu' }}
+                        </span>
+                      </div>
+                      <p class="text-sm text-gray-600 mt-1">
+                        {{ scan.success ? 'Ticket validé avec succès' : scan.error }}
+                      </p>
+                      <p class="text-xs text-gray-500 mt-1">
+                        {{ formatTime(scan.timestamp) }}
+                      </p>
+                    </div>
+                    <div v-if="scan.success" class="text-right">
+                      <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                        Siège {{ scan.seatNumber }}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Statistiques rapides -->
+            <div class="grid grid-cols-2 gap-4">
+              <div class="bg-green-50 rounded-lg p-3 text-center">
+                <div class="text-2xl font-bold text-green-600">{{ successfulScans }}</div>
+                <div class="text-sm text-green-700">Validations réussies</div>
+              </div>
+              <div class="bg-red-50 rounded-lg p-3 text-center">
+                <div class="text-2xl font-bold text-red-600">{{ failedScans }}</div>
+                <div class="text-sm text-red-700">Échecs</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Modal de confirmation -->
+    <div v-if="showValidationModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
+      <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
+        <div class="mt-3 text-center">
+          <div 
+            :class="[
+              'mx-auto flex items-center justify-center h-12 w-12 rounded-full',
+              validationResult.success ? 'bg-green-100' : 'bg-red-100'
+            ]"
+          >
+            <i 
+              :class="[
+                'text-xl',
+                validationResult.success ? 'fas fa-check text-green-600' : 'fas fa-times text-red-600'
+              ]"
+            ></i>
+          </div>
+          <h3 class="text-lg font-medium text-gray-900 mt-2">
+            {{ validationResult.success ? 'Ticket validé !' : 'Validation échouée' }}
+          </h3>
+          <div class="mt-2 px-7 py-3">
+            <p class="text-sm text-gray-500">
+              {{ validationResult.message }}
+            </p>
+          </div>
+          <div class="items-center px-4 py-3">
+            <button
+              @click="closeValidationModal"
+              :class="[
+                'px-4 py-2 text-white text-base font-medium rounded-md w-full shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2',
+                validationResult.success 
+                  ? 'bg-green-500 hover:bg-green-600 focus:ring-green-300' 
+                  : 'bg-red-500 hover:bg-red-600 focus:ring-red-300'
+              ]"
+            >
+              OK
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import EmployeeService, { type TicketValidationResult } from '@/services/EmployeeService'
+
+// Props
+interface Trip {
+  id: number
+  departure: string
+  arrival: string
+  departureTime: string
+  arrivalTime: string
+  date: string
+  passengers: number
+  capacity: number
+  status: string
+}
+
+interface Props {
+  trip?: Trip
+}
+
+const props = defineProps<Props>()
+
+// Émissions
+const emit = defineEmits<{
+  'ticket-validated': [data: any]
+  'back': []
+}>()
+
+// État réactif
+const videoElement = ref<HTMLVideoElement>()
+const isCameraActive = ref(false)
+const cameraError = ref('')
+const manualCode = ref('')
+const showValidationModal = ref(false)
+const validationResult = ref({ success: false, message: '' })
+const validatedCount = ref(0)
+
+interface ScanRecord {
+  id: number
+  timestamp: string
+  success: boolean
+  passengerName?: string
+  seatNumber?: string
+  error?: string
+  qrCode: string
+}
+
+const scanHistory = ref<ScanRecord[]>([])
+
+// Variables pour le scanner QR
+let stream: MediaStream | null = null
+let scanInterval: number | null = null
+
+// Computed
+const successfulScans = computed(() => 
+  scanHistory.value.filter(scan => scan.success).length
+)
+
+const failedScans = computed(() => 
+  scanHistory.value.filter(scan => !scan.success).length
+)
+
+// Méthodes
+const toggleCamera = () => {
+  if (isCameraActive.value) {
+    stopCamera()
+  } else {
+    initCamera()
+  }
+}
+
+const initCamera = async () => {
+  try {
+    cameraError.value = ''
+    
+    // Demander l'accès à la caméra
+    stream = await navigator.mediaDevices.getUserMedia({
+      video: {
+        facingMode: 'environment', // Caméra arrière si disponible
+        width: { ideal: 640 },
+        height: { ideal: 640 }
+      }
+    })
+    
+    if (videoElement.value) {
+      videoElement.value.srcObject = stream
+      isCameraActive.value = true
+      
+      // Démarrer la détection QR
+      startQRDetection()
+    }
+  } catch (error) {
+    console.error('Erreur d\'accès à la caméra:', error)
+    cameraError.value = 'Impossible d\'accéder à la caméra. Vérifiez les permissions.'
+    isCameraActive.value = false
+  }
+}
+
+const stopCamera = () => {
+  if (stream) {
+    stream.getTracks().forEach(track => track.stop())
+    stream = null
+  }
+  
+  if (scanInterval) {
+    clearInterval(scanInterval)
+    scanInterval = null
+  }
+  
+  isCameraActive.value = false
+}
+
+const startQRDetection = () => {
+  // Simulation de la détection QR (remplacer par une vraie librairie comme jsQR)
+  scanInterval = window.setInterval(() => {
+    if (isCameraActive.value && videoElement.value) {
+      // Ici, vous intégreriez une vraie librairie de détection QR
+      // comme jsQR ou qr-scanner
+      detectQRCode()
+    }
+  }, 1000)
+}
+
+const detectQRCode = () => {
+  // Simulation de détection QR
+  // Dans un vrai projet, vous utiliseriez jsQR ou une autre librairie
+  
+  // Exemple de simulation aléatoire pour la démo
+  if (Math.random() < 0.1) { // 10% de chance de détecter un QR
+    const mockQRCode = `TICKET_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
+    validateQRCode(mockQRCode)
+  }
+}
+
+const validateQRCode = async (qrCode: string) => {
+  try {
+    // Utilisation de l'API réelle pour valider le ticket
+    const response = await validateTicketAPI(qrCode)
+    
+    const scanRecord: ScanRecord = {
+      id: Date.now(),
+      timestamp: new Date().toISOString(),
+      success: response.success,
+      qrCode
+    }
+    
+    if (response.success) {
+      scanRecord.passengerName = response.passengerName
+      scanRecord.seatNumber = response.seatNumber
+      validatedCount.value++
+      
+      validationResult.value = {
+        success: true,
+        message: `Ticket validé pour ${response.passengerName} (Siège ${response.seatNumber})`
+      }
+      
+      emit('ticket-validated', response)
+    } else {
+      scanRecord.error = response.error
+      validationResult.value = {
+        success: false,
+        message: response.error
+      }
+    }
+    
+    scanHistory.value.unshift(scanRecord)
+    showValidationModal.value = true
+    
+    // Arrêter temporairement le scan pour éviter les doublons
+    if (scanInterval) {
+      clearInterval(scanInterval)
+      setTimeout(() => {
+        if (isCameraActive.value) {
+          startQRDetection()
+        }
+      }, 2000)
+    }
+    
+  } catch (error) {
+    console.error('Erreur lors de la validation:', error)
+    
+    const scanRecord: ScanRecord = {
+      id: Date.now(),
+      timestamp: new Date().toISOString(),
+      success: false,
+      error: 'Erreur de connexion',
+      qrCode
+    }
+    
+    scanHistory.value.unshift(scanRecord)
+    
+    validationResult.value = {
+      success: false,
+      message: 'Erreur de connexion. Veuillez réessayer.'
+    }
+    
+    showValidationModal.value = true
+  }
+}
+
+const validateManualCode = () => {
+  if (manualCode.value.trim()) {
+    validateQRCode(manualCode.value.trim())
+    manualCode.value = ''
+  }
+}
+
+const closeValidationModal = () => {
+  showValidationModal.value = false
+}
+
+// Validation des tickets via l'API
+const validateTicketAPI = async (qrCode: string) => {
+  try {
+    const result = await EmployeeService.validateTicket(qrCode, props.trip?.id)
+    
+    if (result.success) {
+      return {
+        success: true,
+        passengerName: `${result.passenger.firstName} ${result.passenger.lastName}`,
+        seatNumber: result.passenger.seatNumber,
+        ticketId: qrCode
+      }
+    } else {
+      return {
+        success: false,
+        error: result.message || 'Ticket invalide'
+      }
+    }
+  } catch (error) {
+    return {
+      success: false,
+      error: 'Erreur de connexion au serveur'
+    }
+  }
+}
+
+const formatDate = (date: string) => {
+  if (!date) return 'Date non définie'
+  
+  const today = new Date().toISOString().split('T')[0]
+  const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]
+  
+  if (date === today) return 'Aujourd\'hui'
+  if (date === tomorrow) return 'Demain'
+  
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'long',
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric'
+  })
+}
+
+const formatTime = (timestamp: string) => {
+  return new Date(timestamp).toLocaleTimeString('fr-FR', {
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  })
+}
+
+// Lifecycle
+onMounted(() => {
+  // Vérifier la disponibilité de la caméra
+  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+    cameraError.value = 'Votre navigateur ne supporte pas l\'accès à la caméra'
+  }
+})
+
+onUnmounted(() => {
+  stopCamera()
+})
+</script>
+
+<style scoped>
+/* Animation pour la ligne de scan */
+@keyframes scan {
+  0% {
+    top: 0;
+  }
+  50% {
+    top: calc(100% - 2px);
+  }
+  100% {
+    top: 0;
+  }
+}
+
+.animate-scan {
+  animation: scan 2s ease-in-out infinite;
+}
+
+/* Responsive design */
+@media (max-width: 1024px) {
+  .grid-cols-1.lg\:grid-cols-2 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+}
+
+/* Styles pour le modal */
+.fixed {
+  position: fixed;
+}
+
+.inset-0 {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.z-50 {
+  z-index: 50;
+}
+
+/* Styles pour la caméra */
+video {
+  transform: scaleX(-1); /* Effet miroir pour une meilleure UX */
+}
+
+/* Scrollbar personnalisée pour l'historique */
+.overflow-y-auto::-webkit-scrollbar {
+  width: 6px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.overflow-y-auto::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+</style>

+ 629 - 0
app_transport_front/src/components/employee/TripDetails.vue

@@ -0,0 +1,629 @@
+<template>
+  <div class="space-y-6">
+    <!-- En-tête avec navigation -->
+    <div class="bg-white shadow rounded-lg">
+      <div class="px-4 py-5 sm:p-6">
+        <div class="flex items-center justify-between mb-4">
+          <button
+            @click="$emit('back')"
+            class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+          >
+            <i class="fas fa-arrow-left mr-2"></i>
+            Retour à la liste
+          </button>
+          
+          <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"
+            >
+              <i class="fas fa-qrcode mr-2"></i>
+              Scanner les tickets
+            </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"
+            >
+              <i class="fas fa-sync-alt mr-2"></i>
+              Actualiser
+            </button>
+          </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 }}
+            </h1>
+            <span 
+              :class="getStatusBadgeClass(tripDetails.status)"
+              class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
+            >
+              <div 
+                :class="getStatusColor(tripDetails.status)"
+                class="w-2 h-2 rounded-full mr-2"
+              ></div>
+              {{ getStatusText(tripDetails.status) }}
+            </span>
+          </div>
+
+          <!-- Grille d'informations -->
+          <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
+            <div class="bg-gray-50 rounded-lg p-4">
+              <div class="flex items-center">
+                <i class="fas fa-calendar text-blue-500 text-xl mr-3"></i>
+                <div>
+                  <p class="text-sm font-medium text-gray-500">Date</p>
+                  <p class="text-lg font-semibold text-gray-900">{{ formatDate(tripDetails.date) }}</p>
+                </div>
+              </div>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4">
+              <div class="flex items-center">
+                <i class="fas fa-clock text-green-500 text-xl mr-3"></i>
+                <div>
+                  <p class="text-sm font-medium text-gray-500">Horaires</p>
+                  <p class="text-lg font-semibold text-gray-900">{{ tripDetails.departureTime }} - {{ tripDetails.arrivalTime }}</p>
+                </div>
+              </div>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4">
+              <div class="flex items-center">
+                <i class="fas fa-users text-purple-500 text-xl mr-3"></i>
+                <div>
+                  <p class="text-sm font-medium text-gray-500">Passagers</p>
+                  <p class="text-lg font-semibold text-gray-900">{{ tripDetails.occupiedSeats }}/{{ tripDetails.totalSeats }}</p>
+                </div>
+              </div>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4">
+              <div class="flex items-center">
+                <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>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- Barre de progression -->
+          <div class="mt-6">
+            <div class="flex justify-between text-sm text-gray-600 mb-2">
+              <span>Taux d'occupation</span>
+              <span>{{ Math.round((tripDetails.occupiedSeats / tripDetails.totalSeats) * 100) }}%</span>
+            </div>
+            <div class="w-full bg-gray-200 rounded-full h-3">
+              <div 
+                class="bg-gradient-to-r from-blue-500 to-green-500 h-3 rounded-full transition-all duration-500"
+                :style="{ width: `${(tripDetails.occupiedSeats / tripDetails.totalSeats) * 100}%` }"
+              ></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Statistiques des tickets -->
+    <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
+      <div class="bg-white overflow-hidden shadow rounded-lg">
+        <div class="p-5">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-ticket-alt text-blue-500 text-2xl"></i>
+            </div>
+            <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>
+              </dl>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="bg-white overflow-hidden shadow rounded-lg">
+        <div class="p-5">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-check-circle text-green-500 text-2xl"></i>
+            </div>
+            <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>
+              </dl>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="bg-white overflow-hidden shadow rounded-lg">
+        <div class="p-5">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-clock text-yellow-500 text-2xl"></i>
+            </div>
+            <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>
+              </dl>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="bg-white overflow-hidden shadow rounded-lg">
+        <div class="p-5">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-times-circle text-red-500 text-2xl"></i>
+            </div>
+            <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>
+              </dl>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Liste des passagers -->
+    <div class="bg-white shadow rounded-lg">
+      <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">
+            <i class="fas fa-users mr-2 text-blue-600"></i>
+            Liste des passagers
+          </h3>
+          
+          <!-- Filtres -->
+          <div class="flex space-x-3">
+            <select
+              v-model="passengerFilter"
+              class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+            >
+              <option value="">Tous les passagers</option>
+              <option value="validated">Validés</option>
+              <option value="pending">En attente</option>
+              <option value="cancelled">Annulés</option>
+            </select>
+            
+            <input
+              v-model="passengerSearch"
+              type="text"
+              placeholder="Rechercher un passager..."
+              class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+            />
+          </div>
+        </div>
+
+        <!-- Table des passagers -->
+        <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
+          <table class="min-w-full divide-y divide-gray-300">
+            <thead class="bg-gray-50">
+              <tr>
+                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                  Passager
+                </th>
+                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                  Siège
+                </th>
+                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                  Statut
+                </th>
+                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                  Heure validation
+                </th>
+                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                  Actions
+                </th>
+              </tr>
+            </thead>
+            <tbody class="bg-white divide-y divide-gray-200">
+              <tr v-if="filteredPassengers.length === 0">
+                <td colspan="5" class="px-6 py-12 text-center text-gray-500">
+                  <i class="fas fa-users text-4xl mb-4 text-gray-300"></i>
+                  <p>Aucun passager trouvé</p>
+                </td>
+              </tr>
+              <tr v-for="passenger in filteredPassengers" :key="passenger.id" class="hover:bg-gray-50">
+                <td class="px-6 py-4 whitespace-nowrap">
+                  <div class="flex items-center">
+                    <div class="flex-shrink-0 h-10 w-10">
+                      <div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
+                        <i class="fas fa-user text-gray-600"></i>
+                      </div>
+                    </div>
+                    <div class="ml-4">
+                      <div class="text-sm font-medium text-gray-900">{{ passenger.firstName }} {{ passenger.lastName }}</div>
+                      <div class="text-sm text-gray-500">{{ passenger.email }}</div>
+                    </div>
+                  </div>
+                </td>
+                <td class="px-6 py-4 whitespace-nowrap">
+                  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
+                    {{ passenger.seatNumber }}
+                  </span>
+                </td>
+                <td class="px-6 py-4 whitespace-nowrap">
+                  <span 
+                    :class="getTicketStatusClass(passenger.ticketStatus)"
+                    class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
+                  >
+                    <div 
+                      :class="getTicketStatusColor(passenger.ticketStatus)"
+                      class="w-1.5 h-1.5 rounded-full mr-1.5"
+                    ></div>
+                    {{ getTicketStatusText(passenger.ticketStatus) }}
+                  </span>
+                </td>
+                <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+                  {{ passenger.validatedAt ? formatDateTime(passenger.validatedAt) : '-' }}
+                </td>
+                <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
+                  <button
+                    v-if="passenger.ticketStatus === 'pending'"
+                    @click="validateTicket(passenger)"
+                    class="text-green-600 hover:text-green-900 mr-3"
+                  >
+                    <i class="fas fa-check mr-1"></i>
+                    Valider
+                  </button>
+                  <button
+                    @click="viewPassengerDetails(passenger)"
+                    class="text-blue-600 hover:text-blue-900"
+                  >
+                    <i class="fas fa-eye mr-1"></i>
+                    Détails
+                  </button>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch } from 'vue'
+import EmployeeService, { type TripDetails, type TripPassenger } from '@/services/EmployeeService'
+
+// Props
+const props = defineProps<{
+  tripId: number
+}>()
+
+// État réactif
+const isLoading = ref(false)
+const searchQuery = ref('')
+const statusFilter = ref('all')
+const currentPage = ref(1)
+const itemsPerPage = ref(10)
+const tripDetails = ref<TripDetails | 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
+})
+
+const statistics = computed(() => {
+  if (!tripDetails.value) return { total: 0, validated: 0, pending: 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
+  
+  return {
+    total,
+    validated,
+    pending,
+    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)
+  }
+
+  // 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)
+    )
+  }
+
+  return filtered
+})
+
+// Méthodes
+const loadTripDetails = async () => {
+  isLoading.value = true
+  try {
+    const details = await EmployeeService.getTripDetails(props.tripId)
+    tripDetails.value = details
+  } 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
+  }
+}
+
+const applyFilters = () => {
+  currentPage.value = 1
+  loadPassengers()
+}
+
+const changePage = (page: number) => {
+  currentPage.value = page
+  loadPassengers()
+}
+
+const getStatusColor = (status: string) => {
+  const colors = {
+    'scheduled': 'bg-blue-500',
+    'in-progress': 'bg-green-500',
+    'completed': 'bg-gray-500',
+    'cancelled': 'bg-red-500'
+  }
+  return colors[status] || 'bg-gray-500'
+}
+
+const getStatusBadgeClass = (status: string) => {
+  const classes = {
+    'scheduled': 'bg-blue-100 text-blue-800',
+    'in-progress': 'bg-green-100 text-green-800',
+    'completed': 'bg-gray-100 text-gray-800',
+    'cancelled': 'bg-red-100 text-red-800'
+  }
+  return classes[status] || 'bg-gray-100 text-gray-800'
+}
+
+const getStatusText = (status: string) => {
+  const texts = {
+    'scheduled': 'Programmé',
+    'in-progress': 'En cours',
+    'completed': 'Terminé',
+    'cancelled': 'Annulé'
+  }
+  return texts[status] || status
+}
+
+const getTicketStatusColor = (status: string) => {
+  const colors = {
+    'validated': 'bg-green-500',
+    'pending': 'bg-yellow-500',
+    'cancelled': 'bg-red-500'
+  }
+  return colors[status] || 'bg-gray-500'
+}
+
+const getTicketStatusClass = (status: string) => {
+  const classes = {
+    'validated': 'bg-green-100 text-green-800',
+    'pending': 'bg-yellow-100 text-yellow-800',
+    'cancelled': 'bg-red-100 text-red-800'
+  }
+  return classes[status] || 'bg-gray-100 text-gray-800'
+}
+
+const getTicketStatusText = (status: string) => {
+  const texts = {
+    'validated': 'Validé',
+    'pending': 'En attente',
+    'cancelled': 'Annulé'
+  }
+  return texts[status] || status
+}
+
+const formatDate = (date: string) => {
+  if (!date) return 'Date non définie'
+  
+  const today = new Date().toISOString().split('T')[0]
+  const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]
+  
+  if (date === today) return 'Aujourd\'hui'
+  if (date === tomorrow) return 'Demain'
+  
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'long',
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric'
+  })
+}
+
+const formatDateTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleString('fr-FR', {
+    hour: '2-digit',
+    minute: '2-digit',
+    day: '2-digit',
+    month: '2-digit'
+  })
+}
+
+const validateTicket = (passenger: TripPassenger) => {
+  passenger.ticketStatus = 'validated'
+  passenger.validatedAt = new Date().toISOString()
+  
+  // Ici, vous feriez un appel API pour valider le ticket
+  console.log('Ticket validé pour:', passenger.firstName + ' ' + passenger.lastName)
+}
+
+const viewPassengerDetails = (passenger: TripPassenger) => {
+  // Ouvrir un modal ou naviguer vers les détails du passager
+  console.log('Voir détails de:', passenger.firstName + ' ' + passenger.lastName)
+}
+
+const refreshTripData = async () => {
+  try {
+    // Recharger les données du trajet depuis l'API
+    console.log('Actualisation des données du trajet...')
+  } catch (error) {
+    console.error('Erreur lors de l\'actualisation:', error)
+  }
+}
+
+// Lifecycle
+onMounted(() => {
+  loadTripDetails()
+  loadPassengers()
+})
+
+// Watchers
+watch([searchQuery, statusFilter], () => {
+  applyFilters()
+})
+
+watch(() => props.tripId, () => {
+  loadTripDetails()
+  loadPassengers()
+})
+
+const loadPassengers = async () => {
+  try {
+    const status = statusFilter.value === 'all' ? undefined : statusFilter.value
+    const search = searchQuery.value || undefined
+
+    const result = await EmployeeService.getTripPassengers(
+      props.tripId,
+      currentPage.value,
+      itemsPerPage.value,
+      status,
+      search
+    )
+    
+    passengers.value = result.passengers
+    totalPassengers.value = result.pagination.total
+    totalPages.value = result.pagination.totalPages
+  } catch (error) {
+    console.error('Erreur lors du chargement des passagers:', error)
+    // Fallback avec des données de démonstration
+    passengers.value = [
+      {
+        id: 1,
+        firstName: 'Marie',
+        lastName: 'Koné',
+        phone: '+225 07 12 34 56 78',
+        email: 'marie.kone@email.com',
+        seatNumber: 'A1',
+        ticketNumber: 'TK001234',
+        status: 'validated',
+        validatedAt: '2024-01-15T07:45:00Z',
+        qrCode: 'QR001234'
+      }
+    ]
+    totalPassengers.value = 1
+    totalPages.value = 1
+  }
+}
+</script>
+
+<style scoped>
+/* Animations pour les transitions */
+.transition-all {
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 500ms;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+  .grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+  
+  .grid-cols-1.md\:grid-cols-4 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+  
+  .overflow-hidden {
+    overflow-x: auto;
+  }
+  
+  .min-w-full {
+    min-width: 600px;
+  }
+}
+
+@media (min-width: 769px) and (max-width: 1024px) {
+  .md\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+  
+  .md\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+}
+
+@media (min-width: 1025px) {
+  .lg\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+}
+
+/* Gradient pour la barre de progression */
+.bg-gradient-to-r {
+  background-image: linear-gradient(to right, var(--tw-gradient-stops));
+}
+
+.from-blue-500 {
+  --tw-gradient-from: #3b82f6;
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(59, 130, 246, 0));
+}
+
+.to-green-500 {
+  --tw-gradient-to: #10b981;
+}
+</style>

+ 482 - 0
app_transport_front/src/components/employee/TripList.vue

@@ -0,0 +1,482 @@
+<template>
+  <div class="bg-white shadow rounded-lg">
+    <div class="px-4 py-5 sm:p-6">
+      <!-- En-tête avec filtres -->
+      <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 sm:mb-0">
+          <i class="fas fa-route mr-2 text-blue-600"></i>
+          Mes trajets assignés
+        </h3>
+        
+        <!-- Filtres et recherche -->
+        <div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-4">
+          <!-- Recherche -->
+          <div class="relative">
+            <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
+              <i class="fas fa-search text-gray-400"></i>
+            </div>
+            <input
+              v-model="searchQuery"
+              type="text"
+              placeholder="Rechercher un trajet..."
+              class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+            />
+          </div>
+          
+          <!-- Filtre par statut -->
+          <select
+            v-model="statusFilter"
+            class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+          >
+            <option value="">Tous les statuts</option>
+            <option value="scheduled">Programmé</option>
+            <option value="in-progress">En cours</option>
+            <option value="completed">Terminé</option>
+            <option value="cancelled">Annulé</option>
+          </select>
+          
+          <!-- Filtre par date -->
+          <input
+            v-model="dateFilter"
+            type="date"
+            class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+          />
+        </div>
+      </div>
+
+      <!-- Liste des trajets -->
+      <div v-if="filteredTrips.length === 0" class="text-center py-12">
+        <i class="fas fa-route text-gray-400 text-5xl mb-4"></i>
+        <h3 class="text-lg font-medium text-gray-900 mb-2">Aucun trajet trouvé</h3>
+        <p class="text-gray-500">
+          {{ searchQuery || statusFilter || dateFilter 
+            ? 'Aucun trajet ne correspond à vos critères de recherche.' 
+            : 'Aucun trajet assigné pour le moment.' 
+          }}
+        </p>
+      </div>
+
+      <div v-else class="space-y-4">
+        <div
+          v-for="trip in paginatedTrips"
+          :key="trip.id"
+          class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-all duration-200 cursor-pointer"
+          @click="$emit('select-trip', trip)"
+        >
+          <div class="flex items-center justify-between">
+            <div class="flex-1">
+              <!-- Informations principales -->
+              <div class="flex items-center space-x-4 mb-3">
+                <div class="flex-shrink-0">
+                  <div 
+                    :class="getStatusColor(trip.status)"
+                    class="w-3 h-3 rounded-full"
+                  ></div>
+                </div>
+                <div class="flex-1 min-w-0">
+                  <div class="flex items-center space-x-2">
+                    <h4 class="text-lg font-medium text-gray-900 truncate">
+                      {{ trip.departure }} → {{ trip.arrival }}
+                    </h4>
+                    <span 
+                      :class="getStatusBadgeClass(trip.status)"
+                      class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
+                    >
+                      {{ getStatusText(trip.status) }}
+                    </span>
+                  </div>
+                  <p class="text-sm text-gray-500 mt-1">
+                    <i class="fas fa-calendar mr-1"></i>
+                    {{ formatDate(trip.date) }}
+                  </p>
+                </div>
+              </div>
+
+              <!-- Détails du trajet -->
+              <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
+                <div class="flex items-center text-gray-600">
+                  <i class="fas fa-clock mr-2 text-blue-500"></i>
+                  <span>{{ trip.departureTime }} - {{ trip.arrivalTime }}</span>
+                </div>
+                <div class="flex items-center text-gray-600">
+                  <i class="fas fa-users mr-2 text-green-500"></i>
+                  <span>{{ trip.passengers }}/{{ trip.capacity }} passagers</span>
+                </div>
+                <div class="flex items-center text-gray-600">
+                  <i class="fas fa-bus mr-2 text-purple-500"></i>
+                  <span>{{ trip.vehicleNumber || 'N/A' }}</span>
+                </div>
+                <div class="flex items-center text-gray-600">
+                  <i class="fas fa-euro-sign mr-2 text-yellow-500"></i>
+                  <span>{{ trip.price || 'N/A' }} €</span>
+                </div>
+              </div>
+
+              <!-- Barre de progression des passagers -->
+              <div class="mt-3">
+                <div class="flex justify-between text-xs text-gray-600 mb-1">
+                  <span>Occupation</span>
+                  <span>{{ Math.round((trip.passengers / trip.capacity) * 100) }}%</span>
+                </div>
+                <div class="w-full bg-gray-200 rounded-full h-2">
+                  <div 
+                    class="bg-blue-600 h-2 rounded-full transition-all duration-300"
+                    :style="{ width: `${(trip.passengers / trip.capacity) * 100}%` }"
+                  ></div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Actions -->
+            <div class="flex flex-col space-y-2 ml-4">
+              <button
+                @click.stop="$emit('select-trip', trip)"
+                class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
+              >
+                <i class="fas fa-eye mr-1"></i>
+                Détails
+              </button>
+              <button
+                v-if="trip.status === 'scheduled' || trip.status === 'in-progress'"
+                @click.stop="$emit('scan-tickets', trip)"
+                class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors"
+              >
+                <i class="fas fa-qrcode mr-1"></i>
+                Scanner
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Pagination -->
+      <div v-if="totalPages > 1" class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 mt-6">
+        <div class="flex flex-1 justify-between sm:hidden">
+          <button
+            @click="currentPage--"
+            :disabled="currentPage === 1"
+            class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            Précédent
+          </button>
+          <button
+            @click="currentPage++"
+            :disabled="currentPage === totalPages"
+            class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            Suivant
+          </button>
+        </div>
+        <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
+          <div>
+            <p class="text-sm text-gray-700">
+              Affichage de
+              <span class="font-medium">{{ (currentPage - 1) * itemsPerPage + 1 }}</span>
+              à
+              <span class="font-medium">{{ Math.min(currentPage * itemsPerPage, filteredTrips.length) }}</span>
+              sur
+              <span class="font-medium">{{ filteredTrips.length }}</span>
+              résultats
+            </p>
+          </div>
+          <div>
+            <nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
+              <button
+                @click="currentPage--"
+                :disabled="currentPage === 1"
+                class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50 disabled:cursor-not-allowed"
+              >
+                <i class="fas fa-chevron-left"></i>
+              </button>
+              <button
+                v-for="page in visiblePages"
+                :key="page"
+                @click="changePage(page)"
+                :class="[
+                  page === currentPage
+                    ? 'relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600'
+                    : 'relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0'
+                ]"
+              >
+                {{ page }}
+              </button>
+              <button
+                @click="currentPage++"
+                :disabled="currentPage === totalPages"
+                class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50 disabled:cursor-not-allowed"
+              >
+                <i class="fas fa-chevron-right"></i>
+              </button>
+            </nav>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import EmployeeService, { type AssignedTrip } from '@/services/EmployeeService'
+import { useUserStore } from '@/stores/useUserStore'
+
+// Props
+interface Trip {
+  id: number
+  departure: string
+  arrival: string
+  departureTime: string
+  arrivalTime: string
+  date: string
+  passengers: number
+  capacity: number
+  status: 'scheduled' | 'in-progress' | 'completed' | 'cancelled'
+  vehicleNumber?: string
+  price?: number
+}
+
+interface Props {
+  trips: Trip[]
+}
+
+const props = defineProps<Props>()
+
+// Émissions
+const emit = defineEmits<{
+  'select-trip': [trip: Trip]
+  'scan-tickets': [trip: Trip]
+}>()
+
+// État réactif
+const searchQuery = ref('')
+const statusFilter = ref('all')
+const dateFilter = ref('')
+const currentPage = ref(1)
+const itemsPerPage = ref(10)
+const isLoading = ref(false)
+const trips = ref<AssignedTrip[]>([])
+const totalTrips = ref(0)
+const totalPages = ref(1)
+
+const userStore = useUserStore()
+
+// Computed
+const filteredTrips = computed(() => {
+  let filtered = props.trips
+
+  // Filtre par recherche
+  if (searchQuery.value) {
+    const query = searchQuery.value.toLowerCase()
+    filtered = filtered.filter(trip => 
+      trip.departure.toLowerCase().includes(query) ||
+      trip.arrival.toLowerCase().includes(query) ||
+      trip.vehicleNumber?.toLowerCase().includes(query)
+    )
+  }
+
+  // Filtre par statut
+  if (statusFilter.value) {
+    filtered = filtered.filter(trip => trip.status === statusFilter.value)
+  }
+
+  // Filtre par date
+  if (dateFilter.value) {
+    filtered = filtered.filter(trip => trip.date === dateFilter.value)
+  }
+
+  return filtered
+})
+
+const paginatedTrips = computed(() => {
+  const start = (currentPage.value - 1) * itemsPerPage.value
+  const end = start + itemsPerPage.value
+  return filteredTrips.value.slice(start, end)
+})
+
+const visiblePages = computed(() => {
+  const pages = []
+  const maxVisible = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
+  let end = Math.min(totalPages.value, start + maxVisible - 1)
+  
+  if (end - start + 1 < maxVisible) {
+    start = Math.max(1, end - maxVisible + 1)
+  }
+  
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  
+  return pages
+})
+
+// Méthodes
+const loadTrips = async () => {
+  isLoading.value = true
+  try {
+    const userId = userStore.userId
+    if (!userId) {
+      console.error('ID utilisateur non disponible')
+      return
+    }
+
+    const status = statusFilter.value === 'all' ? undefined : statusFilter.value
+    const date = dateFilter.value || undefined
+
+    const result = await EmployeeService.getAssignedTrips(
+      userId,
+      currentPage.value,
+      itemsPerPage.value,
+      status,
+      date
+    )
+    
+    trips.value = result.trips
+    totalTrips.value = result.pagination.total
+    totalPages.value = result.pagination.totalPages
+  } catch (error) {
+    console.error('Erreur lors du chargement des trajets:', error)
+    // Fallback avec des données de démonstration
+    trips.value = [
+      {
+        id: 1,
+        departure: 'Abidjan',
+        arrival: 'Bouaké',
+        departureTime: '08:00',
+        arrivalTime: '11:30',
+        date: '2024-01-15',
+        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: {
+          id: 1,
+          departureStation: { id: 1, name: 'Gare d\'Abidjan', city: 'Abidjan' },
+          arrivalStation: { id: 2, name: 'Gare de Bouaké', city: 'Bouaké' }
+        }
+      }
+    ]
+    totalTrips.value = 1
+    totalPages.value = 1
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const applyFilters = () => {
+  currentPage.value = 1
+  loadTrips()
+}
+
+const changePage = (page: number) => {
+  currentPage.value = page
+  loadTrips()
+}
+
+const getStatusColor = (status: string) => {
+  const colors = {
+    'scheduled': 'bg-blue-500',
+    'in-progress': 'bg-green-500',
+    'completed': 'bg-gray-500',
+    'cancelled': 'bg-red-500'
+  }
+  return colors[status] || 'bg-gray-500'
+}
+
+const getStatusBadgeClass = (status: string) => {
+  const classes = {
+    'scheduled': 'bg-blue-100 text-blue-800',
+    'in-progress': 'bg-green-100 text-green-800',
+    'completed': 'bg-gray-100 text-gray-800',
+    'cancelled': 'bg-red-100 text-red-800'
+  }
+  return classes[status] || 'bg-gray-100 text-gray-800'
+}
+
+const getStatusText = (status: string) => {
+  const texts = {
+    'scheduled': 'Programmé',
+    'in-progress': 'En cours',
+    'completed': 'Terminé',
+    'cancelled': 'Annulé'
+  }
+  return texts[status] || status
+}
+
+const formatDate = (date: string) => {
+  if (!date) return 'Date non définie'
+  
+  const today = new Date().toISOString().split('T')[0]
+  const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]
+  
+  if (date === today) return 'Aujourd\'hui'
+  if (date === tomorrow) return 'Demain'
+  
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'long',
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric'
+  })
+}
+
+const selectTrip = (trip: any) => {
+  emit('tripSelected', trip)
+}
+
+// Lifecycle
+onMounted(() => {
+  loadTrips()
+})
+
+// Watchers
+watch([searchQuery, statusFilter, dateFilter], () => {
+  applyFilters()
+})
+</script>
+
+<style scoped>
+/* Animations pour les transitions */
+.transition-all {
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 200ms;
+}
+
+.transition-colors {
+  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+
+/* Responsive design */
+@media (max-width: 640px) {
+  .grid-cols-1.sm\:grid-cols-2.lg\:grid-cols-4 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+    gap: 0.5rem;
+  }
+  
+  .space-y-2.sm\:space-y-0.sm\:space-x-4 > * + * {
+    margin-top: 0.5rem;
+    margin-left: 0;
+  }
+}
+
+@media (min-width: 641px) {
+  .sm\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+  
+  .sm\:space-x-4 > * + * {
+    margin-left: 1rem;
+    margin-top: 0;
+  }
+}
+
+@media (min-width: 1024px) {
+  .lg\:grid-cols-4 {
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+  }
+}
+</style>

+ 43 - 29
app_transport_front/src/components/user/TravelTickets.vue

@@ -122,7 +122,7 @@
                 <!-- QR Code -->
                 <div class="bg-white p-3 rounded-lg">
                   <div ref="qrCodeRef" class="w-20 h-20 flex items-center justify-center">
-                    <canvas :id="`qr-${ticket.id}`" class="max-w-full max-h-full"></canvas>
+                    <canvas :id="`qr-${ticket.qrcode?.value || ticket.id}`" class="max-w-full max-h-full"></canvas>
                   </div>
                 </div>
               </div>
@@ -195,7 +195,7 @@
 import { ref, computed, onMounted, nextTick } from 'vue'
 import { useUserStore } from '@/stores/useUserStore'
 import ReservationService  from '@/services/ReservationService'
-import QRCode from 'qrcode'
+import * as QRCode from 'qrcode'
 import type { Reservation } from '@/types/Reservation'
 
 const userStore = useUserStore()
@@ -235,7 +235,6 @@ const loadTickets = async () => {
     for (const reservation of response.data.data) {
       try {
         const details = await ReservationService.getReservationsByDetail(reservation.id)
-        console.log('details for reservation', details.data)
         ticketsWithDetails.push(details.data)
       } catch (error) {
         console.error('Erreur lors de la récupération des détails pour la réservation', reservation.id, error)
@@ -247,7 +246,9 @@ const loadTickets = async () => {
     
     // Générer les QR codes après le chargement
     await nextTick()
-    generateQRCodes()
+    setTimeout(() => {
+      generateQRCodes()
+    }, 100)
   } catch (error) {
     console.error('Erreur lors du chargement des tickets:', error)
   } finally {
@@ -258,30 +259,23 @@ const loadTickets = async () => {
 const generateQRCodes = async () => {
   for (const ticket of filteredTickets.value) {
     try {
-      const canvas = document.getElementById(`qr-${ticket.id}`) as HTMLCanvasElement
-      if (canvas) {
-        const qrData = {
-          reservationId: ticket.id,
-          reservationNumber: ticket.reservationNumber,
-          passengerName: ticket.passengers[0]?.firstName + ' ' + ticket.passengers[0]?.lastName,
-          departure: ticket.trip.departureStation.name,
-          arrival: ticket.trip.arrivalStation.name,
-          date: ticket.trajet.departAt.date,
-          time: ticket.trip.trajet.departAt.date,
-          seats: ticket.numberOfSeats
-        }
-        
-        await QRCode.toCanvas(canvas, JSON.stringify(qrData), {
+      const canvasId = `qr-${ticket.qrcode?.value || ticket.id}`
+      const canvas = document.getElementById(canvasId) as HTMLCanvasElement
+      
+      if (canvas && ticket.qrcode?.value) {
+        // Utiliser directement la valeur du QR code depuis le backend
+        await QRCode.toCanvas(canvas, ticket.qrcode.value, {
           width: 80,
           margin: 1,
           color: {
             dark: '#000000',
             light: '#FFFFFF'
-          }
+          },
+          errorCorrectionLevel: 'M'
         })
       }
     } catch (error) {
-      console.error('Erreur lors de la génération du QR code:', error)
+      console.error('Erreur lors de la génération du QR code pour ticket', ticket.id, ':', error)
     }
   }
 }
@@ -396,36 +390,56 @@ const downloadTicket = async (ticket: Reservation) => {
   }
 }
 
-const printTicket = (ticket: Reservation) => {
+const printTicket = async (ticket: Reservation) => {
   const printWindow = window.open('', '_blank')
   if (!printWindow) return
   
+  // Générer le QR code en base64 pour l'impression
+  let qrCodeDataUrl = ''
+  if (ticket.qrcode?.value) {
+    try {
+      qrCodeDataUrl = await QRCode.toDataURL(ticket.qrcode.value, {
+        width: 150,
+        margin: 2,
+        color: {
+          dark: '#000000',
+          light: '#FFFFFF'
+        },
+        errorCorrectionLevel: 'M'
+      })
+    } catch (error) {
+      console.error('Erreur lors de la génération du QR code pour impression:', error)
+    }
+  }
+  
   const ticketHtml = `
     <!DOCTYPE html>
     <html>
     <head>
-      <title>Ticket - ${ticket.reservationNumber}</title>
+      <title>Ticket - ${ticket.id}</title>
       <style>
         body { font-family: Arial, sans-serif; margin: 20px; }
         .ticket { border: 2px solid #3B82F6; border-radius: 10px; padding: 20px; max-width: 400px; }
         .header { text-align: center; border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-bottom: 20px; }
         .info { margin-bottom: 10px; }
         .qr-code { text-align: center; margin-top: 20px; }
+        .qr-code img { max-width: 150px; height: auto; }
       </style>
     </head>
     <body>
       <div class="ticket">
         <div class="header">
           <h2>TICKET DE VOYAGE</h2>
-          <p>${ticket.reservationNumber}</p>
+          <p>${ticket.reservationNumber || ticket.id}</p>
         </div>
-        <div class="info"><strong>Trajet:</strong> ${ticket.trip.departureStation.name} → ${ticket.trajet.departAt.station.name}</div>
-        <div class="info"><strong>Date:</strong> ${formatDate(ticket.trip.departureDate)}</div>
-        <div class="info"><strong>Heure:</strong> ${formatTime(ticket.trip.departureTime)}</div>
-        <div class="info"><strong>Passager:</strong> ${ticket.passengers[0]?.firstName} ${ticket.passengers[0]?.lastName}</div>
-        <div class="info"><strong>Places:</strong> ${ticket.numberOfSeats}</div>
-        <div class="info"><strong>Prix:</strong> ${formatCurrency(ticket.totalAmount)}</div>
+        <div class="info"><strong>Trajet:</strong> ${ticket.departStation?.name || 'N/A'} → ${ticket.arriveStation?.name || 'N/A'}</div>
+        <div class="info"><strong>Date:</strong> ${ticket.trajet?.departAt ? formatDate(ticket.trajet.departAt.date) : 'N/A'}</div>
+        <div class="info"><strong>Heure:</strong> ${ticket.trajet?.departAt ? formatTime(ticket.trajet.departAt.date) : 'N/A'}</div>
+        <div class="info"><strong>Passager:</strong> ${ticket.user?.name || 'N/A'}</div>
+        <div class="info"><strong>Places:</strong> ${ticket.place?.number || ticket.numberOfSeats || 'N/A'}</div>
+        <div class="info"><strong>Prix:</strong> ${formatCurrency(ticket.price || ticket.totalAmount || 0)}</div>
         <div class="qr-code">
+          ${qrCodeDataUrl ? `<img src="${qrCodeDataUrl}" alt="QR Code" />` : ''}
           <p>Présentez ce ticket lors de l'embarquement</p>
         </div>
       </div>

+ 9 - 0
app_transport_front/src/router/index.ts

@@ -115,6 +115,13 @@ const routes: Array<RouteRecordRaw> = [
     component: () => import('../views/UserDashboardView.vue'),
     meta: { requiresAuth: true, roles: [Role.User] }
   },
+  // Routes pour l'espace employé/conducteur
+  {
+    path: '/employee/dashboard',
+    name: 'EmployeeDashboard',
+    component: () => import('../views/EmployeeDashboard.vue'),
+    meta: { requiresAuth: true, roles: [Role.Employee, Role.Driver] }
+  },
   // {
   //   path: '/driver-setup',
   //   name: 'driver-setup',
@@ -154,6 +161,8 @@ router.beforeEach((to, from, next) => {
         // Pour les managers, on devrait idéalement rediriger vers leur dashboard de compagnie
         // Mais comme on n'a pas l'ID de la compagnie ici, on redirige vers /login
         next('/login')
+      } else if (userRole === Role.Employee || userRole === Role.Driver) {
+        next('/employee/dashboard')
       } else {
         next('/login')
       }

+ 407 - 0
app_transport_front/src/services/EmployeeService.ts

@@ -0,0 +1,407 @@
+import { authenticatedFetch } from '@/services/authService'
+
+const API_URL = (import.meta as any).env.VITE_API_BASE_URL || 'http://localhost:3001/api'
+
+export interface AssignedTrip {
+  id: number
+  departure: string
+  arrival: string
+  departureTime: string
+  arrivalTime: string
+  date: string
+  passengers: number
+  capacity: number
+  status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
+  vehicle: {
+    id: number
+    licensePlate: string
+    model: string
+    capacity: number
+  }
+  driver?: {
+    id: number
+    name: string
+    otherName: string
+    telephone: string
+  }
+  company: {
+    id: number
+    name: string
+  }
+  route: {
+    id: number
+    departureStation: {
+      id: number
+      name: string
+      city: string
+    }
+    arrivalStation: {
+      id: number
+      name: string
+      city: string
+    }
+  }
+}
+
+export interface TripPassenger {
+  id: number
+  name: string
+  otherName: string
+  telephone: string
+  email?: string
+  seatNumber: string
+  ticketStatus: 'valid' | 'validated' | 'expired' | 'cancelled'
+  qrCode: string
+  reservationId: number
+  validatedAt?: string
+  validatedBy?: {
+    id: number
+    name: string
+    role: string
+  }
+}
+
+export interface TicketValidationResult {
+  success: boolean
+  message: string
+  passenger?: {
+    id: number
+    name: string
+    otherName: string
+    seatNumber: string
+    ticketId: string
+  }
+  error?: string
+}
+
+export interface TripStatistics {
+  totalPassengers: number
+  validatedTickets: number
+  pendingTickets: number
+  revenue: number
+  occupancyRate: number
+}
+
+export default {
+  /**
+   * Récupérer les trajets assignés à un employé/conducteur
+   */
+  async getAssignedTrips(
+    employeeId: number, 
+    page: number = 1, 
+    limit: number = 10,
+    status?: string,
+    date?: string
+  ): Promise<{ 
+    trips: AssignedTrip[], 
+    pagination: { total: number, page: number, limit: number, totalPages: number } 
+  }> {
+    try {
+      let url = `${API_URL}/employees/${employeeId}/trips?page=${page}&limit=${limit}`
+      
+      if (status) {
+        url += `&status=${encodeURIComponent(status)}`
+      }
+      
+      if (date) {
+        url += `&date=${encodeURIComponent(date)}`
+      }
+      
+      const response = await authenticatedFetch(url)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des trajets assignés')
+      }
+      
+      const result = await response.json()
+      return {
+        trips: result.data?.data || result.data || [],
+        pagination: {
+          total: result.data?.total || 0,
+          page: result.data?.page || 1,
+          limit: result.data?.limit || 10,
+          totalPages: result.data?.totalPages || 1
+        }
+      }
+    } catch (error) {
+      console.error('Erreur lors de la récupération des trajets assignés:', error)
+      return {
+        trips: [],
+        pagination: { total: 0, page: 1, limit: 10, totalPages: 1 }
+      }
+    }
+  },
+
+  /**
+   * Récupérer les détails d'un trajet spécifique
+   */
+  async getTripDetails(tripId: number): Promise<AssignedTrip | null> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/trajets/${tripId}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des détails du trajet')
+      }
+      
+      const result = await response.json()
+      return result.data || null
+    } catch (error) {
+      console.error('Erreur lors de la récupération des détails du trajet:', error)
+      return null
+    }
+  },
+
+  /**
+   * Récupérer la liste des passagers d'un trajet
+   */
+  async getTripPassengers(
+    tripId: number,
+    page: number = 1,
+    limit: number = 50,
+    search?: string,
+    status?: string
+  ): Promise<{
+    passengers: TripPassenger[],
+    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')
+      }
+      
+      const result = await response.json()
+      return {
+        passengers: result.data?.data || result.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 lors de la récupération des passagers:', error)
+      return {
+        passengers: [],
+        pagination: { total: 0, page: 1, limit: 50, totalPages: 1 }
+      }
+    }
+  },
+
+  /**
+   * Valider un ticket via QR code
+   */
+  async validateTicket(qrCode: string, tripId: number, employeeId: number): Promise<TicketValidationResult> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/tickets/validate`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          qrCode,
+          tripId,
+          validatedBy: employeeId
+        })
+      })
+      
+      const result = await response.json()
+      
+      if (!response.ok) {
+        return {
+          success: false,
+          message: result.message || 'Erreur lors de la validation du ticket',
+          error: result.error || 'Erreur inconnue'
+        }
+      }
+      
+      return {
+        success: true,
+        message: result.message || 'Ticket validé avec succès',
+        passenger: result.data?.passenger || result.passenger
+      }
+    } catch (error) {
+      console.error('Erreur lors de la validation du ticket:', error)
+      return {
+        success: false,
+        message: 'Erreur de connexion au serveur',
+        error: 'Erreur réseau'
+      }
+    }
+  },
+
+  /**
+   * Récupérer les statistiques d'un trajet
+   */
+  async getTripStatistics(tripId: number): Promise<TripStatistics | null> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/trajets/${tripId}/statistics`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des statistiques')
+      }
+      
+      const result = await response.json()
+      return result.data || null
+    } catch (error) {
+      console.error('Erreur lors de la récupération des statistiques:', error)
+      return null
+    }
+  },
+
+  /**
+   * Marquer un trajet comme commencé
+   */
+  async startTrip(tripId: number, employeeId: number): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/trajets/${tripId}/start`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          startedBy: employeeId,
+          startTime: new Date().toISOString()
+        })
+      })
+      
+      const result = await response.json()
+      
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Trajet démarré avec succès' : 'Erreur lors du démarrage du trajet')
+      }
+    } catch (error) {
+      console.error('Erreur lors du démarrage du trajet:', error)
+      return {
+        success: false,
+        message: 'Erreur de connexion au serveur'
+      }
+    }
+  },
+
+  /**
+   * Marquer un trajet comme terminé
+   */
+  async completeTrip(tripId: number, employeeId: number): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/trajets/${tripId}/complete`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          completedBy: employeeId,
+          endTime: new Date().toISOString()
+        })
+      })
+      
+      const result = await response.json()
+      
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Trajet terminé avec succès' : 'Erreur lors de la finalisation du trajet')
+      }
+    } catch (error) {
+      console.error('Erreur lors de la finalisation du trajet:', error)
+      return {
+        success: false,
+        message: 'Erreur de connexion au serveur'
+      }
+    }
+  },
+
+  /**
+   * Récupérer l'historique des validations d'un employé
+   */
+  async getValidationHistory(
+    employeeId: number,
+    page: number = 1,
+    limit: number = 20,
+    tripId?: number,
+    date?: string
+  ): Promise<{
+    validations: any[],
+    pagination: { total: number, page: number, limit: number, totalPages: number }
+  }> {
+    try {
+      let url = `${API_URL}/employees/${employeeId}/validations?page=${page}&limit=${limit}`
+      
+      if (tripId) {
+        url += `&tripId=${tripId}`
+      }
+      
+      if (date) {
+        url += `&date=${encodeURIComponent(date)}`
+      }
+      
+      const response = await authenticatedFetch(url)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération de l\'historique des validations')
+      }
+      
+      const result = await response.json()
+      return {
+        validations: result.data?.data || result.data || [],
+        pagination: {
+          total: result.data?.total || 0,
+          page: result.data?.page || 1,
+          limit: result.data?.limit || 20,
+          totalPages: result.data?.totalPages || 1
+        }
+      }
+    } catch (error) {
+      console.error('Erreur lors de la récupération de l\'historique des validations:', error)
+      return {
+        validations: [],
+        pagination: { total: 0, page: 1, limit: 20, totalPages: 1 }
+      }
+    }
+  },
+
+  /**
+   * Signaler un problème sur un trajet
+   */
+  async reportIssue(
+    tripId: number, 
+    employeeId: number, 
+    issueType: string, 
+    description: string
+  ): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/trajets/${tripId}/issues`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          reportedBy: employeeId,
+          issueType,
+          description,
+          reportedAt: new Date().toISOString()
+        })
+      })
+      
+      const result = await response.json()
+      
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Problème signalé avec succès' : 'Erreur lors du signalement')
+      }
+    } catch (error) {
+      console.error('Erreur lors du signalement du problème:', error)
+      return {
+        success: false,
+        message: 'Erreur de connexion au serveur'
+      }
+    }
+  }
+}

+ 5 - 7
app_transport_front/src/types/index.ts

@@ -147,14 +147,12 @@ export interface Transaction {
 }
 
 export interface Qrcode {
-  id: number
-  code: string
-  type: string
-  data: string
-  isActive: boolean
-  expiresAt?: string
+  id: string
+  reservationId: number
+  value: string
   createdAt: string
-  updatedAt?: string
+  scannedAt?: string | null
+  reservation?: Reservation
 }
 
 import { Role } from '@/Enum/Role'

+ 473 - 0
app_transport_front/src/views/EmployeeDashboard.vue

@@ -0,0 +1,473 @@
+<template>
+  <div class="min-h-screen bg-gray-50">
+    <!-- Header -->
+    <div class="bg-white shadow-sm border-b">
+      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+        <div class="flex justify-between items-center py-6">
+          <div class="flex items-center">
+            <h1 class="text-2xl font-bold text-gray-900">
+              <i class="fas fa-briefcase mr-2 text-blue-600"></i>
+              Espace {{ userStore?.userRole === 'DRIVER' ? 'Conducteur' : 'Employé' }}
+            </h1>
+          </div>
+          <div class="flex items-center space-x-4">
+            <span class="text-sm text-gray-500">
+              Bienvenue, {{ userStore?.name }} {{ userStore?.otherName }}
+            </span>
+            <button @click="logout" class="text-sm text-red-600 hover:text-red-800">
+              <i class="fas fa-sign-out-alt mr-1"></i>
+              Déconnexion
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Navigation Tabs -->
+    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
+      <div class="border-b border-gray-200">
+        <nav class="-mb-px flex space-x-8 overflow-x-auto">
+          <button
+            v-for="tab in tabs"
+            :key="tab.id"
+            @click="activeTab = tab.id"
+            :class="[
+              activeTab === tab.id
+                ? 'border-blue-500 text-blue-600'
+                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
+              'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm flex items-center',
+            ]"
+          >
+            <i :class="tab.icon" class="mr-2"></i>
+            {{ tab.name }}
+          </button>
+        </nav>
+      </div>
+    </div>
+
+    <!-- Content -->
+    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
+      <!-- Vue d'ensemble -->
+      <div v-if="activeTab === 'overview'" class="space-y-6">
+        <!-- Statistiques rapides -->
+        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
+          <div class="bg-white overflow-hidden shadow rounded-lg">
+            <div class="p-5">
+              <div class="flex items-center">
+                <div class="flex-shrink-0">
+                  <i class="fas fa-route text-blue-500 text-2xl"></i>
+                </div>
+                <div class="ml-5 w-0 flex-1">
+                  <dl>
+                    <dt class="text-sm font-medium text-gray-500 truncate">
+                      Trajets assignés
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ stats.assignedTrips }}
+                    </dd>
+                  </dl>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="bg-white overflow-hidden shadow rounded-lg">
+            <div class="p-5">
+              <div class="flex items-center">
+                <div class="flex-shrink-0">
+                  <i class="fas fa-qrcode text-green-500 text-2xl"></i>
+                </div>
+                <div class="ml-5 w-0 flex-1">
+                  <dl>
+                    <dt class="text-sm font-medium text-gray-500 truncate">
+                      Tickets scannés
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ stats.scannedTickets }}
+                    </dd>
+                  </dl>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="bg-white overflow-hidden shadow rounded-lg">
+            <div class="p-5">
+              <div class="flex items-center">
+                <div class="flex-shrink-0">
+                  <i class="fas fa-users text-purple-500 text-2xl"></i>
+                </div>
+                <div class="ml-5 w-0 flex-1">
+                  <dl>
+                    <dt class="text-sm font-medium text-gray-500 truncate">
+                      Passagers aujourd'hui
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ stats.todayPassengers }}
+                    </dd>
+                  </dl>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="bg-white overflow-hidden shadow rounded-lg">
+            <div class="p-5">
+              <div class="flex items-center">
+                <div class="flex-shrink-0">
+                  <i class="fas fa-clock text-yellow-500 text-2xl"></i>
+                </div>
+                <div class="ml-5 w-0 flex-1">
+                  <dl>
+                    <dt class="text-sm font-medium text-gray-500 truncate">
+                      Prochain trajet
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ nextTripTime }}
+                    </dd>
+                  </dl>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Trajets du jour -->
+        <div class="bg-white shadow rounded-lg">
+          <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
+            </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>
+              <p class="text-gray-500">Aucun trajet assigné pour aujourd'hui</p>
+            </div>
+            <div v-else class="space-y-4">
+              <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"
+                @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>
+                      <div class="flex-1 min-w-0">
+                        <p class="text-sm font-medium text-gray-900 truncate">
+                          {{ trip.departure }} → {{ trip.arrival }}
+                        </p>
+                        <p class="text-sm text-gray-500">
+                          {{ formatTime(trip.departureTime) }} - {{ formatTime(trip.arrivalTime) }}
+                        </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>
+                    <i class="fas fa-chevron-right text-gray-400"></i>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Liste des trajets -->
+      <div v-if="activeTab === 'trips'">
+        <TripList 
+          :trips="allTrips" 
+          @select-trip="selectTrip"
+          @scan-tickets="openScanner"
+        />
+      </div>
+
+      <!-- Détails du trajet -->
+      <div v-if="activeTab === 'trip-details' && selectedTrip">
+        <TripDetails 
+          :trip="selectedTrip"
+          @scan-tickets="openScanner"
+          @back="activeTab = 'trips'"
+        />
+      </div>
+
+      <!-- Scanner QR -->
+      <div v-if="activeTab === 'scanner'">
+        <QRScanner 
+          :trip="selectedTrip"
+          @ticket-validated="onTicketValidated"
+          @back="activeTab = selectedTrip ? 'trip-details' : 'trips'"
+        />
+      </div>
+    </div>
+
+    <!-- Modal de confirmation -->
+    <div v-if="showValidationModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
+      <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
+        <div class="mt-3 text-center">
+          <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
+            <i class="fas fa-check text-green-600 text-xl"></i>
+          </div>
+          <h3 class="text-lg font-medium text-gray-900 mt-2">Ticket validé !</h3>
+          <div class="mt-2 px-7 py-3">
+            <p class="text-sm text-gray-500">
+              Le ticket a été validé avec succès.
+            </p>
+          </div>
+          <div class="items-center px-4 py-3">
+            <button
+              @click="showValidationModal = false"
+              class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
+            >
+              OK
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/useUserStore'
+import TripList from '@/components/employee/TripList.vue'
+import TripDetails from '@/components/employee/TripDetails.vue'
+import QRScanner from '@/components/employee/QRScanner.vue'
+import EmployeeService, { type AssignedTrip } from '@/services/EmployeeService'
+
+const router = useRouter()
+const userStore = useUserStore()
+
+// État réactif
+const activeTab = ref('overview')
+const selectedTrip = ref(null)
+const showValidationModal = ref(false)
+
+// Données simulées (à remplacer par des appels API)
+const stats = ref({
+  assignedTrips: 5,
+  scannedTickets: 23,
+  todayPassengers: 45,
+})
+
+const todayTrips = ref([
+  {
+    id: 1,
+    departure: 'Abidjan',
+    arrival: 'Bouaké',
+    departureTime: '08:00',
+    arrivalTime: '11:30',
+    passengers: 28,
+    capacity: 35,
+    status: 'scheduled'
+  },
+  {
+    id: 2,
+    departure: 'Bouaké',
+    arrival: 'Abidjan',
+    departureTime: '14:00',
+    arrivalTime: '17:30',
+    passengers: 32,
+    capacity: 35,
+    status: 'scheduled'
+  }
+])
+
+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'
+  }
+])
+
+// Onglets de navigation
+const tabs = computed(() => [
+  { id: 'overview', name: 'Vue d\'ensemble', icon: 'fas fa-tachometer-alt' },
+  { id: 'trips', name: 'Mes trajets', icon: 'fas fa-route' },
+  { id: 'trip-details', name: 'Détails trajet', icon: 'fas fa-info-circle', hidden: !selectedTrip.value },
+  { id: 'scanner', name: 'Scanner QR', icon: 'fas fa-qrcode', hidden: true }
+].filter(tab => !tab.hidden))
+
+// Computed
+const nextTripTime = computed(() => {
+  if (todayTrips.value.length > 0) {
+    return todayTrips.value[0].departureTime
+  }
+  return 'Aucun'
+})
+
+// Méthodes
+const loadDashboardData = async () => {
+  isLoading.value = true
+  try {
+    // Chargement des données réelles via le service
+    await Promise.all([
+      loadTrips(),
+      loadStatistics()
+    ])
+  } catch (error) {
+    console.error('Erreur lors du chargement des données:', error)
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const loadTrips = async () => {
+  try {
+    const userId = userStore.userId
+    if (!userId) {
+      console.error('ID utilisateur non disponible')
+      return
+    }
+
+    const result = await EmployeeService.getAssignedTrips(
+      userId,
+      1, // page
+      10, // limit
+      undefined, // status filter
+      new Date().toISOString().split('T')[0] // 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: {
+          id: 1,
+          departureStation: { id: 1, name: 'Gare d\'Abidjan', city: 'Abidjan' },
+          arrivalStation: { id: 2, name: 'Gare de Bouaké', city: 'Bouaké' }
+        }
+      }
+    ]
+  }
+}
+
+const logout = () => {
+  userStore.logout()
+  router.push('/login')
+}
+
+const selectTrip = (trip: any) => {
+  selectedTrip.value = trip
+  activeTab.value = 'trip-details'
+}
+
+const openScanner = (trip?: any) => {
+  if (trip) {
+    selectedTrip.value = trip
+  }
+  activeTab.value = 'scanner'
+}
+
+const onTicketValidated = (ticketData: any) => {
+  stats.value.scannedTickets++
+  showValidationModal.value = true
+  
+  // Mettre à jour les statistiques du trajet
+  if (selectedTrip.value) {
+    // Logique de mise à jour des passagers validés
+  }
+}
+
+const formatTime = (time: string) => {
+  return time
+}
+
+// Lifecycle
+onMounted(() => {
+  // Charger les données depuis l'API
+  loadEmployeeData()
+})
+
+const loadEmployeeData = async () => {
+  try {
+    // Appels API pour charger les trajets assignés, statistiques, etc.
+    console.log('Chargement des données employé/conducteur...')
+  } catch (error) {
+    console.error('Erreur lors du chargement des données:', error)
+  }
+}
+</script>
+
+<style scoped>
+/* Styles responsive pour mobile */
+@media (max-width: 640px) {
+  .max-w-7xl {
+    padding-left: 1rem;
+    padding-right: 1rem;
+  }
+  
+  .grid-cols-1 {
+    grid-template-columns: repeat(1, minmax(0, 1fr));
+  }
+  
+  .space-x-8 {
+    gap: 1rem;
+  }
+  
+  .text-2xl {
+    font-size: 1.5rem;
+  }
+}
+
+/* Styles pour tablettes */
+@media (min-width: 641px) and (max-width: 1024px) {
+  .grid-cols-1.md\:grid-cols-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+/* Animation pour les cartes */
+.shadow {
+  transition: box-shadow 0.3s ease;
+}
+
+.hover\:shadow-md:hover {
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+/* Styles pour le modal */
+.fixed {
+  position: fixed;
+}
+
+.inset-0 {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.z-50 {
+  z-index: 50;
+}
+</style>

+ 8 - 1
app_transport_front/src/views/LoginView.vue

@@ -132,7 +132,14 @@ const redirectionUser = async () => {
       return
     }
     
-    // Pour les autres rôles (Driver, Employee), rediriger vers le dashboard utilisateur par défaut
+    // Pour les employés et conducteurs, rediriger vers leur dashboard spécialisé
+    if (userRole === Role.Employee || userRole === Role.Driver) {
+      console.log('Redirection employé/conducteur vers /employee/dashboard')
+      router.push('/employee/dashboard')
+      return
+    }
+    
+    // Pour les autres rôles, rediriger vers le dashboard utilisateur par défaut
     router.push('/user/dashboard')
   }
 }