Explorar o código

(feat) ticket et qrcode

Brin AMONKONAN hai 1 ano
pai
achega
a966168b6c
Modificáronse 67 ficheiros con 6064 adicións e 1088 borrados
  1. 155 0
      GUIDE_TEST_CINETPAY.md
  2. 8 1
      app_transport_front/.env
  3. 320 14
      app_transport_front/package-lock.json
  4. 1 0
      app_transport_front/package.json
  5. BIN=BIN
      app_transport_front/src/assets/images/logo-2-transparent.jpeg
  6. BIN=BIN
      app_transport_front/src/assets/images/logo-2.jpeg
  7. 334 91
      app_transport_front/src/components/ReservationWorkflowModal.vue
  8. 552 52
      app_transport_front/src/components/TripSearchForm.vue
  9. 463 0
      app_transport_front/src/components/user/ReceiptViewModal.vue
  10. 377 0
      app_transport_front/src/components/user/ReservationDetailsModal.vue
  11. 338 0
      app_transport_front/src/components/user/ReservationHistory.vue
  12. 482 0
      app_transport_front/src/components/user/ReservationReceipts.vue
  13. 485 0
      app_transport_front/src/components/user/TravelTickets.vue
  14. 25 2
      app_transport_front/src/router/index.ts
  15. 148 0
      app_transport_front/src/services/CinetPayService.ts
  16. 19 5
      app_transport_front/src/services/ReservationService.ts
  17. 100 0
      app_transport_front/src/types/cinetpay.ts
  18. 5 12
      app_transport_front/src/views/HomeView.vue
  19. 48 25
      app_transport_front/src/views/LoginView.vue
  20. 143 0
      app_transport_front/src/views/PaymentSuccessView.vue
  21. 12 1
      app_transport_front/src/views/ReservationView.vue
  22. 352 0
      app_transport_front/src/views/UserDashboardView.vue
  23. 9 8
      node_app/Dockerfile
  24. 221 666
      node_app/package-lock.json
  25. 9 4
      node_app/package.json
  26. 8 0
      node_app/prisma/migrations/20250831071439_add_employee_to_trajet/migration.sql
  27. 38 0
      node_app/prisma/migrations/20250831103923_add_cinetpay_fields/migration.sql
  28. 11 0
      node_app/prisma/migrations/20250831104223_add_cinetpay_fields/migration.sql
  29. 17 0
      node_app/prisma/schema.prisma
  30. 6 2
      node_app/src/app.module.ts
  31. 9 0
      node_app/src/application/dto/create-trajet.dto.ts
  32. 61 0
      node_app/src/application/dto/responses/reservation-details-response.dto.ts
  33. 10 0
      node_app/src/application/dto/responses/trajet-response.dto.ts
  34. 31 0
      node_app/src/application/handlers/reservation-created.handler.ts
  35. 7 1
      node_app/src/application/use-cases/create-user.use-case.ts
  36. 43 0
      node_app/src/application/use-cases/qrcode/find-qrcode.use-case.ts
  37. 56 0
      node_app/src/application/use-cases/qrcode/generate-qrcode.use-case.ts
  38. 52 0
      node_app/src/application/use-cases/qrcode/scan-qrcode.use-case.ts
  39. 12 1
      node_app/src/application/use-cases/reservation/create-reservation.use-case.ts
  40. 14 0
      node_app/src/application/use-cases/reservation/find-reservations-by-user.use-case.ts
  41. 31 0
      node_app/src/application/use-cases/trajet/search-trajets.use-case.ts
  42. 1 1
      node_app/src/config/auth.config.ts
  43. 39 0
      node_app/src/domain/entities/qrcode.entity.ts
  44. 4 0
      node_app/src/domain/entities/trajet.entity.ts
  45. 6 0
      node_app/src/domain/events/reservation-created.event.ts
  46. 33 0
      node_app/src/domain/repositories/qrcode.repository.interface.ts
  47. 10 0
      node_app/src/domain/repositories/reservation.repository.interface.ts
  48. 88 3
      node_app/src/infrastructure/database/repositories/reservation.repository.ts
  49. 196 0
      node_app/src/infrastructure/database/repositories/trajet.repository.ts
  50. 6 2
      node_app/src/infrastructure/database/repositories/transaction.repository.ts
  51. 2 0
      node_app/src/infrastructure/mappers/place.mapper.ts
  52. 83 6
      node_app/src/infrastructure/mappers/reservation.mapper.ts
  53. 38 1
      node_app/src/infrastructure/mappers/trajet.mapper.ts
  54. 10 0
      node_app/src/infrastructure/mappers/transaction.mapper.ts
  55. 2 0
      node_app/src/infrastructure/mappers/user.mapper.ts
  56. 139 0
      node_app/src/infrastructure/repositories/qrcode.repository.ts
  57. 3 3
      node_app/src/infrastructure/services/hasher.service.ts
  58. 28 0
      node_app/src/infrastructure/services/paydunya.service.ts
  59. 71 11
      node_app/src/presentation/controllers/payment.controller.ts
  60. 48 162
      node_app/src/presentation/controllers/qrcode.controller.ts
  61. 115 6
      node_app/src/presentation/controllers/reservation.controller.ts
  62. 21 0
      node_app/src/presentation/controllers/trajet.controller.ts
  63. 38 0
      node_app/src/presentation/modules/qrcode.module.ts
  64. 23 2
      node_app/src/presentation/modules/reservation.module.ts
  65. 40 0
      node_app/src/presentation/modules/shared.module.ts
  66. 6 5
      node_app/test/user.service.spec.ts
  67. 2 1
      node_app/tsconfig.json

+ 155 - 0
GUIDE_TEST_CINETPAY.md

@@ -0,0 +1,155 @@
+# Guide de Test - Intégration CinetPay
+
+## 📋 Prérequis
+
+Avant de tester l'intégration CinetPay, vous devez :
+
+1. **Obtenir vos identifiants CinetPay** :
+   - Créer un compte sur [CinetPay](https://cinetpay.com)
+   - Récupérer votre `APIKEY` et `SITE_ID` depuis votre tableau de bord
+   - Utiliser les identifiants de test pour l'environnement de développement
+
+2. **Configurer les variables d'environnement** :
+   ```bash
+   # Dans app_transport_front/.env
+   VITE_CINETPAY_APIKEY=votre_apikey_de_test
+   VITE_CINETPAY_SITE_ID=votre_site_id_de_test
+   ```
+
+## 🚀 Architecture Implémentée
+
+### Frontend
+- **Service CinetPay** : `src/services/CinetPayService.ts`
+- **Types TypeScript** : `src/types/cinetpay.ts`
+- **Composant de réservation** : `src/components/ReservationWorkflowModal.vue`
+- **Page de succès** : `src/views/PaymentSuccessView.vue`
+
+### Backend
+- **Webhook CinetPay** : `src/presentation/controllers/payment.controller.ts`
+- **Endpoint** : `POST /api/payments/cinetpay/webhook`
+
+## 🧪 Comment Tester
+
+### 1. Test en Mode Développement (Simulation)
+
+1. **Démarrer les serveurs** :
+   ```bash
+   # Frontend
+   cd app_transport_front
+   npm run dev
+   
+   # Backend
+   cd node_app
+   npm run start:dev
+   ```
+
+2. **Accéder à l'application** :
+   - Ouvrir http://localhost:5173
+   - Naviguer vers une page de réservation
+   - Remplir le formulaire de réservation
+
+3. **Processus de test** :
+   - Le système collecte les données sans créer d'entités
+   - Au moment du paiement, CinetPay génère une URL de paiement
+   - En mode DEV, le paiement est simulé automatiquement
+   - Les entités (utilisateur + réservations) sont créées après "paiement" réussi
+
+### 2. Test avec CinetPay Réel
+
+1. **Configurer les vraies clés** :
+   ```bash
+   VITE_CINETPAY_APIKEY=votre_vraie_apikey
+   VITE_CINETPAY_SITE_ID=votre_vrai_site_id
+   ```
+
+2. **Modifier le mode de test** :
+   Dans `ReservationWorkflowModal.vue`, commenter la simulation :
+   ```typescript
+   // En production, rediriger vers l'URL de paiement CinetPay
+   window.location.href = result.data.payment_url
+   ```
+
+3. **Tester le workflow complet** :
+   - Remplir le formulaire de réservation
+   - Cliquer sur "Payer"
+   - Être redirigé vers CinetPay
+   - Effectuer le paiement test
+   - Être redirigé vers la page de succès
+
+## 🔧 Configuration CinetPay selon la Documentation
+
+### Paramètres Obligatoires <mcreference link="https://docs.cinetpay.com/api/1.0-fr/checkout/initialisation" index="0">0</mcreference>
+
+- `apikey` : Votre clé API CinetPay
+- `site_id` : Votre identifiant de site
+- `transaction_id` : ID unique de transaction (généré automatiquement)
+- `amount` : Montant (multiple de 5)
+- `currency` : Devise (XOF, XAF, CDF, GNF, USD)
+- `description` : Description du paiement
+- `notify_url` : URL de notification webhook
+- `return_url` : URL de retour après paiement
+- `channels` : Canaux de paiement (ALL, MOBILE_MONEY, CREDIT_CARD, WALLET)
+
+### Données Client (Obligatoires pour Cartes Bancaires) <mcreference link="https://docs.cinetpay.com/api/1.0-fr/checkout/initialisation" index="0">0</mcreference>
+
+- `customer_name` : Nom du client
+- `customer_surname` : Prénom du client
+- `customer_email` : Email du client
+- `customer_phone_number` : Téléphone du client
+- `customer_address` : Adresse du client
+- `customer_city` : Ville du client
+- `customer_country` : Code pays (ex: CI, TG, SN)
+- `customer_state` : État/région
+- `customer_zip_code` : Code postal
+
+## 📱 Canaux de Paiement Supportés
+
+- **ALL** : Tous les canaux disponibles
+- **MOBILE_MONEY** : Paiement mobile (Orange Money, MTN Money, etc.)
+- **CREDIT_CARD** : Cartes bancaires
+- **WALLET** : Portefeuilles électroniques
+
+## 🔄 Workflow de Paiement Optimisé
+
+1. **Collecte des données** : Formulaire multi-étapes sans création d'entités
+2. **Génération du paiement** : Appel API CinetPay avec toutes les données
+3. **Redirection** : Vers l'interface de paiement CinetPay
+4. **Notification webhook** : CinetPay notifie le succès/échec
+5. **Création des entités** : Seulement après confirmation de paiement
+6. **Redirection de retour** : Vers la page de succès
+
+## 🚨 Points d'Attention
+
+1. **Sécurité** :
+   - Ne jamais exposer les clés API dans le code frontend
+   - Valider les webhooks côté serveur
+   - Vérifier les signatures des notifications
+
+2. **Gestion d'erreurs** :
+   - Gérer les échecs de paiement
+   - Prévoir les timeouts
+   - Logger toutes les transactions
+
+3. **Tests** :
+   - Utiliser les identifiants de test CinetPay
+   - Tester tous les canaux de paiement
+   - Vérifier les webhooks en local avec ngrok si nécessaire
+
+## 📞 Support
+
+- **Documentation CinetPay** : https://docs.cinetpay.com
+- **Support technique** : support@cinetpay.com
+- **Environnement de test** : Utiliser les clés de test fournies par CinetPay
+
+## ✅ Checklist de Test
+
+- [ ] Variables d'environnement configurées
+- [ ] Serveurs frontend et backend démarrés
+- [ ] Formulaire de réservation fonctionnel
+- [ ] Génération d'URL de paiement CinetPay
+- [ ] Simulation de paiement en mode DEV
+- [ ] Création des entités après paiement
+- [ ] Page de succès accessible
+- [ ] Webhook backend fonctionnel
+- [ ] Gestion des erreurs de paiement
+- [ ] Test avec vrais identifiants CinetPay (optionnel)

+ 8 - 1
app_transport_front/.env

@@ -1 +1,8 @@
-VITE_API_BASE_URL=http://localhost:3001/api
+VITE_API_BASE_URL=http://localhost:3001/api
+
+# Configuration CinetPay pour les tests
+VITE_CINETPAY_APIKEY=174173934865f8799156f094.97916229
+VITE_CINETPAY_SITE_ID=105897483
+VITE_CINETPAY_API_URL=https://api-checkout.cinetpay.com/v2/payment
+VITE_CINETPAY_NOTIFY_URL=http://localhost:3001/api/cinetpay/webhook
+VITE_CINETPAY_RETURN_URL=http://localhost:5173/payment/success

+ 320 - 14
app_transport_front/package-lock.json

@@ -16,6 +16,7 @@
         "axios": "^1.6.7",
         "bootstrap": "^5.3.6",
         "pinia": "^2.3.1",
+        "qrcode": "^1.5.4",
         "toastify-js": "^1.12.0",
         "v-click-outside": "^3.2.0",
         "vue": "^3.5.13",
@@ -739,17 +740,6 @@
         "node": ">=14"
       }
     },
-    "node_modules/@popperjs/core": {
-      "version": "2.11.8",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
-      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
-      "license": "MIT",
-      "peer": true,
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
       "version": "4.40.2",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@@ -1536,6 +1526,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/camelcase-css": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -1615,11 +1614,91 @@
         "consola": "^3.2.3"
       }
     },
+    "node_modules/cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "color-name": "~1.1.4"
@@ -1632,7 +1711,6 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/combined-stream": {
@@ -1708,6 +1786,15 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "license": "MIT"
     },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/deep-pick-omit": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz",
@@ -1745,6 +1832,12 @@
       "dev": true,
       "license": "Apache-2.0"
     },
+    "node_modules/dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+      "license": "MIT"
+    },
     "node_modules/dlv": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -1992,6 +2085,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/follow-redirects": {
       "version": "1.15.9",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -2082,6 +2188,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2275,7 +2390,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -2437,6 +2551,18 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/lottie-web": {
       "version": "5.12.2",
       "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz",
@@ -2682,6 +2808,42 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/package-json-from-dist": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -2695,6 +2857,15 @@
       "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
       "license": "(MIT AND Zlib)"
     },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/path-key": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -2841,6 +3012,15 @@
         "pathe": "^2.0.3"
       }
     },
+    "node_modules/pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.5.3",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@@ -3012,6 +3192,23 @@
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "license": "MIT",
+      "dependencies": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      },
+      "bin": {
+        "qrcode": "bin/qrcode"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/quansync": {
       "version": "0.2.10",
       "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
@@ -3084,6 +3281,21 @@
         "node": ">=8.10.0"
       }
     },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "license": "ISC"
+    },
     "node_modules/resize-observer-polyfill": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -3206,6 +3418,12 @@
         "node": ">=10"
       }
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+      "license": "ISC"
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3915,6 +4133,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+      "license": "ISC"
+    },
     "node_modules/wrap-ansi": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -4013,6 +4237,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+      "license": "ISC"
+    },
     "node_modules/yaml": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
@@ -4025,6 +4255,82 @@
       "engines": {
         "node": ">= 14"
       }
+    },
+    "node_modules/yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/yargs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
     }
   }
 }

+ 1 - 0
app_transport_front/package.json

@@ -16,6 +16,7 @@
     "axios": "^1.6.7",
     "bootstrap": "^5.3.6",
     "pinia": "^2.3.1",
+    "qrcode": "^1.5.4",
     "toastify-js": "^1.12.0",
     "v-click-outside": "^3.2.0",
     "vue": "^3.5.13",

BIN=BIN
app_transport_front/src/assets/images/logo-2-transparent.jpeg


BIN=BIN
app_transport_front/src/assets/images/logo-2.jpeg


+ 334 - 91
app_transport_front/src/components/ReservationWorkflowModal.vue

@@ -92,11 +92,15 @@
         <div v-if="currentStep === 3" class="space-y-4">
           <h3 class="text-lg font-semibold mb-4">Paiement</h3>
           
+          <!-- Sélection de la méthode de paiement -->
+
+
+          
           <div class="bg-gray-50 p-4 rounded-md">
             <h4 class="font-medium mb-2">Récapitulatif du paiement :</h4>
             <p><strong>Réservation ID :</strong> {{ createdReservationId }}</p>
             <p><strong>Montant :</strong> {{ formatPrice(totalAmount) }} FCFA</p>
-            <p><strong>Méthode :</strong> Mobile Money</p>
+            <p><strong>Méthode :</strong> {{ getPaymentMethodText(props.paymentMethod) }}</p>
           </div>
 
           <div v-if="step3Error" class="text-red-600 text-sm">
@@ -156,8 +160,11 @@
 
 <script setup lang="ts">
 import { ref, computed, watch } from 'vue'
+import { useRouter } from 'vue-router'
 import UserService from '@/services/UserService'
 import ReservationService from '@/services/ReservationService'
+import CinetPayService from '@/services/CinetPayService'
+import { CinetPayCurrency, CinetPayChannels, CinetPayLanguage } from '@/types/cinetpay'
 import TransactionService from '@/services/TransactionService'
 import type { User, Reservation } from '@/types'
 import { Role } from '@/Enum/Role'
@@ -176,6 +183,7 @@ interface Props {
   totalAmount: number
   tripId: number
   tripDetails: string
+  paymentMethod: string
 }
 
 interface Emits {
@@ -185,15 +193,24 @@ interface Emits {
 
 const props = defineProps<Props>()
 const emit = defineEmits<Emits>()
+const router = useRouter()
 
 // State
 const currentStep = ref(1)
 const isLoading = ref(false)
 const loadingMessage = ref('')
 const selectedMainPassenger = ref(0)
+const paymentSuccess = ref(false)
+// La méthode de paiement est maintenant reçue en prop
+// const selectedPaymentMethod = ref('mobile_money')
+
+// Collected data (not persisted until payment success)
+const collectedUserData = ref<any>(null)
+const collectedReservationData = ref<any[]>([])
+
+// Final created entities (after payment)
 const createdUserId = ref<number | null>(null)
 const createdReservationId = ref<number | null>(null)
-const paymentSuccess = ref(false)
 
 // Error states
 const step1Error = ref('')
@@ -225,10 +242,10 @@ const canProceed = computed(() => {
     return mainPassenger.value && mainPassenger.value.name && mainPassenger.value.otherName && mainPassenger.value.phone
   }
   if (currentStep.value === 2) {
-    return createdUserId.value !== null
+    return props.selectedSeats.length > 0 && props.passengers.length > 0
   }
   if (currentStep.value === 3) {
-    return createdReservationId.value !== null
+    return collectedUserData.value !== null && collectedReservationData.value.length > 0 && props.paymentMethod
   }
   return false
 })
@@ -253,9 +270,17 @@ const closeModal = () => {
   currentStep.value = 1
   isLoading.value = false
   selectedMainPassenger.value = 0
+  
+  // Reset collected data
+  collectedUserData.value = null
+  collectedReservationData.value = []
+  
+  // Reset created entities
   createdUserId.value = null
   createdReservationId.value = null
   paymentSuccess.value = false
+  
+  // Reset errors
   step1Error.value = ''
   step2Error.value = ''
   step3Error.value = ''
@@ -273,17 +298,17 @@ const nextStep = async () => {
   if (!canProceed.value) return
   
   if (currentStep.value === 1) {
-    await createUser()
+    prepareUserData()
+    currentStep.value = 2
   } else if (currentStep.value === 2) {
-    await createReservation()
+    prepareReservationData()
+    currentStep.value = 3
   } else if (currentStep.value === 3) {
     await processPayment()
   }
 }
 
-const createUser = async () => {
-  isLoading.value = true
-  loadingMessage.value = 'Création du compte utilisateur...'
+const prepareUserData = () => {
   step1Error.value = ''
   
   try {
@@ -296,124 +321,334 @@ const createUser = async () => {
     }
 
     delete userData.role
-    const createdUser = await UserService.createUser(userData)
-    
-    if (createdUser && createdUser.id) {
-      createdUserId.value = createdUser.id
-      currentStep.value = 2
-    } else {
-      step1Error.value = 'Erreur lors de la création du compte utilisateur'
-    }
+    collectedUserData.value = userData
   } catch (error) {
-    console.error('Erreur création utilisateur:', error)
-    step1Error.value = 'Erreur lors de la création du compte utilisateur'
-  } finally {
-    isLoading.value = false
+    console.error('Erreur préparation données utilisateur:', error)
+    step1Error.value = 'Erreur lors de la préparation des données utilisateur'
   }
 }
 
-const createReservation = async () => {
-  isLoading.value = true
-  loadingMessage.value = 'Création de la réservation...'
+const prepareReservationData = () => {
   step2Error.value = ''
   
   try {
-    // Créer une réservation pour chaque passager/place
+    // Préparer les données de réservation pour chaque passager/place
     const reservations = []
     
     for (let i = 0; i < props.passengers.length; i++) {
       const reservationData: Omit<Reservation, 'id'> = {
         dateAt: new Date().toISOString(),
         placeId: props.selectedSeats[i],
-        userId: createdUserId.value!,
+        userId: 0, // Sera remplacé par l'ID utilisateur créé après paiement
         trajetId: props.tripId
       }
       
-      const reservation = await ReservationService.createReservation(reservationData)
-      console.log('Réservations créées 1:', reservations)
-      if (reservation) {
-        reservations.push(reservation)
-      }
-    }
-    console.log('Réservations créées:', reservations) // Ajout de cette ligne pour débogage
-    if (reservations.length > 0) {
-      // Utiliser la première réservation comme référence principale
-      console.log('Je suis dedans', reservations) 
-      createdReservationId.value = reservations[0].data.id
-      currentStep.value = 3
-    } else {
-      step2Error.value = 'Erreur lors de la création de la réservation'
+      reservations.push(reservationData)
     }
+    
+    collectedReservationData.value = reservations
   } catch (error) {
-    console.error('Erreur création réservation:', error)
-    step2Error.value = 'Erreur lors de la création de la réservation'
-  } finally {
-    isLoading.value = false
+    console.error('Erreur préparation données réservation:', error)
+    step2Error.value = 'Erreur lors de la préparation des données de réservation'
   }
 }
 
 const processPayment = async () => {
+  if (!collectedUserData.value || !collectedReservationData.value.length) {
+    step3Error.value = 'Données utilisateur ou réservation manquantes'
+    return
+  }
+
+  if (!props.paymentMethod) {
+    step3Error.value = 'Veuillez sélectionner une méthode de paiement'
+    return
+  }
+
   isLoading.value = true
-  loadingMessage.value = 'Traitement du paiement...'
+  loadingMessage.value = 'Initialisation du paiement...'
   step3Error.value = ''
-  
+
   try {
-    // Vérifier que nous avons un ID de réservation valide
-    if (!createdReservationId.value || typeof createdReservationId.value !== 'number') {
-      throw new Error('ID de réservation manquant ou invalide')
+    // Traitement selon la méthode de paiement sélectionnée
+    if (props.paymentMethod === 'mobile') {
+      // Utiliser CinetPay uniquement pour Mobile Money
+      await processMobileMoneyPayment()
+    } else if (props.paymentMethod === 'card') {
+      // Traitement pour carte bancaire (sans CinetPay)
+      await processCardPayment()
+    } else if (props.paymentMethod === 'cash') {
+      // Traitement pour paiement en espèces
+      await processCashPayment()
+    } else {
+      throw new Error('Méthode de paiement non supportée')
     }
+  } catch (error) {
+    console.error('Erreur paiement:', error)
     
-    // Données pour CinetPay selon la documentation
-    const cinetPayData = {
-      apikey: "174173934865f8799156f094.97916229", // À remplacer par votre vraie clé API
-      site_id: "105897483", // À remplacer par votre site ID
-      transaction_id: `TRANSPORT_${createdReservationId.value}_${Date.now()}`, // ID unique de transaction
-      amount: props.totalAmount,
-      currency: "XOF",
-      description: `Réservation transport - ${props.passengers.length} place(s)`,
-      customer_id: createdUserId.value?.toString() || "guest",
-      customer_name: mainPassenger.value.name || "",
-      customer_surname: mainPassenger.value.otherName || "",
-      customer_email: mainPassenger.value.email || generatedEmail.value,
-      customer_phone_number: mainPassenger.value.phone,
-      customer_address: "Côte d'Ivoire",
-      customer_city: "Abidjan",
-      customer_country: "CI",
-      customer_state: "CI",
-      customer_zip_code: "00225",
-      notify_url: `${window.location.origin}/api/payments/cinetpay/notify`,
-      return_url: `${window.location.origin}/payment/success?reservation=${createdReservationId.value}`,
-      channels: "MOBILE_MONEY", // Uniquement mobile money
-      metadata: JSON.stringify({ reservationId: createdReservationId.value }),
-      lang: "FR"
+    if (error instanceof Error) {
+      step3Error.value = `❌ ${error.message}`
+    } else {
+      step3Error.value = '❌ Erreur lors du traitement du paiement'
     }
+  } finally {
+    isLoading.value = false
+  }
+}
+
+// Traitement spécifique pour Mobile Money avec CinetPay
+const processMobileMoneyPayment = async () => {
+  // Préparer les données pour CinetPay selon la documentation officielle
+  const paymentData = {
+    amount: props.totalAmount,
+    currency: CinetPayCurrency.EUR,
+    description: `Réservation transport - ${props.passengers.length} place(s)`,
+    customerData: {
+      id: generatedEmail.value,
+      name: mainPassenger.value.name || "",
+      surname: mainPassenger.value.otherName || "",
+      email: generatedEmail.value,
+      phone: mainPassenger.value.phone,
+      address: "Côte d'Ivoire",
+      city: "Abidjan",
+      country: "CI",
+      state: "CI",
+      zipCode: "00225"
+    },
+    metadata: {
+      userData: collectedUserData.value,
+      reservationData: collectedReservationData.value,
+      tripId: props.tripId
+    },
+    channels: CinetPayChannels.MOBILE_MONEY,
+    lang: CinetPayLanguage.FR
+  }
+  
+  loadingMessage.value = 'Génération du lien de paiement Mobile Money...'
+  
+  // Utiliser le service CinetPay pour initialiser le paiement
+  const result = await CinetPayService.initializePayment(paymentData)
     
-    // Appel à l'API CinetPay pour générer l'URL de paiement
-    const paymentResult = await fetch('https://api-checkout.cinetpay.com/v2/payment', {
-      method: 'POST',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify(cinetPayData)
-    })
+    // Vérifier la réponse de CinetPay
+    if (result.code === '201' && result.data && result.data.payment_url) {
+      loadingMessage.value = 'Redirection vers le paiement...'
+      
+      // Sauvegarder les données CinetPay pour référence future
+      const cinetPayResponseData = {
+        transaction_id: result.data.transaction_id,
+        payment_token: result.data.payment_token,
+        payment_url: result.data.payment_url,
+        amount: result.data.amount,
+        currency: result.data.currency,
+        description: result.data.description,
+        created_at: result.data.created_at,
+        api_response_code: result.code,
+        api_response_message: result.message
+      }
+      
+      // En mode test, simuler un paiement réussi
+      // En production, rediriger vers result.data.payment_url
+      if (import.meta.env.DEV) {
+        console.log('Mode développement - Simulation du paiement')
+        console.log('URL de paiement générée:', result.data.payment_url)
+        console.log('Données CinetPay à sauvegarder:', cinetPayResponseData)
+        await simulatePaymentSuccess(cinetPayResponseData)
+      } else {
+        // En production, sauvegarder les données CinetPay dans le localStorage pour les récupérer au retour
+        localStorage.setItem('cinetpay_pending_data', JSON.stringify({
+          cinetPayResponse: cinetPayResponseData,
+          userData: collectedUserData.value,
+          reservationData: collectedReservationData.value,
+          totalAmount: props.totalAmount,
+          tripId: props.tripId
+        }))
+        
+        // Rediriger vers l'URL de paiement CinetPay
+        window.location.href = result.data.payment_url
+      }
+    } else {
+      // Erreur spécifique du service de paiement CinetPay
+      const errorMessage = result.message || 'Service de paiement indisponible'
+      throw new Error(`Erreur du service de paiement: ${errorMessage}`)
+    }
+}
+
+// Traitement pour carte bancaire (sans CinetPay)
+const processCardPayment = async () => {
+  loadingMessage.value = 'Traitement du paiement par carte...'
+  
+  // Simulation du paiement par carte bancaire
+  // En production, intégrer avec un autre service de paiement pour les cartes
+  await new Promise(resolve => setTimeout(resolve, 2000))
+  
+  // Simuler un paiement réussi
+  const cardPaymentData = {
+    transaction_id: `CARD_${Date.now()}`,
+    payment_method: 'CARD',
+    amount: props.totalAmount,
+    currency: 'XOF',
+    status: 'SUCCESS'
+  }
+  
+  await simulatePaymentSuccess(cardPaymentData)
+}
+
+// Traitement pour paiement en espèces
+const processCashPayment = async () => {
+  loadingMessage.value = 'Confirmation de la réservation...'
+  
+  // Pour le paiement en espèces, créer directement la réservation
+  // Le paiement sera effectué à bord du véhicule
+  await new Promise(resolve => setTimeout(resolve, 1000))
+  
+  const cashPaymentData = {
+    transaction_id: `CASH_${Date.now()}`,
+    payment_method: 'CASH',
+    amount: props.totalAmount,
+    currency: 'XOF',
+    status: 'PENDING' // Le paiement sera effectué plus tard
+  }
+  
+  await simulatePaymentSuccess(cashPaymentData)
+}
+
+// Fonction pour simuler un paiement réussi et créer toutes les entités
+const simulatePaymentSuccess = async (cinetPayResponseData = null) => {
+  console.log('je suis ici')
+  try {
+    loadingMessage.value = 'Création du compte utilisateur...'
+    
+    // 1. Créer l'utilisateur
+    const createdUser = await UserService.createUser(collectedUserData.value)
     
-    if (!paymentResult.ok) {
-      throw new Error(`Erreur HTTP CinetPay: ${paymentResult.status}`)
+    if (!createdUser || !createdUser.id) {
+      throw new Error('Erreur lors de la création du compte utilisateur')
     }
     
-    const result = await paymentResult.json()
+    createdUserId.value = createdUser.id
+    loadingMessage.value = 'Création des réservations...'
     
-    // Vérifier la réponse de CinetPay
-    if (result.code === '201' && result.data && result.data.payment_url) {
-      // Rediriger vers l'URL de paiement CinetPay
-      window.location.href = result.data.payment_url
-    } else {
-      throw new Error(result.message || 'Erreur lors de la génération du lien de paiement')
+    // 2. Créer les réservations avec l'ID utilisateur
+    const reservations = []
+    
+    for (const reservationData of collectedReservationData.value) {
+      const updatedReservationData = {
+        ...reservationData,
+        userId: createdUser.id
+      }
+      
+      const reservation = await ReservationService.createReservation(updatedReservationData)
+       if (reservation && reservation.data && reservation.data.id) {
+         reservations.push(reservation.data)
+       }
+     }
+     
+     if (reservations.length === 0) {
+       throw new Error('Erreur lors de la création des réservations')
+     }
+     
+     // Utiliser la première réservation comme référence principale
+     createdReservationId.value = reservations[0].id
+    
+    // 3. Créer la transaction avec les données CinetPay simulées
+    loadingMessage.value = 'Enregistrement de la transaction...'
+    
+    // Utiliser les vraies données de paiement ou simuler une réponse
+    const paymentResponse = cinetPayResponseData ? {
+      code: cinetPayResponseData.api_response_code || '201',
+      message: cinetPayResponseData.api_response_message || 'Payment successful',
+      data: {
+        transaction_id: cinetPayResponseData.transaction_id,
+        payment_token: cinetPayResponseData.payment_token,
+        payment_url: cinetPayResponseData.payment_url,
+        payment_method: cinetPayResponseData.payment_method || props.paymentMethod.toUpperCase(),
+        operator_id: props.paymentMethod === 'mobile' ? 'ORANGE_CI' : null, // Seulement pour mobile money
+        phone_number: collectedUserData.value?.phone || '',
+        amount: cinetPayResponseData.amount || props.totalAmount,
+        currency: cinetPayResponseData.currency || 'EUR',
+        status: 'COMPLETED', // En simulation, toujours réussi
+        created_at: cinetPayResponseData.created_at
+      }
+    } : {
+      // Données simulées selon la méthode de paiement sélectionnée
+      code: '201',
+      message: 'Payment successful',
+      data: {
+        transaction_id: `${props.paymentMethod.toUpperCase()}_${Date.now()}`,
+        payment_token: props.paymentMethod === 'mobile' ? `TOKEN_${Math.random().toString(36).substr(2, 9)}` : null,
+         payment_url: props.paymentMethod === 'mobile' ? 'https://checkout.cinetpay.com/payment/mock' : null,
+        payment_method: props.paymentMethod.toUpperCase(),
+        operator_id: props.paymentMethod === 'mobile' ? 'ORANGE_CI' : null,
+        phone_number: collectedUserData.value?.phone || '',
+        amount: props.totalAmount,
+        currency: 'XOF',
+        status: props.paymentMethod === 'cash' ? 'PENDING' : 'COMPLETED'
+      }
+    }
+    
+    // Créer la transaction pour chaque réservation
+    for (const reservation of reservations) {
+      const transactionData = {
+        amount: props.totalAmount / reservations.length, // Répartir le montant
+        currency: 'XOF',
+        status: paymentResponse.data.status,
+        provider: props.paymentMethod === 'mobile' ? 'CinetPay' : getPaymentMethodText(props.paymentMethod),
+        reference: paymentResponse.data.transaction_id,
+        metadata: {
+          payment_response: paymentResponse,
+          payment_method: paymentResponse.data.payment_method,
+          operator_id: paymentResponse.data.operator_id,
+          phone_number: paymentResponse.data.phone_number,
+          reservation_details: {
+            trajet: `${reservation.trajet?.departStation?.name || ''} → ${reservation.trajet?.arriveStation?.name || ''}`,
+            departure_time: reservation.trajet?.departAt,
+            place_number: reservation.place?.number
+          }
+        },
+        userId: createdUser.id,
+        reservationId: reservation.id
+      }
+      
+      const transaction = await TransactionService.createTransaction(transactionData)
+      if (transaction) {
+        console.log('Transaction créée:', transaction.id, 'pour la réservation:', reservation.id)
+      }
     }
     
+    // 4. Marquer le paiement comme réussi
+    paymentSuccess.value = true
+    loadingMessage.value = 'Réservation confirmée !'
+    
+    // Émettre l'événement de succès
+    emit('success', createdReservationId.value)
+    
+    // Rediriger vers l'espace utilisateur après un délai
+    setTimeout(() => {
+      router.push('/user/dashboard')
+    }, 2000)
+    
   } catch (error) {
-    console.error('Erreur paiement CinetPay:', error)
-    step3Error.value = error instanceof Error ? error.message : 'Erreur lors du traitement du paiement'
-  } finally {
-    isLoading.value = false
+    console.error('Erreur lors de la création des entités:', error)
+    step3Error.value = error instanceof Error ? error.message : 'Erreur lors de la finalisation de la réservation'
+    
+    // En cas d'erreur, nettoyer les entités partiellement créées si nécessaire
+    if (createdUserId.value) {
+      // Ici on pourrait ajouter une logique de nettoyage si nécessaire
+      console.warn('Utilisateur créé mais réservation échouée, ID:', createdUserId.value)
+    }
+  }
+}
+
+// Fonction pour obtenir le texte de la méthode de paiement
+const getPaymentMethodText = (method: string) => {
+  switch (method) {
+    case 'mobile_money':
+      return 'Mobile Money'
+    case 'card':
+      return 'Carte bancaire'
+    case 'cash':
+      return 'Espèces'
+    default:
+      return 'Non défini'
   }
 }
 
@@ -422,9 +657,17 @@ watch(() => props.isOpen, (newValue) => {
   if (newValue) {
     currentStep.value = 1
     selectedMainPassenger.value = 0
+    
+    // Reset collected data
+    collectedUserData.value = null
+    collectedReservationData.value = []
+    
+    // Reset created entities
     createdUserId.value = null
     createdReservationId.value = null
     paymentSuccess.value = false
+    
+    // Reset errors
     step1Error.value = ''
     step2Error.value = ''
     step3Error.value = ''

+ 552 - 52
app_transport_front/src/components/TripSearchForm.vue

@@ -1,59 +1,175 @@
 <template>
-  <div class="trip-search-form-container w-full">
-    <h3>Trouvez votre prochain voyage</h3>
-    <form @submit.prevent="handleSearch" class="search-form">
-      <div class="form-row">
-        <div class="form-group">
-          <label for="departure">Station de départ</label>
-          <select id="departure" class="flex w-full" v-model="searchCriteria.departure" required>
-            <option value="" disabled>Sélectionner une station</option>
-            <option v-for="station in cities" :key="station.id" :value="station.name">
-              {{ station.name }}
-            </option>
-          </select>
-        </div>
-        <div class="form-group">
-          <label for="arrival">Station d'arrivée</label>
-          <select id="arrival" class="flex w-full" v-model="searchCriteria.arrival" required>
-            <option value="" disabled>Sélectionner une station</option>
-            <option v-for="station in cities" :key="station.id" :value="station.name">
-              {{ station.name }}
-            </option>
-          </select>
-        </div>
+  <div class="trip-search-container w-full">
+    <!-- Layout principal avec formulaire et résultats -->
+    <div class="main-layout">
+      <!-- Formulaire de recherche -->
+      <div class="trip-search-form-container bg-red-100">
+        <h3>Trouvez votre prochain voyage</h3>
+        <form @submit.prevent="handleSearch" class="search-form">
+          <div class="form-row">
+            <div class="form-group">
+              <label for="departure">Station de départ</label>
+              <select id="departure" class="flex w-full" v-model="searchCriteria.departure" required>
+                <option value="" disabled>Sélectionner une station</option>
+                <option v-for="station in cities" :key="station.id" :value="station.name">
+                  {{ station.name }}
+                </option>
+              </select>
+            </div>
+            <div class="form-group">
+              <label for="arrival">Station d'arrivée</label>
+              <select id="arrival" class="flex w-full" v-model="searchCriteria.arrival" required>
+                <option value="" disabled>Sélectionner une station</option>
+                <option v-for="station in cities" :key="station.id" :value="station.name">
+                  {{ station.name }}
+                </option>
+              </select>
+            </div>
+          </div>
+
+          <div class="form-row">
+            <div class="form-group">
+              <label for="departureDate">Date de départ</label>
+              <input type="date" id="departureDate" v-model="searchCriteria.departureDate"/>
+            </div>
+            <div class="form-group">
+              <label for="returnDate">Date de retour (optionnel)</label>
+              <input type="date" id="returnDate" v-model="searchCriteria.returnDate" />
+            </div>
+          </div>
+
+          <div class="form-row">
+            <div class="form-group">
+              <label for="passengers">Passagers</label>
+              <input type="number" id="passengers" v-model.number="searchCriteria.passengers" min="1" placeholder="Ex: 1" required />
+            </div>
+          </div>
+
+          <div class="form-group">
+            <button type="submit" class="search-button" :disabled="isLoading">
+              {{ isLoading ? 'Recherche en cours...' : 'Rechercher' }}
+            </button>
+          </div>
+        </form>
       </div>
 
-      <div class="form-row">
-        <div class="form-group">
-          <label for="departureDate">Date de départ</label>
-          <input type="date" id="departureDate" v-model="searchCriteria.departureDate" required />
+      <!-- Section des résultats de recherche -->
+    <div v-if="hasSearched" class="search-results-container results-section">
+      <h4>Résultats de recherche</h4>
+      
+      <!-- État de chargement -->
+      <div v-if="isLoading" class="loading-state">
+        <p>Recherche en cours...</p>
+      </div>
+      
+      <!-- Message d'erreur -->
+      <div v-else-if="error" class="error-state">
+        <p class="error-message">{{ error }}</p>
+        <button @click="handleSearch" class="retry-button">Réessayer</button>
+      </div>
+      
+      <!-- Aucun résultat -->
+      <div v-else-if="searchResults.length === 0" class="no-results">
+        <p>Aucun trajet trouvé pour vos critères de recherche.</p>
+        <p class="suggestion">Essayez de modifier vos critères ou choisir une autre date.</p>
+      </div>
+      
+      <!-- Liste des résultats -->
+      <div v-else class="results-list">
+        <div class="results-header">
+          <p>{{ searchResults.length }} trajet(s) trouvé(s)</p>
         </div>
-        <div class="form-group">
-          <label for="returnDate">Date de retour (optionnel)</label>
-          <input type="date" id="returnDate" v-model="searchCriteria.returnDate" />
+        
+        <div class="trip-card" v-for="trip in searchResults" :key="trip.id">
+          <div class="trip-header">
+            <div class="route-info">
+              <span class="departure">{{ trip.departStation?.name || 'Station inconnue' }}</span>
+              <span class="arrow">→</span>
+              <span class="arrival">{{ trip.arriveStation?.name || 'Station inconnue' }}</span>
+            </div>
+            <div class="price" v-if="trip.pricePerSeat">
+              {{ trip.pricePerSeat }}€
+            </div>
+          </div>
+          
+          <div class="trip-details">
+            <div class="time-info">
+              <div class="departure-time">
+                <strong>Départ:</strong> {{ formatDateTime(trip.departAt) }}
+              </div>
+              <div class="arrival-time">
+                <strong>Arrivée:</strong> {{ formatDateTime(trip.arriveAt) }}
+              </div>
+            </div>
+            
+            <div class="vehicle-info" v-if="trip.vehicule">
+              <span class="vehicle-model">{{ trip.vehicule.model }}</span>
+              <span class="vehicle-seats" v-if="trip.vehicule.number_places">
+                {{ trip.vehicule.number_places }} places
+              </span>
+            </div>
+            
+            <div class="availability" v-if="trip.availableSeats !== undefined">
+              <span class="seats-available">{{ trip.availableSeats }} places disponibles</span>
+            </div>
+          </div>
+          
+          <div class="trip-actions">
+            <button class="book-button" @click="selectTrip(trip)">
+              Réserver
+            </button>
+          </div>
         </div>
-      </div>
-
-      <div class="form-row">
-        <div class="form-group">
-          <label for="passengers">Passagers</label>
-          <input type="number" id="passengers" v-model.number="searchCriteria.passengers" min="1" placeholder="Ex: 1" required />
+        
+        <!-- Pagination -->
+        <div v-if="pagination.total > 2" class="pagination">
+          <button 
+            @click="changePage(pagination.page - 1)" 
+            :disabled="pagination.page <= 1"
+            class="page-button"
+          >
+            Précédent
+          </button>
+          
+          <span class="page-info">
+            Page {{ pagination.page }} sur {{ pagination.totalPages }}
+          </span>
+          
+          <button 
+            @click="changePage(pagination.page + 1)" 
+            :disabled="pagination.page >= pagination.totalPages"
+            class="page-button"
+          >
+            Suivant
+          </button>
         </div>
       </div>
-
-      <div class="form-group">
-        <button type="submit" class="search-button">Rechercher</button>
-      </div>
-    </form>
+    </div>
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
 import StationService from '@/services/StationService';
-import type { Station } from '@/types';
+import TripService from '@/services/TripService';
+import type { Station, Trip } from '@/types';
+
+const router = useRouter();
 
 const cities = ref<Station[]>([]);
+const searchResults = ref<Trip[]>([]);
+const isLoading = ref(false);
+const error = ref('');
+const hasSearched = ref(false);
+const currentPage = ref(1);
+const pagination = ref({
+  total: 0,
+  page: 1,
+  limit: 2,
+  totalPages: 1
+});
 
 const searchCriteria = ref({
   departure: '',
@@ -63,11 +179,97 @@ const searchCriteria = ref({
   passengers: 1,
 });
 
-const emit = defineEmits(['search-trips']);
+const emit = defineEmits(['search-trips', 'trip-selected']);
+
+const handleSearch = async () => {
+  if (!searchCriteria.value.departure || !searchCriteria.value.arrival) {
+    error.value = 'Veuillez sélectionner les stations de départ et d\'arrivée';
+    return;
+  }
+
+  isLoading.value = true;
+  error.value = '';
+  hasSearched.value = true;
+  currentPage.value = 1;
+
+  try {
+    // Construire la requête de recherche
+    const searchQuery = `${searchCriteria.value.departure} ${searchCriteria.value.arrival}`;
+    
+    const result = await TripService.searchTrips(searchQuery, currentPage.value, 2);
+    
+    searchResults.value = result.trips;
+    pagination.value = result.pagination;
+    
+    // Émettre l'événement pour compatibilité
+    emit('search-trips', { 
+      ...searchCriteria.value, 
+      results: result.trips 
+    });
+    
+  } catch (err) {
+    console.error('Erreur lors de la recherche:', err);
+    error.value = 'Une erreur est survenue lors de la recherche. Veuillez réessayer.';
+    searchResults.value = [];
+  } finally {
+    isLoading.value = false;
+  }
+};
 
-const handleSearch = () => {
-  // Valider les données ici si nécessaire avant d'émettre
-  emit('search-trips', { ...searchCriteria.value });
+const changePage = async (newPage: number) => {
+  if (newPage < 1 || newPage > pagination.value.totalPages) return;
+  
+  currentPage.value = newPage;
+  isLoading.value = true;
+  
+  try {
+    const searchQuery = `${searchCriteria.value.departure} ${searchCriteria.value.arrival}`;
+    const result = await TripService.searchTrips(searchQuery, newPage, 2);
+    
+    searchResults.value = result.trips;
+    pagination.value = result.pagination;
+    
+    // Animation de défilement vers le haut des résultats
+    setTimeout(() => {
+      const resultsSection = document.querySelector('.search-results-container');
+      if (resultsSection) {
+        resultsSection.scrollIntoView({ 
+          behavior: 'smooth', 
+          block: 'start' 
+        });
+      }
+    }, 100);
+  } catch (err) {
+    console.error('Erreur lors du changement de page:', err);
+    error.value = 'Erreur lors du chargement de la page.';
+  } finally {
+    isLoading.value = false;
+  }
+};
+
+const selectTrip = (trip: Trip) => {
+  // Naviguer vers la page de réservation avec l'ID du trajet
+  router.push(`/reservation/${trip.id}`);
+  
+  // Émettre l'événement pour compatibilité
+  emit('trip-selected', trip);
+};
+
+const formatDateTime = (dateString: string) => {
+  if (!dateString) return 'Non défini';
+  
+  try {
+    const date = new Date(dateString);
+    return date.toLocaleString('fr-FR', {
+      day: '2-digit',
+      month: '2-digit',
+      year: 'numeric',
+      hour: '2-digit',
+      minute: '2-digit'
+    });
+  } catch {
+    return 'Date invalide';
+  }
 };
 
 onMounted(async () => {
@@ -87,9 +289,16 @@ onMounted(async () => {
 .trip-search-form-container {
   background-color: #ffffff;
   padding: 25px;
+  margin: 5px;
   border-radius: 10px;
   box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
-  margin: 20px auto;
+  flex: 1;
+}
+
+@media (max-width: 768px) {
+  .trip-search-form-container {
+    flex: none;
+  }
 }
 
 .trip-search-form-container h3 {
@@ -121,19 +330,19 @@ onMounted(async () => {
 .search-form input[type="text"],
 .search-form input[type="date"],
 .search-form input[type="number"],
-.search-form select { /* Ajout de select ici */
+.search-form select {
   padding: 10px;
   border: 1px solid #ddd;
   border-radius: 5px;
   font-size: 1em;
   transition: border-color 0.3s;
-  background-color: white; /* Assurer un fond blanc pour les select */
+  background-color: white;
 }
 
 .search-form input[type="text"]:focus,
 .search-form input[type="date"]:focus,
 .search-form input[type="number"]:focus,
-.search-form select:focus { /* Ajout de select:focus ici */
+.search-form select:focus {
   border-color: #4b8ef1;
   outline: none;
 }
@@ -152,22 +361,313 @@ onMounted(async () => {
   margin-top: 10px;
 }
 
-.search-button:hover {
+.search-button:hover:not(:disabled) {
+  background-color: #3a75c4;
+}
+
+.search-button:disabled {
+  background-color: #ccc;
+  cursor: not-allowed;
+}
+
+/* Section des résultats */
+.search-results-container {
+  background-color: #ffffff;
+  padding: 25px;
+  border-radius: 10px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+  flex: 1;
+}
+
+@media (max-width: 768px) {
+  .search-results-container {
+    flex: none;
+  }
+}
+
+.search-results-container h4 {
+  color: #333;
+  margin-bottom: 20px;
+  font-size: 1.3em;
+  border-bottom: 2px solid #4b8ef1;
+  padding-bottom: 10px;
+}
+
+/* États de chargement et d'erreur */
+.loading-state, .error-state, .no-results {
+  text-align: center;
+  padding: 40px 20px;
+}
+
+.loading-state p {
+  color: #666;
+  font-size: 1.1em;
+}
+
+.error-state .error-message {
+  color: #e74c3c;
+  margin-bottom: 15px;
+  font-weight: 500;
+}
+
+.retry-button {
+  background-color: #e74c3c;
+  color: white;
+  padding: 10px 20px;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 0.9em;
+}
+
+.retry-button:hover {
+  background-color: #c0392b;
+}
+
+.no-results p {
+  color: #666;
+  margin-bottom: 10px;
+}
+
+.no-results .suggestion {
+  color: #888;
+  font-style: italic;
+  font-size: 0.9em;
+}
+
+/* Liste des résultats */
+.results-header {
+  margin-bottom: 20px;
+  padding: 10px;
+  background-color: #f8f9fa;
+  border-radius: 5px;
+}
+
+.results-header p {
+  margin: 0;
+  color: #333;
+  font-weight: 500;
+}
+
+.trip-card {
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  padding: 20px;
+  margin-bottom: 15px;
+  transition: box-shadow 0.3s, transform 0.2s;
+  background-color: #fafafa;
+}
+
+.trip-card:hover {
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  transform: translateY(-2px);
+}
+
+.trip-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #eee;
+}
+
+.route-info {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  font-size: 1.1em;
+  font-weight: 600;
+}
+
+.departure {
+  color: #2c3e50;
+}
+
+.arrow {
+  color: #4b8ef1;
+  font-weight: bold;
+  font-size: 1.2em;
+}
+
+.arrival {
+  color: #2c3e50;
+}
+
+.price {
+  font-size: 1.2em;
+  font-weight: bold;
+  color: #27ae60;
+  background-color: #e8f5e8;
+  padding: 5px 10px;
+  border-radius: 5px;
+}
+
+.trip-details {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 15px;
+  margin-bottom: 15px;
+}
+
+.time-info {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.departure-time, .arrival-time {
+  color: #555;
+  font-size: 0.9em;
+}
+
+.vehicle-info {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.vehicle-model {
+  font-weight: 500;
+  color: #333;
+}
+
+.vehicle-seats {
+  color: #666;
+  font-size: 0.9em;
+}
+
+.availability {
+  grid-column: 1 / -1;
+  text-align: center;
+  margin-top: 10px;
+}
+
+.seats-available {
+  background-color: #e8f5e8;
+  color: #27ae60;
+  padding: 5px 10px;
+  border-radius: 15px;
+  font-size: 0.9em;
+  font-weight: 500;
+}
+
+.trip-actions {
+  text-align: right;
+}
+
+.book-button {
+  background-color: #27ae60;
+  color: white;
+  padding: 10px 20px;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 0.9em;
+  font-weight: 500;
+  transition: background-color 0.3s;
+}
+
+.book-button:hover {
+  background-color: #219a52;
+}
+
+/* Pagination */
+.pagination {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 15px;
+  margin-top: 30px;
+  padding: 20px;
+  background-color: #f8f9fa;
+  border-radius: 5px;
+}
+
+.page-button {
+  background-color: #4b8ef1;
+  color: white;
+  padding: 8px 15px;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 0.9em;
+  transition: background-color 0.3s;
+}
+
+.page-button:hover:not(:disabled) {
   background-color: #3a75c4;
 }
 
+.page-button:disabled {
+  background-color: #ccc;
+  cursor: not-allowed;
+}
+
+.page-info {
+  color: #555;
+  font-weight: 500;
+}
+
 /* Responsive adjustments */
 @media (max-width: 768px) {
+  .trip-search-container {
+    padding: 0 10px;
+  }
+  
   .search-form .form-row {
     flex-direction: column;
     gap: 10px;
     margin-bottom: 10px;
   }
-   .trip-search-form-container h3 {
+  
+  .trip-search-form-container h3 {
     font-size: 1.3em;
   }
-   .search-button {
+  
+  .search-button {
     padding: 10px 15px;
   }
+  
+  .trip-details {
+    grid-template-columns: 1fr;
+    gap: 10px;
+  }
+  
+  .trip-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 10px;
+  }
+  
+  .route-info {
+    font-size: 1em;
+  }
+  
+  .pagination {
+    flex-direction: column;
+    gap: 10px;
+  }
+}
+
+@media (max-width: 480px) {
+  .trip-search-form-container,
+  .search-results-container {
+    padding: 15px;
+  }
+  
+  .trip-card {
+    padding: 15px;
+  }
+  
+  .route-info {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 5px;
+  }
+  
+  .arrow {
+    transform: rotate(90deg);
+  }
 }
 </style>

+ 463 - 0
app_transport_front/src/components/user/ReceiptViewModal.vue

@@ -0,0 +1,463 @@
+<template>
+  <div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
+    <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
+      <!-- Overlay -->
+      <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="$emit('close')"></div>
+
+      <!-- Modal -->
+      <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
+        <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
+          <!-- Header -->
+          <div class="flex justify-between items-center mb-6">
+            <div>
+              <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
+                Reçu de paiement
+              </h3>
+              <p class="text-sm text-gray-500">{{ receipt.receiptNumber }}</p>
+            </div>
+            <button
+              @click="$emit('close')"
+              class="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+            >
+              <i class="fas fa-times text-xl"></i>
+            </button>
+          </div>
+
+          <!-- Contenu du reçu -->
+          <div id="receipt-content" class="bg-white border border-gray-200 rounded-lg p-6">
+            <!-- En-tête de l'entreprise -->
+            <div class="text-center border-b border-gray-200 pb-6 mb-6">
+              <h2 class="text-2xl font-bold text-gray-900 mb-2">TRANSPORT EXPRESS</h2>
+              <p class="text-gray-600">Service de transport de voyageurs</p>
+              <p class="text-sm text-gray-500">Abidjan, Côte d'Ivoire</p>
+              <p class="text-sm text-gray-500">Tél: +225 XX XX XX XX | Email: contact@transport-express.ci</p>
+            </div>
+
+            <!-- Informations du reçu -->
+            <div class="grid grid-cols-2 gap-6 mb-6">
+              <div>
+                <h4 class="text-sm font-semibold text-gray-900 mb-3">INFORMATIONS DU REÇU</h4>
+                <div class="space-y-2 text-sm">
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Numéro de reçu:</span>
+                    <span class="font-medium">{{ receipt.receiptNumber }}</span>
+                  </div>
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Date d'émission:</span>
+                    <span class="font-medium">{{ formatDateTime(receipt.paidAt || receipt.createdAt) }}</span>
+                  </div>
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Réservation:</span>
+                    <span class="font-medium">{{ receipt.reservationNumber }}</span>
+                  </div>
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Statut:</span>
+                    <span :class="getPaymentStatusClass(receipt.paymentStatus)" class="px-2 py-1 text-xs font-medium rounded">
+                      {{ getPaymentStatusText(receipt.paymentStatus) }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              <div>
+                <h4 class="text-sm font-semibold text-gray-900 mb-3">INFORMATIONS CLIENT</h4>
+                <div class="space-y-2 text-sm">
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Nom:</span>
+                    <span class="font-medium">{{ receipt.passengers[0]?.firstName }} {{ receipt.passengers[0]?.lastName }}</span>
+                  </div>
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Téléphone:</span>
+                    <span class="font-medium">{{ receipt.passengers[0]?.phone }}</span>
+                  </div>
+                  <div class="flex justify-between">
+                    <span class="text-gray-600">Email:</span>
+                    <span class="font-medium">{{ receipt.passengers[0]?.email || 'N/A' }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Détails du voyage -->
+            <div class="mb-6">
+              <h4 class="text-sm font-semibold text-gray-900 mb-3">DÉTAILS DU VOYAGE</h4>
+              <div class="bg-gray-50 rounded-lg p-4">
+                <div class="grid grid-cols-2 gap-4 mb-4">
+                  <div>
+                    <p class="text-xs text-gray-500 uppercase tracking-wide">Départ</p>
+                    <p class="font-semibold text-gray-900">{{ receipt.trip.departureStation.name }}</p>
+                    <p class="text-sm text-gray-600">{{ receipt.trip.departureStation.city }}</p>
+                    <p class="text-sm text-gray-600">
+                      {{ formatDate(receipt.trip.departureDate) }} à {{ formatTime(receipt.trip.departureTime) }}
+                    </p>
+                  </div>
+                  <div>
+                    <p class="text-xs text-gray-500 uppercase tracking-wide">Arrivée</p>
+                    <p class="font-semibold text-gray-900">{{ receipt.trip.arrivalStation.name }}</p>
+                    <p class="text-sm text-gray-600">{{ receipt.trip.arrivalStation.city }}</p>
+                    <p class="text-sm text-gray-600">
+                      {{ formatDate(receipt.trip.arrivalDate) }} à {{ formatTime(receipt.trip.arrivalTime) }}
+                    </p>
+                  </div>
+                </div>
+                
+                <div class="grid grid-cols-3 gap-4 text-sm border-t border-gray-200 pt-4">
+                  <div>
+                    <p class="text-gray-600">Compagnie</p>
+                    <p class="font-medium">{{ receipt.trip.vehicle.company.name }}</p>
+                  </div>
+                  <div>
+                    <p class="text-gray-600">Véhicule</p>
+                    <p class="font-medium">{{ receipt.trip.vehicle.licensePlate }}</p>
+                  </div>
+                  <div>
+                    <p class="text-gray-600">Distance</p>
+                    <p class="font-medium">{{ receipt.trip.distance || 'N/A' }} km</p>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Détail des passagers et tarification -->
+            <div class="mb-6">
+              <h4 class="text-sm font-semibold text-gray-900 mb-3">DÉTAIL DE LA FACTURATION</h4>
+              <div class="overflow-hidden border border-gray-200 rounded-lg">
+                <table class="min-w-full divide-y divide-gray-200">
+                  <thead class="bg-gray-50">
+                    <tr>
+                      <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                        Passager
+                      </th>
+                      <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+                        Siège
+                      </th>
+                      <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
+                        Prix unitaire
+                      </th>
+                      <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
+                        Total
+                      </th>
+                    </tr>
+                  </thead>
+                  <tbody class="bg-white divide-y divide-gray-200">
+                    <tr v-for="(passenger, index) in receipt.passengers" :key="index">
+                      <td class="px-4 py-3 text-sm text-gray-900">
+                        {{ passenger.firstName }} {{ passenger.lastName }}
+                      </td>
+                      <td class="px-4 py-3 text-sm text-gray-500">
+                        {{ passenger.seatNumber || 'Non assigné' }}
+                      </td>
+                      <td class="px-4 py-3 text-sm text-gray-900 text-right">
+                        {{ formatCurrency(receipt.trip.price) }}
+                      </td>
+                      <td class="px-4 py-3 text-sm text-gray-900 text-right">
+                        {{ formatCurrency(receipt.trip.price) }}
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+            </div>
+
+            <!-- Récapitulatif des montants -->
+            <div class="border-t border-gray-200 pt-6">
+              <div class="space-y-2">
+                <div class="flex justify-between text-sm">
+                  <span class="text-gray-600">Sous-total ({{ receipt.numberOfSeats }} place(s))</span>
+                  <span class="text-gray-900">{{ formatCurrency(receipt.trip.price * receipt.numberOfSeats) }}</span>
+                </div>
+                
+                <div v-if="receipt.discountAmount > 0" class="flex justify-between text-sm">
+                  <span class="text-gray-600">Remise</span>
+                  <span class="text-green-600">-{{ formatCurrency(receipt.discountAmount) }}</span>
+                </div>
+                
+                <div class="flex justify-between text-base font-semibold border-t border-gray-200 pt-2">
+                  <span class="text-gray-900">Total payé</span>
+                  <span class="text-gray-900">{{ formatCurrency(receipt.totalAmount) }}</span>
+                </div>
+              </div>
+            </div>
+
+            <!-- Informations de paiement -->
+            <div class="mt-6 pt-6 border-t border-gray-200">
+              <h4 class="text-sm font-semibold text-gray-900 mb-3">INFORMATIONS DE PAIEMENT</h4>
+              <div class="grid grid-cols-2 gap-4 text-sm">
+                <div>
+                  <span class="text-gray-600">Méthode de paiement:</span>
+                  <span class="ml-2 font-medium">{{ getPaymentMethodText(receipt.paymentMethod) }}</span>
+                </div>
+                <div>
+                  <span class="text-gray-600">Date de paiement:</span>
+                  <span class="ml-2 font-medium">{{ formatDateTime(receipt.paidAt || receipt.createdAt) }}</span>
+                </div>
+              </div>
+            </div>
+
+            <!-- Footer -->
+            <div class="mt-8 pt-6 border-t border-gray-200 text-center">
+              <p class="text-xs text-gray-500">
+                Ce reçu fait foi de paiement. Conservez-le précieusement.
+              </p>
+              <p class="text-xs text-gray-500 mt-1">
+                Pour toute réclamation, contactez notre service client avec ce numéro de reçu.
+              </p>
+              <div class="mt-4">
+                <p class="text-xs text-gray-400">
+                  Reçu généré automatiquement le {{ formatDateTime(new Date().toISOString()) }}
+                </p>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Actions -->
+        <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
+          <div class="flex space-x-3">
+            <button
+              @click="downloadPDF"
+              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-download mr-2"></i>
+              Télécharger PDF
+            </button>
+            
+            <button
+              @click="printReceipt"
+              class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm 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-print mr-2"></i>
+              Imprimer
+            </button>
+            
+            <button
+              @click="emailReceipt"
+              class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm 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-envelope mr-2"></i>
+              Envoyer par email
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Reservation } from '@/types/Reservation'
+
+interface Props {
+  receipt: Reservation
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<{
+  close: []
+}>()
+
+const getPaymentStatusClass = (status: string) => {
+  switch (status) {
+    case 'paid':
+      return 'bg-green-100 text-green-800'
+    case 'pending':
+      return 'bg-yellow-100 text-yellow-800'
+    case 'failed':
+      return 'bg-red-100 text-red-800'
+    default:
+      return 'bg-gray-100 text-gray-800'
+  }
+}
+
+const getPaymentStatusText = (status: string) => {
+  switch (status) {
+    case 'paid':
+      return 'Payé'
+    case 'pending':
+      return 'En attente'
+    case 'failed':
+      return 'Échec'
+    default:
+      return status
+  }
+}
+
+const getPaymentMethodText = (method: string) => {
+  switch (method) {
+    case 'mobile_money':
+      return 'Mobile Money'
+    case 'card':
+      return 'Carte bancaire'
+    case 'cash':
+      return 'Espèces'
+    default:
+      return method
+  }
+}
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat('fr-FR', {
+    style: 'currency',
+    currency: 'XOF'
+  }).format(amount)
+}
+
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'long',
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric'
+  })
+}
+
+const formatTime = (time: string) => {
+  return new Date(`2000-01-01T${time}`).toLocaleTimeString('fr-FR', {
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const formatDateTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleDateString('fr-FR', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const downloadPDF = async () => {
+  try {
+    // Utiliser html2pdf ou jsPDF pour générer le PDF
+    const element = document.getElementById('receipt-content')
+    if (!element) return
+
+    // Logique de génération PDF (nécessite une bibliothèque comme html2pdf)
+    console.log('Génération du PDF pour le reçu:', props.receipt.receiptNumber)
+    
+    // Exemple avec html2pdf (à installer: npm install html2pdf.js)
+    // const html2pdf = (await import('html2pdf.js')).default
+    // const opt = {
+    //   margin: 1,
+    //   filename: `recu-${props.receipt.receiptNumber}.pdf`,
+    //   image: { type: 'jpeg', quality: 0.98 },
+    //   html2canvas: { scale: 2 },
+    //   jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
+    // }
+    // html2pdf().set(opt).from(element).save()
+    
+  } catch (error) {
+    console.error('Erreur lors de la génération du PDF:', error)
+  }
+}
+
+const printReceipt = () => {
+  const printWindow = window.open('', '_blank')
+  if (!printWindow) return
+  
+  const receiptContent = document.getElementById('receipt-content')?.innerHTML
+  if (!receiptContent) return
+  
+  const printHtml = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+      <title>Reçu - ${props.receipt.receiptNumber}</title>
+      <style>
+        body { font-family: Arial, sans-serif; margin: 20px; color: #333; }
+        .text-center { text-align: center; }
+        .text-right { text-align: right; }
+        .font-bold { font-weight: bold; }
+        .font-semibold { font-weight: 600; }
+        .text-sm { font-size: 14px; }
+        .text-xs { font-size: 12px; }
+        .mb-2 { margin-bottom: 8px; }
+        .mb-3 { margin-bottom: 12px; }
+        .mb-4 { margin-bottom: 16px; }
+        .mb-6 { margin-bottom: 24px; }
+        .mt-4 { margin-top: 16px; }
+        .mt-6 { margin-top: 24px; }
+        .mt-8 { margin-top: 32px; }
+        .p-4 { padding: 16px; }
+        .p-6 { padding: 24px; }
+        .px-4 { padding-left: 16px; padding-right: 16px; }
+        .py-3 { padding-top: 12px; padding-bottom: 12px; }
+        .pt-2 { padding-top: 8px; }
+        .pt-4 { padding-top: 16px; }
+        .pt-6 { padding-top: 24px; }
+        .pb-6 { padding-bottom: 24px; }
+        .border-t { border-top: 1px solid #e5e7eb; }
+        .border-b { border-bottom: 1px solid #e5e7eb; }
+        .border { border: 1px solid #e5e7eb; }
+        .rounded-lg { border-radius: 8px; }
+        .bg-gray-50 { background-color: #f9fafb; }
+        .text-gray-500 { color: #6b7280; }
+        .text-gray-600 { color: #4b5563; }
+        .text-gray-900 { color: #111827; }
+        .text-green-600 { color: #059669; }
+        .grid { display: grid; }
+        .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+        .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+        .gap-4 { gap: 16px; }
+        .gap-6 { gap: 24px; }
+        .space-y-2 > * + * { margin-top: 8px; }
+        .flex { display: flex; }
+        .justify-between { justify-content: space-between; }
+        table { width: 100%; border-collapse: collapse; }
+        th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e7eb; }
+        th { background-color: #f9fafb; font-weight: 600; font-size: 12px; text-transform: uppercase; }
+        .uppercase { text-transform: uppercase; }
+        .tracking-wide { letter-spacing: 0.025em; }
+        @media print {
+          body { margin: 0; }
+          .no-print { display: none; }
+        }
+      </style>
+    </head>
+    <body>
+      ${receiptContent}
+    </body>
+    </html>
+  `
+  
+  printWindow.document.write(printHtml)
+  printWindow.document.close()
+  printWindow.print()
+}
+
+const emailReceipt = async () => {
+  // Logique pour envoyer le reçu par email
+  console.log('Envoi du reçu par email pour:', props.receipt.receiptNumber)
+  
+  // Ici, vous pourriez appeler une API pour envoyer l'email
+  // ou ouvrir le client email par défaut avec les informations pré-remplies
+  const subject = encodeURIComponent(`Reçu de voyage - ${props.receipt.receiptNumber}`)
+  const body = encodeURIComponent(`
+Bonjour,
+
+Veuillez trouver ci-joint votre reçu de voyage.
+
+Détails du voyage:
+- Trajet: ${props.receipt.trip.departureStation.name} → ${props.receipt.trip.arrivalStation.name}
+- Date: ${formatDate(props.receipt.trip.departureDate)}
+- Montant: ${formatCurrency(props.receipt.totalAmount)}
+- Numéro de reçu: ${props.receipt.receiptNumber}
+
+Cordialement,
+L'équipe Transport Express
+  `)
+  
+  window.open(`mailto:?subject=${subject}&body=${body}`)
+}
+</script>
+
+<style scoped>
+/* Styles personnalisés pour l'impression */
+@media print {
+  .no-print {
+    display: none !important;
+  }
+}
+</style>

+ 377 - 0
app_transport_front/src/components/user/ReservationDetailsModal.vue

@@ -0,0 +1,377 @@
+<template>
+  <div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
+    <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
+      <!-- Overlay -->
+      <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="$emit('close')"></div>
+
+      <!-- Modal -->
+      <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
+        <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
+          <!-- Header -->
+          <div class="flex justify-between items-center mb-6">
+            <div>
+              <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
+                Détails de la réservation
+              </h3>
+              <p class="text-sm text-gray-500">{{ reservation.reservationNumber }}</p>
+            </div>
+            <button
+              @click="$emit('close')"
+              class="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+            >
+              <i class="fas fa-times text-xl"></i>
+            </button>
+          </div>
+
+          <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+            <!-- Informations du voyage -->
+            <div class="bg-gray-50 rounded-lg p-6">
+              <h4 class="text-lg font-semibold text-gray-900 mb-4">
+                <i class="fas fa-route mr-2 text-blue-500"></i>
+                Informations du voyage
+              </h4>
+              
+              <div class="space-y-4">
+                <div class="flex items-center justify-between">
+                  <div class="flex items-center space-x-3">
+                    <div class="w-3 h-3 bg-green-500 rounded-full"></div>
+                    <div>
+                      <p class="font-medium text-gray-900">{{ reservation.trip.departureStation.name }}</p>
+                      <p class="text-sm text-gray-500">{{ reservation.trip.departureStation.city }}</p>
+                    </div>
+                  </div>
+                  <div class="text-right">
+                    <p class="font-medium text-gray-900">{{ formatTime(reservation.trip.departureTime) }}</p>
+                    <p class="text-sm text-gray-500">{{ formatDate(reservation.trip.departureDate) }}</p>
+                  </div>
+                </div>
+                
+                <div class="flex justify-center">
+                  <div class="flex items-center space-x-2 text-gray-400">
+                    <div class="w-8 border-t border-dashed border-gray-300"></div>
+                    <i class="fas fa-bus"></i>
+                    <div class="w-8 border-t border-dashed border-gray-300"></div>
+                  </div>
+                </div>
+                
+                <div class="flex items-center justify-between">
+                  <div class="flex items-center space-x-3">
+                    <div class="w-3 h-3 bg-red-500 rounded-full"></div>
+                    <div>
+                      <p class="font-medium text-gray-900">{{ reservation.trip.arrivalStation.name }}</p>
+                      <p class="text-sm text-gray-500">{{ reservation.trip.arrivalStation.city }}</p>
+                    </div>
+                  </div>
+                  <div class="text-right">
+                    <p class="font-medium text-gray-900">{{ formatTime(reservation.trip.arrivalTime) }}</p>
+                    <p class="text-sm text-gray-500">{{ formatDate(reservation.trip.arrivalDate) }}</p>
+                  </div>
+                </div>
+              </div>
+              
+              <div class="mt-6 pt-4 border-t border-gray-200">
+                <div class="grid grid-cols-2 gap-4 text-sm">
+                  <div>
+                    <p class="text-gray-500">Compagnie</p>
+                    <p class="font-medium text-gray-900">{{ reservation.trip.vehicle.company.name }}</p>
+                  </div>
+                  <div>
+                    <p class="text-gray-500">Véhicule</p>
+                    <p class="font-medium text-gray-900">{{ reservation.trip.vehicle.licensePlate }}</p>
+                  </div>
+                  <div>
+                    <p class="text-gray-500">Durée estimée</p>
+                    <p class="font-medium text-gray-900">{{ calculateDuration() }}</p>
+                  </div>
+                  <div>
+                    <p class="text-gray-500">Distance</p>
+                    <p class="font-medium text-gray-900">{{ reservation.trip.distance || 'N/A' }} km</p>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Informations de la réservation -->
+            <div class="space-y-6">
+              <!-- Statut et dates -->
+              <div class="bg-white border border-gray-200 rounded-lg p-6">
+                <h4 class="text-lg font-semibold text-gray-900 mb-4">
+                  <i class="fas fa-info-circle mr-2 text-blue-500"></i>
+                  Statut de la réservation
+                </h4>
+                
+                <div class="space-y-3">
+                  <div class="flex justify-between items-center">
+                    <span class="text-gray-600">Statut</span>
+                    <span :class="getStatusBadgeClass(reservation.status)" class="px-3 py-1 text-sm font-medium rounded-full">
+                      {{ getStatusText(reservation.status) }}
+                    </span>
+                  </div>
+                  
+                  <div class="flex justify-between items-center">
+                    <span class="text-gray-600">Réservé le</span>
+                    <span class="font-medium text-gray-900">{{ formatDateTime(reservation.createdAt) }}</span>
+                  </div>
+                  
+                  <div v-if="reservation.confirmedAt" class="flex justify-between items-center">
+                    <span class="text-gray-600">Confirmé le</span>
+                    <span class="font-medium text-gray-900">{{ formatDateTime(reservation.confirmedAt) }}</span>
+                  </div>
+                </div>
+              </div>
+
+              <!-- Passagers -->
+              <div class="bg-white border border-gray-200 rounded-lg p-6">
+                <h4 class="text-lg font-semibold text-gray-900 mb-4">
+                  <i class="fas fa-users mr-2 text-blue-500"></i>
+                  Passagers ({{ reservation.numberOfSeats }})
+                </h4>
+                
+                <div class="space-y-3">
+                  <div v-for="(passenger, index) in reservation.passengers" :key="index" class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
+                    <div>
+                      <p class="font-medium text-gray-900">{{ passenger.firstName }} {{ passenger.lastName }}</p>
+                      <p class="text-sm text-gray-500">{{ passenger.phone }}</p>
+                    </div>
+                    <div class="text-right">
+                      <p class="text-sm text-gray-500">Siège {{ passenger.seatNumber || 'Non assigné' }}</p>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              <!-- Informations de paiement -->
+              <div class="bg-white border border-gray-200 rounded-lg p-6">
+                <h4 class="text-lg font-semibold text-gray-900 mb-4">
+                  <i class="fas fa-credit-card mr-2 text-blue-500"></i>
+                  Paiement
+                </h4>
+                
+                <div class="space-y-3">
+                  <div class="flex justify-between items-center">
+                    <span class="text-gray-600">Prix unitaire</span>
+                    <span class="font-medium text-gray-900">{{ formatCurrency(reservation.trip.price) }}</span>
+                  </div>
+                  
+                  <div class="flex justify-between items-center">
+                    <span class="text-gray-600">Nombre de places</span>
+                    <span class="font-medium text-gray-900">{{ reservation.numberOfSeats }}</span>
+                  </div>
+                  
+                  <div v-if="reservation.discountAmount > 0" class="flex justify-between items-center">
+                    <span class="text-gray-600">Remise</span>
+                    <span class="font-medium text-green-600">-{{ formatCurrency(reservation.discountAmount) }}</span>
+                  </div>
+                  
+                  <div class="flex justify-between items-center pt-3 border-t border-gray-200">
+                    <span class="text-lg font-semibold text-gray-900">Total</span>
+                    <span class="text-lg font-bold text-gray-900">{{ formatCurrency(reservation.totalAmount) }}</span>
+                  </div>
+                  
+                  <div class="flex justify-between items-center">
+                    <span class="text-gray-600">Méthode de paiement</span>
+                    <span class="font-medium text-gray-900">{{ getPaymentMethodText(reservation.paymentMethod) }}</span>
+                  </div>
+                  
+                  <div class="flex justify-between items-center">
+                    <span class="text-gray-600">Statut du paiement</span>
+                    <span :class="getPaymentStatusClass(reservation.paymentStatus)" class="px-2 py-1 text-xs font-medium rounded">
+                      {{ getPaymentStatusText(reservation.paymentStatus) }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Actions -->
+        <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
+          <div class="flex space-x-3">
+            <button
+              v-if="reservation.status === 'confirmed'"
+              @click="downloadTicket"
+              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-ticket-alt mr-2"></i>
+              Télécharger le ticket
+            </button>
+            
+            <button
+              @click="downloadReceipt"
+              class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm 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-receipt mr-2"></i>
+              Télécharger le reçu
+            </button>
+            
+            <button
+              v-if="canCancelReservation"
+              @click="cancelReservation"
+              class="inline-flex items-center px-4 py-2 border border-red-300 text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
+            >
+              <i class="fas fa-times mr-2"></i>
+              Annuler la réservation
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import type { Reservation } from '@/types/Reservation'
+
+interface Props {
+  reservation: Reservation
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<{
+  close: []
+}>()
+
+const canCancelReservation = computed(() => {
+  const now = new Date()
+  const departureDate = new Date(props.reservation.trip.departureDate)
+  const hoursUntilDeparture = (departureDate.getTime() - now.getTime()) / (1000 * 60 * 60)
+  
+  return props.reservation.status === 'confirmed' && hoursUntilDeparture > 24
+})
+
+const calculateDuration = () => {
+  const departure = new Date(`2000-01-01T${props.reservation.trip.departureTime}`)
+  const arrival = new Date(`2000-01-01T${props.reservation.trip.arrivalTime}`)
+  
+  let diff = arrival.getTime() - departure.getTime()
+  
+  // Si l'arrivée est le lendemain
+  if (diff < 0) {
+    diff += 24 * 60 * 60 * 1000
+  }
+  
+  const hours = Math.floor(diff / (1000 * 60 * 60))
+  const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+  
+  return `${hours}h${minutes.toString().padStart(2, '0')}`
+}
+
+const getStatusBadgeClass = (status: string) => {
+  switch (status) {
+    case 'confirmed':
+      return 'bg-green-100 text-green-800'
+    case 'pending':
+      return 'bg-yellow-100 text-yellow-800'
+    case 'cancelled':
+      return 'bg-red-100 text-red-800'
+    default:
+      return 'bg-gray-100 text-gray-800'
+  }
+}
+
+const getStatusText = (status: string) => {
+  switch (status) {
+    case 'confirmed':
+      return 'Confirmé'
+    case 'pending':
+      return 'En attente'
+    case 'cancelled':
+      return 'Annulé'
+    default:
+      return status
+  }
+}
+
+const getPaymentMethodText = (method: string) => {
+  switch (method) {
+    case 'mobile_money':
+      return 'Mobile Money'
+    case 'card':
+      return 'Carte bancaire'
+    case 'cash':
+      return 'Espèces'
+    default:
+      return method
+  }
+}
+
+const getPaymentStatusClass = (status: string) => {
+  switch (status) {
+    case 'paid':
+      return 'bg-green-100 text-green-800'
+    case 'pending':
+      return 'bg-yellow-100 text-yellow-800'
+    case 'failed':
+      return 'bg-red-100 text-red-800'
+    default:
+      return 'bg-gray-100 text-gray-800'
+  }
+}
+
+const getPaymentStatusText = (status: string) => {
+  switch (status) {
+    case 'paid':
+      return 'Payé'
+    case 'pending':
+      return 'En attente'
+    case 'failed':
+      return 'Échec'
+    default:
+      return status
+  }
+}
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat('fr-FR', {
+    style: 'currency',
+    currency: 'XOF'
+  }).format(amount)
+}
+
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'long',
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric'
+  })
+}
+
+const formatTime = (time: string) => {
+  return new Date(`2000-01-01T${time}`).toLocaleTimeString('fr-FR', {
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const formatDateTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleDateString('fr-FR', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const downloadTicket = () => {
+  // Logique pour télécharger le ticket
+  console.log('Téléchargement du ticket pour:', props.reservation.id)
+}
+
+const downloadReceipt = () => {
+  // Logique pour télécharger le reçu
+  console.log('Téléchargement du reçu pour:', props.reservation.id)
+}
+
+const cancelReservation = () => {
+  // Logique pour annuler la réservation
+  console.log('Annulation de la réservation:', props.reservation.id)
+}
+</script>
+
+<style scoped>
+/* Styles personnalisés si nécessaire */
+</style>

+ 338 - 0
app_transport_front/src/components/user/ReservationHistory.vue

@@ -0,0 +1,338 @@
+<template>
+  <div class="bg-white shadow rounded-lg">
+    <div class="px-4 py-5 sm:p-6">
+      <div class="flex justify-between items-center mb-6">
+        <h3 class="text-lg leading-6 font-medium text-gray-900">
+          Historique des réservations
+        </h3>
+        <div class="flex items-center space-x-4">
+          <!-- Filtre par statut -->
+          <select
+            v-model="selectedStatus"
+            class="block w-40 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
+          >
+            <option value="">Tous les statuts</option>
+            <option value="confirmed">Confirmé</option>
+            <option value="pending">En attente</option>
+            <option value="cancelled">Annulé</option>
+          </select>
+          
+          <!-- Recherche -->
+          <div class="relative">
+            <input
+              v-model="searchQuery"
+              type="text"
+              placeholder="Rechercher..."
+              class="block w-64 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 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>
+          </div>
+        </div>
+      </div>
+
+      <!-- Loading -->
+      <div v-if="loading" class="text-center py-8">
+        <i class="fas fa-spinner fa-spin text-gray-400 text-2xl mb-4"></i>
+        <p class="text-gray-500">Chargement...</p>
+      </div>
+
+      <!-- Liste des réservations -->
+      <div v-else-if="filteredReservations.length > 0" class="space-y-4">
+        <div
+          v-for="reservation in paginatedReservations"
+          :key="reservation.id"
+          class="border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer"
+          @click="selectedReservation = reservation"
+        >
+          <div class="flex justify-between items-start">
+            <div class="flex-1">
+              <div class="flex items-center space-x-3 mb-3">
+                <h4 class="text-lg font-semibold text-gray-900">
+                  {{ reservation.trajet.departAt.station.name }} → {{ reservation.trajet.arriveAt.station.name }}
+                </h4>
+                <span :class="getStatusBadgeClass(reservation.status)" class="px-3 py-1 text-xs font-medium rounded-full">
+                  {{ getStatusText(reservation.status) }}
+                </span>
+              </div>
+              
+              <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-gray-600">
+                <div>
+                  <i class="fas fa-calendar mr-2 text-gray-400"></i>
+                  <span>{{ formatDateTime(reservation.trajet.departAt.date) }}</span>
+                </div>
+                <div>
+                  <i class="fas fa-chair mr-2"></i
+                  >{{ reservation.place.number }} place(s)
+                      
+                </div>
+                <div>
+                    <span
+                        :class="getStatusBadgeClass(reservation.trajet.vehicule.company.name)"
+                        class="px-2 py-1 text-xs font-medium rounded-full"
+                      >
+                      <i class="fas fa-van-shuttle mr-2"></i>
+                        {{ getStatusText(reservation.trajet.vehicule.company.name) }}
+                    </span>
+                </div>
+                <div>
+                  <i class="fas fa-hashtag mr-2 text-gray-400"></i>
+                  <span>{{ reservation.id }}</span>
+                </div>
+              </div>
+              
+              <div class="mt-3 text-sm text-gray-500">
+                <span>Réservé le {{ formatDateTime(reservation.dateAt) }}</span>
+              </div>
+            </div>
+            
+            <div class="text-right ml-6">
+              <p class="text-xl font-bold text-gray-900">{{ formatCurrency(reservation.trajet.pricePerSeat) }}</p>
+              <div class="mt-2 space-x-2">
+                <button
+                  @click.stop="downloadTicket(reservation)"
+                  class="text-sm text-blue-600 hover:text-blue-800"
+                  :disabled="reservation.status !== 'confirmed'"
+                >
+                  <i class="fas fa-ticket-alt mr-1"></i>
+                  Ticket
+                </button>
+                <button
+                  @click.stop="downloadReceipt(reservation)"
+                  class="text-sm text-green-600 hover:text-green-800"
+                >
+                  <i class="fas fa-receipt mr-1"></i>
+                  Reçu
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Pagination -->
+        <div v-if="totalPages > 1" class="flex justify-center mt-6">
+          <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
+            <button
+              @click="currentPage = Math.max(1, currentPage - 1)"
+              :disabled="currentPage === 1"
+              class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
+            >
+              <i class="fas fa-chevron-left"></i>
+            </button>
+            
+            <button
+              v-for="page in visiblePages"
+              :key="page"
+              @click="currentPage = page"
+              :class="[
+                page === currentPage
+                  ? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
+                  : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50',
+                'relative inline-flex items-center px-4 py-2 border text-sm font-medium'
+              ]"
+            >
+              {{ page }}
+            </button>
+            
+            <button
+              @click="currentPage = Math.min(totalPages, currentPage + 1)"
+              :disabled="currentPage === totalPages"
+              class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
+            >
+              <i class="fas fa-chevron-right"></i>
+            </button>
+          </nav>
+        </div>
+      </div>
+
+      <!-- Aucune réservation -->
+      <div v-else class="text-center py-12">
+        <i class="fas fa-inbox text-gray-400 text-4xl mb-4"></i>
+        <h3 class="text-lg font-medium text-gray-900 mb-2">Aucune réservation trouvée</h3>
+        <p class="text-gray-500 mb-6">
+          {{ searchQuery || selectedStatus ? 'Aucun résultat pour vos critères de recherche.' : 'Vous n\'avez pas encore effectué de réservation.' }}
+        </p>
+        <router-link
+          to="/"
+          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"
+        >
+          Réserver un voyage
+        </router-link>
+      </div>
+    </div>
+
+    <!-- Modal détails réservation -->
+    <ReservationDetailsModal
+      v-if="selectedReservation"
+      :reservation="selectedReservation"
+      @close="selectedReservation = null"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/useUserStore'
+import ReservationDetailsModal from './ReservationDetailsModal.vue'
+import ReservationService from '@/services/ReservationService'
+import type { Reservation } from '@/types/Reservation'
+
+const userStore = useUserStore()
+
+const reservations = ref<Reservation[]>([])
+const loading = ref(false)
+const selectedReservation = ref<Reservation | null>(null)
+const searchQuery = ref('')
+const selectedStatus = ref('')
+const currentPage = ref(1)
+const itemsPerPage = 10
+
+const filteredReservations = computed(() => {
+  let filtered = reservations.value
+
+  // Filtre par statut
+  if (selectedStatus.value) {
+    filtered = filtered.filter(r => r.status === selectedStatus.value)
+  }
+
+  // Filtre par recherche
+  if (searchQuery.value) {
+    const query = searchQuery.value.toLowerCase()
+    filtered = filtered.filter(r => 
+      r.reservationNumber.toLowerCase().includes(query) ||
+      r.trajet.departAt.station.name.toLowerCase().includes(query) ||
+      r.trajet.arriveAt.station.name.toLowerCase().includes(query)
+    )
+  }
+
+  // Tri par date de création (plus récent en premier)
+  return filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
+})
+
+const totalPages = computed(() => {
+  return Math.ceil(filteredReservations.value.length / itemsPerPage)
+})
+
+const paginatedReservations = computed(() => {
+  const start = (currentPage.value - 1) * itemsPerPage
+  const end = start + itemsPerPage
+  return filteredReservations.value.slice(start, end)
+})
+
+const visiblePages = computed(() => {
+  const pages = []
+  const start = Math.max(1, currentPage.value - 2)
+  const end = Math.min(totalPages.value, currentPage.value + 2)
+  
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  
+  return pages
+})
+
+const loadReservations = async () => {
+  if (!userStore.userId) return
+  
+  try {
+    loading.value = true
+    const userId = parseInt(userStore.userId, 10)
+    const response = await ReservationService.getReservationsByUser(userId)
+    reservations.value = response.data.data
+    
+    console.log('reponseXXX', response)
+    
+  } catch (error) {
+    console.error('Erreur lors du chargement des réservations:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const getStatusBadgeClass = (status: string) => {
+  switch (status) {
+    case 'confirmed':
+      return 'bg-green-100 text-green-800'
+    case 'pending':
+      return 'bg-yellow-100 text-yellow-800'
+    case 'cancelled':
+      return 'bg-red-100 text-red-800'
+    default:
+      return 'bg-gray-100 text-gray-800'
+  }
+}
+
+const getStatusText = (status: string) => {
+  switch (status) {
+    case 'confirmed':
+      return 'Confirmé'
+    case 'pending':
+      return 'En attente'
+    case 'cancelled':
+      return 'Annulé'
+    default:
+      return status
+  }
+}
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat('fr-FR', {
+    style: 'currency',
+    currency: 'XOF'
+  }).format(amount)
+}
+
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'short',
+    year: 'numeric',
+    month: 'short',
+    day: 'numeric'
+  })
+}
+
+const formatTime = (time: string) => {
+  return new Date(`2000-01-01T${time}`).toLocaleTimeString('fr-FR', {
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const formatDateTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleDateString('fr-FR', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const downloadTicket = (reservation: Reservation) => {
+  // Logique pour télécharger le ticket
+  console.log('Téléchargement du ticket pour:', reservation.id)
+}
+
+const downloadReceipt = (reservation: Reservation) => {
+  // Logique pour télécharger le reçu
+  console.log('Téléchargement du reçu pour:', reservation.id)
+}
+
+// Réinitialiser la page lors du changement de filtre
+const resetPage = () => {
+  currentPage.value = 1
+}
+
+// Watchers pour réinitialiser la pagination
+import { watch } from 'vue'
+watch([searchQuery, selectedStatus], resetPage)
+
+onMounted(() => {
+  loadReservations()
+})
+</script>
+
+<style scoped>
+/* Styles personnalisés si nécessaire */
+</style>

+ 482 - 0
app_transport_front/src/components/user/ReservationReceipts.vue

@@ -0,0 +1,482 @@
+<template>
+  <div class="bg-white shadow rounded-lg">
+    <div class="px-4 py-5 sm:p-6">
+      <div class="flex justify-between items-center mb-6">
+        <h3 class="text-lg leading-6 font-medium text-gray-900">
+          Mes reçus de réservation
+        </h3>
+        <div class="flex items-center space-x-4">
+          <!-- Filtre par période -->
+          <select
+            v-model="selectedPeriod"
+            class="block w-40 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
+          >
+            <option value="all">Toutes les périodes</option>
+            <option value="this_month">Ce mois</option>
+            <option value="last_month">Mois dernier</option>
+            <option value="this_year">Cette année</option>
+            <option value="last_year">Année dernière</option>
+          </select>
+          
+          <!-- Bouton télécharger tout -->
+          <button
+            v-if="filteredReceipts.length > 0"
+            @click="downloadAllReceipts"
+            class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm 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-download mr-2"></i>
+            Tout télécharger
+          </button>
+        </div>
+      </div>
+
+      <!-- Statistiques -->
+      <div v-if="filteredReceipts.length > 0" class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
+        <div class="bg-blue-50 rounded-lg p-4">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-receipt text-blue-500 text-xl"></i>
+            </div>
+            <div class="ml-3">
+              <p class="text-sm font-medium text-blue-600">Total des reçus</p>
+              <p class="text-lg font-semibold text-blue-900">{{ filteredReceipts.length }}</p>
+            </div>
+          </div>
+        </div>
+        
+        <div class="bg-green-50 rounded-lg p-4">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-euro-sign text-green-500 text-xl"></i>
+            </div>
+            <div class="ml-3">
+              <p class="text-sm font-medium text-green-600">Montant total</p>
+              <p class="text-lg font-semibold text-green-900">{{ formatCurrency(totalAmount) }}</p>
+            </div>
+          </div>
+        </div>
+        
+        <div class="bg-yellow-50 rounded-lg p-4">
+          <div class="flex items-center">
+            <div class="flex-shrink-0">
+              <i class="fas fa-calendar text-yellow-500 text-xl"></i>
+            </div>
+            <div class="ml-3">
+              <p class="text-sm font-medium text-yellow-600">Période</p>
+              <p class="text-lg font-semibold text-yellow-900">{{ getPeriodText() }}</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Loading -->
+      <div v-if="loading" class="text-center py-8">
+        <i class="fas fa-spinner fa-spin text-gray-400 text-2xl mb-4"></i>
+        <p class="text-gray-500">Chargement des reçus...</p>
+      </div>
+
+      <!-- Liste des reçus -->
+      <div v-else-if="filteredReceipts.length > 0" class="space-y-4">
+        <div
+          v-for="receipt in paginatedReceipts"
+          :key="receipt.id"
+          class="border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
+        >
+          <div class="p-6">
+            <div class="flex justify-between items-start">
+              <div class="flex-1">
+                <div class="flex items-center space-x-3 mb-3">
+                  <h4 class="text-lg font-semibold text-gray-900">
+                    Reçu #{{ receipt.receiptNumber }}
+                  </h4>
+                  <span :class="getPaymentStatusClass(receipt.paymentStatus)" class="px-3 py-1 text-xs font-medium rounded-full">
+                    {{ getPaymentStatusText(receipt.paymentStatus) }}
+                  </span>
+                </div>
+                
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
+                  <div>
+                    <p class="text-sm text-gray-600 mb-2">
+                      <i class="fas fa-route mr-2 text-gray-400"></i>
+                      <strong>Trajet:</strong> {{ receipt.trip.departureStation.name }} → {{ receipt.trip.arrivalStation.name }}
+                    </p>
+                    <p class="text-sm text-gray-600 mb-2">
+                      <i class="fas fa-calendar mr-2 text-gray-400"></i>
+                      <strong>Date de voyage:</strong> {{ formatDate(receipt.trip.departureDate) }}
+                    </p>
+                    <p class="text-sm text-gray-600">
+                      <i class="fas fa-users mr-2 text-gray-400"></i>
+                      <strong>Passagers:</strong> {{ receipt.numberOfSeats }} place(s)
+                    </p>
+                  </div>
+                  
+                  <div>
+                    <p class="text-sm text-gray-600 mb-2">
+                      <i class="fas fa-credit-card mr-2 text-gray-400"></i>
+                      <strong>Méthode:</strong> {{ getPaymentMethodText(receipt.paymentMethod) }}
+                    </p>
+                    <p class="text-sm text-gray-600 mb-2">
+                      <i class="fas fa-clock mr-2 text-gray-400"></i>
+                      <strong>Payé le:</strong> {{ formatDateTime(receipt.paidAt || receipt.createdAt) }}
+                    </p>
+                    <p class="text-sm text-gray-600">
+                      <i class="fas fa-hashtag mr-2 text-gray-400"></i>
+                      <strong>Réservation:</strong> {{ receipt.reservationNumber }}
+                    </p>
+                  </div>
+                </div>
+                
+                <!-- Détail des passagers -->
+                <div class="bg-gray-50 rounded-lg p-4 mb-4">
+                  <h5 class="text-sm font-medium text-gray-900 mb-2">Détail des passagers</h5>
+                  <div class="space-y-2">
+                    <div v-for="(passenger, index) in receipt.passengers" :key="index" class="flex justify-between items-center text-sm">
+                      <span class="text-gray-700">{{ passenger.firstName }} {{ passenger.lastName }}</span>
+                      <span class="text-gray-500">{{ formatCurrency(receipt.trip.price) }}</span>
+                    </div>
+                  </div>
+                  
+                  <div class="border-t border-gray-200 mt-3 pt-3">
+                    <div class="flex justify-between items-center">
+                      <span class="text-sm font-medium text-gray-900">Sous-total</span>
+                      <span class="text-sm font-medium text-gray-900">{{ formatCurrency(receipt.trip.price * receipt.numberOfSeats) }}</span>
+                    </div>
+                    
+                    <div v-if="receipt.discountAmount > 0" class="flex justify-between items-center mt-1">
+                      <span class="text-sm text-gray-600">Remise</span>
+                      <span class="text-sm text-green-600">-{{ formatCurrency(receipt.discountAmount) }}</span>
+                    </div>
+                    
+                    <div class="flex justify-between items-center mt-2 pt-2 border-t border-gray-200">
+                      <span class="text-base font-semibold text-gray-900">Total</span>
+                      <span class="text-base font-semibold text-gray-900">{{ formatCurrency(receipt.totalAmount) }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+              
+              <div class="ml-6 text-right">
+                <p class="text-2xl font-bold text-gray-900 mb-4">{{ formatCurrency(receipt.totalAmount) }}</p>
+                
+                <div class="space-y-2">
+                  <button
+                    @click="viewReceipt(receipt)"
+                    class="w-full inline-flex items-center justify-center px-4 py-2 border border-blue-300 text-sm font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+                  >
+                    <i class="fas fa-eye mr-2"></i>
+                    Voir
+                  </button>
+                  
+                  <button
+                    @click="downloadReceipt(receipt)"
+                    class="w-full inline-flex items-center justify-center px-4 py-2 border border-green-300 text-sm font-medium rounded-md text-green-700 bg-green-50 hover:bg-green-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
+                  >
+                    <i class="fas fa-download mr-2"></i>
+                    PDF
+                  </button>
+                  
+                  <button
+                    @click="printReceipt(receipt)"
+                    class="w-full inline-flex items-center justify-center px-4 py-2 border border-gray-300 text-sm 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-gray-500"
+                  >
+                    <i class="fas fa-print mr-2"></i>
+                    Imprimer
+                  </button>
+                  
+                  <button
+                    @click="emailReceipt(receipt)"
+                    class="w-full inline-flex items-center justify-center px-4 py-2 border border-purple-300 text-sm font-medium rounded-md text-purple-700 bg-purple-50 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
+                  >
+                    <i class="fas fa-envelope mr-2"></i>
+                    Email
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Pagination -->
+        <div v-if="totalPages > 1" class="flex justify-center mt-6">
+          <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
+            <button
+              @click="currentPage = Math.max(1, currentPage - 1)"
+              :disabled="currentPage === 1"
+              class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
+            >
+              <i class="fas fa-chevron-left"></i>
+            </button>
+            
+            <button
+              v-for="page in visiblePages"
+              :key="page"
+              @click="currentPage = page"
+              :class="[
+                page === currentPage
+                  ? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
+                  : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50',
+                'relative inline-flex items-center px-4 py-2 border text-sm font-medium'
+              ]"
+            >
+              {{ page }}
+            </button>
+            
+            <button
+              @click="currentPage = Math.min(totalPages, currentPage + 1)"
+              :disabled="currentPage === totalPages"
+              class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
+            >
+              <i class="fas fa-chevron-right"></i>
+            </button>
+          </nav>
+        </div>
+      </div>
+
+      <!-- Aucun reçu -->
+      <div v-else class="text-center py-12">
+        <i class="fas fa-receipt text-gray-400 text-4xl mb-4"></i>
+        <h3 class="text-lg font-medium text-gray-900 mb-2">Aucun reçu trouvé</h3>
+        <p class="text-gray-500 mb-6">
+          {{ getEmptyStateMessage() }}
+        </p>
+        <router-link
+          to="/"
+          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"
+        >
+          Réserver un voyage
+        </router-link>
+      </div>
+    </div>
+
+    <!-- Modal de visualisation du reçu -->
+    <ReceiptViewModal
+      v-if="selectedReceipt"
+      :receipt="selectedReceipt"
+      @close="selectedReceipt = null"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/useUserStore'
+import ReceiptViewModal from './ReceiptViewModal.vue'
+import ReservationService  from '@/services/ReservationService'
+import type { Reservation } from '@/types/Reservation'
+
+const userStore = useUserStore()
+
+const receipts = ref<Reservation[]>([])
+const loading = ref(false)
+const selectedReceipt = ref<Reservation | null>(null)
+const selectedPeriod = ref('all')
+const currentPage = ref(1)
+const itemsPerPage = 10
+
+const filteredReceipts = computed(() => {
+  const paidReceipts = receipts.value.filter(r => r.paymentStatus === 'paid')
+  const now = new Date()
+  
+  switch (selectedPeriod.value) {
+    case 'this_month':
+      return paidReceipts.filter(r => {
+        const receiptDate = new Date(r.paidAt || r.createdAt)
+        return receiptDate.getMonth() === now.getMonth() && receiptDate.getFullYear() === now.getFullYear()
+      })
+    case 'last_month':
+      const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1)
+      return paidReceipts.filter(r => {
+        const receiptDate = new Date(r.paidAt || r.createdAt)
+        return receiptDate.getMonth() === lastMonth.getMonth() && receiptDate.getFullYear() === lastMonth.getFullYear()
+      })
+    case 'this_year':
+      return paidReceipts.filter(r => {
+        const receiptDate = new Date(r.paidAt || r.createdAt)
+        return receiptDate.getFullYear() === now.getFullYear()
+      })
+    case 'last_year':
+      return paidReceipts.filter(r => {
+        const receiptDate = new Date(r.paidAt || r.createdAt)
+        return receiptDate.getFullYear() === now.getFullYear() - 1
+      })
+    default:
+      return paidReceipts
+  }
+})
+
+const totalAmount = computed(() => {
+  return filteredReceipts.value.reduce((sum, receipt) => sum + receipt.totalAmount, 0)
+})
+
+const totalPages = computed(() => {
+  return Math.ceil(filteredReceipts.value.length / itemsPerPage)
+})
+
+const paginatedReceipts = computed(() => {
+  const start = (currentPage.value - 1) * itemsPerPage
+  const end = start + itemsPerPage
+  return filteredReceipts.value.slice(start, end)
+})
+
+const visiblePages = computed(() => {
+  const pages = []
+  const start = Math.max(1, currentPage.value - 2)
+  const end = Math.min(totalPages.value, currentPage.value + 2)
+  
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  
+  return pages
+})
+
+const loadReceipts = async () => {
+  if (!userStore.user?.id) return
+  
+  try {
+    loading.value = true
+    const response = await ReservationService.getUserReservations(userStore.user.id)
+    receipts.value = response.data.map(reservation => ({
+      ...reservation,
+      receiptNumber: `REC-${reservation.reservationNumber}`
+    }))
+  } catch (error) {
+    console.error('Erreur lors du chargement des reçus:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const getPeriodText = () => {
+  switch (selectedPeriod.value) {
+    case 'this_month':
+      return 'Ce mois'
+    case 'last_month':
+      return 'Mois dernier'
+    case 'this_year':
+      return 'Cette année'
+    case 'last_year':
+      return 'Année dernière'
+    default:
+      return 'Toutes'
+  }
+}
+
+const getEmptyStateMessage = () => {
+  switch (selectedPeriod.value) {
+    case 'this_month':
+      return 'Aucun reçu pour ce mois.'
+    case 'last_month':
+      return 'Aucun reçu pour le mois dernier.'
+    case 'this_year':
+      return 'Aucun reçu pour cette année.'
+    case 'last_year':
+      return 'Aucun reçu pour l\'année dernière.'
+    default:
+      return 'Vous n\'avez aucun reçu de paiement.'
+  }
+}
+
+const getPaymentStatusClass = (status: string) => {
+  switch (status) {
+    case 'paid':
+      return 'bg-green-100 text-green-800'
+    case 'pending':
+      return 'bg-yellow-100 text-yellow-800'
+    case 'failed':
+      return 'bg-red-100 text-red-800'
+    default:
+      return 'bg-gray-100 text-gray-800'
+  }
+}
+
+const getPaymentStatusText = (status: string) => {
+  switch (status) {
+    case 'paid':
+      return 'Payé'
+    case 'pending':
+      return 'En attente'
+    case 'failed':
+      return 'Échec'
+    default:
+      return status
+  }
+}
+
+const getPaymentMethodText = (method: string) => {
+  switch (method) {
+    case 'mobile_money':
+      return 'Mobile Money'
+    case 'card':
+      return 'Carte bancaire'
+    case 'cash':
+      return 'Espèces'
+    default:
+      return method
+  }
+}
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat('fr-FR', {
+    style: 'currency',
+    currency: 'XOF'
+  }).format(amount)
+}
+
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'long',
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric'
+  })
+}
+
+const formatDateTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleDateString('fr-FR', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const viewReceipt = (receipt: Reservation) => {
+  selectedReceipt.value = receipt
+}
+
+const downloadReceipt = async (receipt: Reservation) => {
+  // Logique pour télécharger le reçu en PDF
+  console.log('Téléchargement du reçu PDF pour:', receipt.id)
+}
+
+const printReceipt = (receipt: Reservation) => {
+  // Logique pour imprimer le reçu
+  console.log('Impression du reçu pour:', receipt.id)
+}
+
+const emailReceipt = async (receipt: Reservation) => {
+  // Logique pour envoyer le reçu par email
+  console.log('Envoi du reçu par email pour:', receipt.id)
+}
+
+const downloadAllReceipts = async () => {
+  // Logique pour télécharger tous les reçus
+  console.log('Téléchargement de tous les reçus')
+}
+
+// Réinitialiser la page lors du changement de filtre
+import { watch } from 'vue'
+watch(selectedPeriod, () => {
+  currentPage.value = 1
+})
+
+onMounted(() => {
+  loadReceipts()
+})
+</script>
+
+<style scoped>
+/* Styles personnalisés si nécessaire */
+</style>

+ 485 - 0
app_transport_front/src/components/user/TravelTickets.vue

@@ -0,0 +1,485 @@
+<template>
+  <div class="bg-white shadow rounded-lg">
+    <div class="px-4 py-5 sm:p-6">
+      <div class="flex justify-between items-center mb-6">
+        <h3 class="text-lg leading-6 font-medium text-gray-900">
+          Mes tickets de voyage
+        </h3>
+        <div class="flex items-center space-x-4">
+          <!-- Filtre par statut -->
+          <select
+            v-model="selectedFilter"
+            class="block w-48 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
+          >
+            <option value="all">Tous les tickets</option>
+            <option value="upcoming">Voyages à venir</option>
+            <option value="past">Voyages passés</option>
+            <option value="today">Aujourd'hui</option>
+          </select>
+        </div>
+      </div>
+
+      <!-- Loading -->
+      <div v-if="loading" class="text-center py-8">
+        <i class="fas fa-spinner fa-spin text-gray-400 text-2xl mb-4"></i>
+        <p class="text-gray-500">Chargement des tickets...</p>
+      </div>
+
+      <!-- Liste des tickets -->
+      <div v-else-if="filteredTickets.length > 0" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+        <div
+          v-for="ticket in filteredTickets"
+          :key="ticket.id"
+          class="relative"
+        >
+          <!-- Ticket Card -->
+          <div class="bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg shadow-lg overflow-hidden text-white">
+            <!-- Header du ticket -->
+            <div class="px-6 py-4 border-b border-blue-400">
+              <div class="flex justify-between items-center">
+                <div>
+                  <h4 class="text-lg font-bold">TICKET DE VOYAGE</h4>
+                  <p class="text-blue-100 text-sm">{{ ticket.reservationNumber }}</p>
+                </div>
+                <div class="text-right">
+                  <span :class="getTicketStatusClass(ticket)" class="px-3 py-1 text-xs font-medium rounded-full">
+                    {{ getTicketStatusText(ticket) }}
+                  </span>
+                </div>
+              </div>
+            </div>
+
+            <!-- Corps du ticket -->
+            <div class="px-6 py-4">
+              <div class="grid grid-cols-2 gap-4 mb-4">
+                <!-- Départ -->
+                <div>
+                  <p class="text-blue-100 text-xs uppercase tracking-wide">Départ</p>
+                  <p class="font-bold text-lg">{{ ticket.trajet.departAt.station.name }}</p>
+                  <p class="text-blue-100 text-sm">{{ ticket.trajet.departAt.station.name }}</p>
+                  <p class="text-sm mt-1">
+                    <i class="fas fa-calendar mr-1"></i>
+                    {{ formatDate(ticket.trajet.departAt.date) }}
+                  </p>
+                  <p class="text-sm">
+                    <i class="fas fa-clock mr-1"></i>
+                    {{ formatTime(ticket.trajet.departAt.date) }}
+                  </p>
+                </div>
+
+                <!-- Arrivée -->
+                <div>
+                  <p class="text-blue-100 text-xs uppercase tracking-wide">Arrivée</p>
+                  <p class="font-bold text-lg">{{ ticket.trajet.arriveAt.station.name }}</p>
+                  <p class="text-blue-100 text-sm">{{ ticket.trajet.arriveAt.station.name }}</p>
+                  <p class="text-sm mt-1">
+                    <i class="fas fa-calendar mr-1"></i>
+                    {{ formatDate(ticket.trajet.arriveAt.date) }}
+                  </p>
+                  <p class="text-sm">
+                    <i class="fas fa-clock mr-1"></i>
+                    {{ formatTime(ticket.trajet.arriveAt.date) }}
+                  </p>
+                </div>
+              </div>
+
+              <!-- Informations du voyage -->
+              <div class="grid grid-cols-3 gap-4 mb-4 text-sm">
+                <div>
+                  <p class="text-blue-100 text-xs uppercase">Compagnie</p>
+                  <p class="font-medium">{{ ticket.company?.name }}</p>
+                </div>
+                <div>
+                  <p class="text-blue-100 text-xs uppercase">Véhicule</p>
+                  <p class="font-medium">{{ ticket.trajet.vehicle?.name }}</p>
+                </div>
+                <div>
+                  <p class="text-blue-100 text-xs uppercase">Places</p>
+                  <p class="font-medium">{{ ticket.place.number }}</p>
+                </div>
+              </div>
+
+              <!-- Passagers -->
+              <div class="mb-4">
+                <p class="text-blue-100 text-xs uppercase mb-2">Passager(s)</p>
+                <div class="space-y-1">
+                  <div v-for="(passenger, index) in ticket.passengers" :key="index" class="flex justify-between text-sm">
+                    <span>{{ passenger.firstName }} {{ passenger.lastName }}</span>
+                    <span v-if="passenger.seatNumber">Siège {{ passenger.seatNumber }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Footer avec QR Code -->
+            <div class="bg-white bg-opacity-10 px-6 py-4">
+              <div class="flex justify-between items-center">
+                <div class="flex-1">
+                  <p class="text-blue-100 text-xs uppercase mb-1">Prix total</p>
+                  <p class="text-2xl font-bold">{{ formatCurrency(ticket.totalAmount) }}</p>
+                </div>
+                
+                <!-- 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>
+                  </div>
+                </div>
+              </div>
+              
+              <div class="mt-4 text-center">
+                <p class="text-blue-100 text-xs">
+                  Présentez ce QR code lors de l'embarquement
+                </p>
+              </div>
+            </div>
+
+            <!-- Actions -->
+            <div class="bg-black bg-opacity-20 px-6 py-3">
+              <div class="flex justify-center space-x-4">
+                <button
+                  @click="downloadTicket(ticket)"
+                  class="flex items-center space-x-2 text-white hover:text-blue-200 text-sm"
+                >
+                  <i class="fas fa-download"></i>
+                  <span>Télécharger</span>
+                </button>
+                
+                <button
+                  @click="printTicket(ticket)"
+                  class="flex items-center space-x-2 text-white hover:text-blue-200 text-sm"
+                >
+                  <i class="fas fa-print"></i>
+                  <span>Imprimer</span>
+                </button>
+                
+                <button
+                  @click="shareTicket(ticket)"
+                  class="flex items-center space-x-2 text-white hover:text-blue-200 text-sm"
+                >
+                  <i class="fas fa-share"></i>
+                  <span>Partager</span>
+                </button>
+              </div>
+            </div>
+          </div>
+
+          <!-- Badge de statut pour les voyages du jour -->
+          <div v-if="isToday(ticket.trajet.departAt.station.name)" class="absolute -top-2 -right-2">
+            <div class="bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full animate-pulse">
+              AUJOURD'HUI
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Aucun ticket -->
+      <div v-else class="text-center py-12">
+        <i class="fas fa-ticket-alt text-gray-400 text-4xl mb-4"></i>
+        <h3 class="text-lg font-medium text-gray-900 mb-2">Aucun ticket trouvé</h3>
+        <p class="text-gray-500 mb-6">
+          {{ getEmptyStateMessage() }}
+        </p>
+        <router-link
+          to="/"
+          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"
+        >
+          Réserver un voyage
+        </router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue'
+import { useUserStore } from '@/stores/useUserStore'
+import ReservationService  from '@/services/ReservationService'
+import QRCode from 'qrcode'
+import type { Reservation } from '@/types/Reservation'
+
+const userStore = useUserStore()
+
+const tickets = ref<Reservation[]>([])
+const loading = ref(false)
+const selectedFilter = ref('all')
+
+const filteredTickets = computed(() => {
+  return tickets.value
+  // const confirmedTickets = tickets.value.filter(t => t.status === 'confirmed')
+  // const now = new Date()
+  
+  // switch (selectedFilter.value) {
+  //   case 'upcoming':
+  //     return confirmedTickets.filter(t => new Date(t.trip.departureDate) > now)
+  //   case 'past':
+  //     return confirmedTickets.filter(t => new Date(t.trip.departureDate) < now)
+  //   case 'today':
+  //     return confirmedTickets.filter(t => isToday(t.trip.departureDate))
+  //   default:
+  //     return confirmedTickets
+  // }
+})
+
+const loadTickets = async () => {
+  console.log('loadTickets', userStore.userId)
+  if (!userStore.userId) return
+  
+  try {
+    loading.value = true
+    const response = await ReservationService.getReservationsByUser(userStore.userId)
+    console.log('loadTickets', response)
+    
+    // Récupérer les détails pour chaque réservation
+    const ticketsWithDetails = []
+    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)
+        ticketsWithDetails.push(reservation)
+      }
+    }
+    
+    tickets.value = ticketsWithDetails
+    
+    // Générer les QR codes après le chargement
+    await nextTick()
+    generateQRCodes()
+  } catch (error) {
+    console.error('Erreur lors du chargement des tickets:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+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), {
+          width: 80,
+          margin: 1,
+          color: {
+            dark: '#000000',
+            light: '#FFFFFF'
+          }
+        })
+      }
+    } catch (error) {
+      console.error('Erreur lors de la génération du QR code:', error)
+    }
+  }
+}
+
+const isToday = (date: string) => {
+  const today = new Date()
+  const ticketDate = new Date(date)
+  return today.toDateString() === ticketDate.toDateString()
+}
+
+const getTicketStatusClass = (ticket: Reservation) => {
+  const now = new Date()
+  const departureDate = new Date(ticket.trajet.departAt.date)
+  
+  if (isToday(ticket.trajet.departAt.date)) {
+    return 'bg-red-500 text-white'
+  } else if (departureDate > now) {
+    return 'bg-green-500 text-white'
+  } else {
+    return 'bg-gray-500 text-white'
+  }
+}
+
+const getTicketStatusText = (ticket: Reservation) => {
+  const now = new Date()
+  const departureDate = new Date(ticket.trajet.departAt.date)
+  
+  if (isToday(ticket.trajet.departAt.date)) {
+    return 'AUJOURD\'HUI'
+  } else if (departureDate > now) {
+    return 'À VENIR'
+  } else {
+    return 'PASSÉ'
+  }
+}
+
+const getEmptyStateMessage = () => {
+  switch (selectedFilter.value) {
+    case 'upcoming':
+      return 'Vous n\'avez aucun voyage prévu.'
+    case 'past':
+      return 'Vous n\'avez aucun voyage passé.'
+    case 'today':
+      return 'Vous n\'avez aucun voyage aujourd\'hui.'
+    default:
+      return 'Vous n\'avez aucun ticket de voyage.'
+  }
+}
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat('fr-FR', {
+    style: 'currency',
+    currency: 'XOF'
+  }).format(amount)
+}
+
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleDateString('fr-FR', {
+    weekday: 'short',
+    day: '2-digit',
+    month: 'short',
+    year: 'numeric'
+  })
+}
+
+const formatTime = (time: string) => {
+  return new Date(`2000-01-01T${time}`).toLocaleTimeString('fr-FR', {
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const downloadTicket = async (ticket: Reservation) => {
+  try {
+    // Créer un canvas temporaire pour le ticket complet
+    const canvas = document.createElement('canvas')
+    const ctx = canvas.getContext('2d')
+    
+    if (!ctx) return
+    
+    // Dimensions du ticket
+    canvas.width = 800
+    canvas.height = 600
+    
+    // Fond du ticket
+    const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0)
+    gradient.addColorStop(0, '#3B82F6')
+    gradient.addColorStop(1, '#2563EB')
+    ctx.fillStyle = gradient
+    ctx.fillRect(0, 0, canvas.width, canvas.height)
+    
+    // Ajouter le contenu du ticket (simplifié)
+    ctx.fillStyle = 'white'
+    ctx.font = 'bold 24px Arial'
+    ctx.fillText('TICKET DE VOYAGE', 50, 60)
+    
+    ctx.font = '16px Arial'
+    ctx.fillText(`${ticket.trajet.departAt.station.name} → ${ticket.trajet.arrivaAt.station.name}`, 50, 120)
+    ctx.fillText(`Date: ${formatDate(ticket.trajet.departAt.date)}`, 50, 150)
+    ctx.fillText(`Heure: ${formatTime(ticket.trajet.departAt.date)}`, 50, 180)
+    ctx.fillText(`Passager: ${ticket.passengers[0]?.firstName} ${ticket.passengers[0]?.lastName}`, 50, 210)
+    ctx.fillText(`Places: ${ticket.numberOfSeats}`, 50, 240)
+    ctx.fillText(`Prix: ${formatCurrency(ticket.totalAmount)}`, 50, 270)
+    
+    // Télécharger l'image
+    const link = document.createElement('a')
+    link.download = `ticket-${ticket.reservationNumber}.png`
+    link.href = canvas.toDataURL()
+    link.click()
+  } catch (error) {
+    console.error('Erreur lors du téléchargement:', error)
+  }
+}
+
+const printTicket = (ticket: Reservation) => {
+  const printWindow = window.open('', '_blank')
+  if (!printWindow) return
+  
+  const ticketHtml = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+      <title>Ticket - ${ticket.reservationNumber}</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; }
+      </style>
+    </head>
+    <body>
+      <div class="ticket">
+        <div class="header">
+          <h2>TICKET DE VOYAGE</h2>
+          <p>${ticket.reservationNumber}</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="qr-code">
+          <p>Présentez ce ticket lors de l'embarquement</p>
+        </div>
+      </div>
+    </body>
+    </html>
+  `
+  
+  printWindow.document.write(ticketHtml)
+  printWindow.document.close()
+  printWindow.print()
+}
+
+const shareTicket = async (ticket: Reservation) => {
+  if (navigator.share) {
+    try {
+      await navigator.share({
+        title: 'Mon ticket de voyage',
+        text: `Ticket de voyage ${ticket.trip.departureStation.name} → ${ticket.trajet.departAt.station.name} le ${formatDate(ticket.trip.departureDate)}`,
+        url: window.location.href
+      })
+    } catch (error) {
+      console.error('Erreur lors du partage:', error)
+    }
+  } else {
+    // Fallback: copier dans le presse-papiers
+    const text = `Ticket de voyage ${ticket.trip.departureStation.name} → ${ticket.trajet.departAt.station.name} le ${formatDate(ticket.trip.departureDate)} - Réservation: ${ticket.reservationNumber}`
+    navigator.clipboard.writeText(text)
+    alert('Informations du ticket copiées dans le presse-papiers')
+  }
+}
+
+// Watcher pour régénérer les QR codes lors du changement de filtre
+import { watch } from 'vue'
+watch(selectedFilter, async () => {
+  await nextTick()
+  generateQRCodes()
+})
+
+onMounted(() => {
+  loadTickets()
+})
+</script>
+
+<style scoped>
+@keyframes pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: .5;
+  }
+}
+
+.animate-pulse {
+  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+</style>

+ 25 - 2
app_transport_front/src/router/index.ts

@@ -103,6 +103,18 @@ const routes: Array<RouteRecordRaw> = [
     component: () => import('../views/ReservationView.vue'),
     meta: { requiresAuth: false }
   },
+  {
+    path: '/payment/success',
+    name: 'PaymentSuccess',
+    component: () => import('../views/PaymentSuccessView.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/user/dashboard',
+    name: 'UserDashboard',
+    component: () => import('../views/UserDashboardView.vue'),
+    meta: { requiresAuth: true, roles: [Role.User] }
+  },
   // {
   //   path: '/driver-setup',
   //   name: 'driver-setup',
@@ -132,8 +144,19 @@ router.beforeEach((to, from, next) => {
   } else if (to.meta.requiresAuth && token) {
     const allowedRoles = to.meta.roles as string[] | undefined
     if(allowedRoles && userRole && !allowedRoles.includes(userRole)) {
-      console.log('Access denied')
-      next('/login')
+      console.log('Access denied - Rôle actuel:', userRole, 'Rôles autorisés:', allowedRoles)
+      // Rediriger vers le dashboard approprié en fonction du rôle
+      if (userRole === Role.User) {
+        next('/user/dashboard')
+      } else if (userRole === Role.Admin) {
+        next('/dashboard')
+      } else if (userRole === Role.Manager) {
+        // 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 {
+        next('/login')
+      }
     } else {
       next()
       return

+ 148 - 0
app_transport_front/src/services/CinetPayService.ts

@@ -0,0 +1,148 @@
+import type { CinetPayPaymentRequest, CinetPayPaymentResponse } from '@/types/cinetpay'
+
+class CinetPayService {
+  private readonly apiKey: string
+  private readonly siteId: string
+  private readonly apiUrl: string
+  private readonly notifyUrl: string
+  private readonly returnUrl: string
+
+  constructor() {
+    this.apiKey = import.meta.env.VITE_CINETPAY_APIKEY || ''
+    this.siteId = import.meta.env.VITE_CINETPAY_SITE_ID || ''
+    this.apiUrl = import.meta.env.VITE_CINETPAY_API_URL || 'https://api-checkout.cinetpay.com/v2/payment'
+    this.notifyUrl = import.meta.env.VITE_CINETPAY_NOTIFY_URL || ''
+    this.returnUrl = import.meta.env.VITE_CINETPAY_RETURN_URL || ''
+
+    if (!this.apiKey || !this.siteId) {
+      console.warn('CinetPay: Variables d\'environnement manquantes (APIKEY ou SITE_ID)')
+    }
+  }
+
+  /**
+   * Génère un identifiant de transaction unique
+   */
+  private generateTransactionId(): string {
+    return `TXN_${Date.now()}_${Math.floor(Math.random() * 100000)}`
+  }
+
+  /**
+   * Initialise un paiement CinetPay selon la documentation officielle
+   * @param paymentData - Données de paiement
+   * @returns Réponse de l'API CinetPay
+   */
+  async initializePayment(paymentData: {
+    amount: number
+    currency: string
+    description: string
+    customerData: {
+      id?: string
+      name: string
+      surname: string
+      email: string
+      phone: string
+      address: string
+      city: string
+      country: string
+      state: string
+      zipCode: string
+    }
+    metadata?: any
+    channels?: string
+    lang?: string
+  }): Promise<CinetPayPaymentResponse> {
+    const requestData: CinetPayPaymentRequest = {
+      apikey: this.apiKey,
+      site_id: this.siteId,
+      transaction_id: this.generateTransactionId(),
+      amount: paymentData.amount,
+      currency: paymentData.currency,
+      description: paymentData.description,
+      notify_url: this.notifyUrl,
+      return_url: this.returnUrl,
+      channels: paymentData.channels || 'ALL',
+      lang: paymentData.lang || 'FR',
+      metadata: paymentData.metadata ? JSON.stringify(paymentData.metadata) : undefined,
+      
+      // Données client obligatoires pour les cartes bancaires
+      customer_id: paymentData.customerData.id,
+      customer_name: paymentData.customerData.name,
+      customer_surname: paymentData.customerData.surname,
+      customer_email: paymentData.customerData.email,
+      customer_phone_number: paymentData.customerData.phone,
+      customer_address: paymentData.customerData.address,
+      customer_city: paymentData.customerData.city,
+      customer_country: paymentData.customerData.country,
+      customer_state: paymentData.customerData.state,
+      customer_zip_code: paymentData.customerData.zipCode
+    }
+
+    try {
+      const response = await fetch(this.apiUrl, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(requestData)
+      })
+
+      if (!response.ok) {
+        const errorText = await response.text().catch(() => 'Réponse non disponible')
+        throw new Error(`Service CinetPay indisponible (${response.status}): ${errorText}`)
+      }
+
+      const result: CinetPayPaymentResponse = await response.json()
+      
+      // Vérifier la réponse selon la documentation CinetPay
+      if (result.code !== '201' && result.code !== '200') {
+        const errorMsg = result.message || result.description || 'Erreur inconnue du service de paiement'
+        throw new Error(`CinetPay: ${errorMsg}`)
+      }
+
+      return result
+    } catch (error) {
+      console.error('Erreur CinetPay:', error)
+      
+      // Gestion spécifique des erreurs réseau et de connexion
+      if (error instanceof TypeError && error.message.includes('fetch')) {
+        throw new Error('Service de paiement CinetPay inaccessible. Vérifiez votre connexion internet.')
+      }
+      
+      if (error instanceof Error && error.name === 'AbortError') {
+        throw new Error('Timeout du service de paiement CinetPay. Veuillez réessayer.')
+      }
+      
+      // Re-lancer l'erreur si elle est déjà formatée
+      if (error instanceof Error && (error.message.includes('CinetPay') || error.message.includes('Service'))) {
+        throw error
+      }
+      
+      // Erreur générique
+      throw new Error('Erreur de communication avec le service de paiement CinetPay')
+    }
+  }
+
+  /**
+   * Vérifie le statut d'un paiement
+   * @param transactionId - ID de la transaction
+   */
+  async checkPaymentStatus(transactionId: string): Promise<any> {
+    // Cette méthode sera implémentée selon les besoins
+    // Elle nécessite l'endpoint de vérification de statut de CinetPay
+    console.log('Vérification du statut pour:', transactionId)
+  }
+
+  /**
+   * Valide les données de webhook CinetPay
+   * @param webhookData - Données reçues du webhook
+   */
+  validateWebhookData(webhookData: any): boolean {
+    // Validation basique - à améliorer selon les spécifications CinetPay
+    return webhookData && 
+           webhookData.transaction_id && 
+           webhookData.status && 
+           webhookData.amount
+  }
+}
+
+export default new CinetPayService()

+ 19 - 5
app_transport_front/src/services/ReservationService.ts

@@ -1,11 +1,12 @@
 import type { Reservation } from '@/types'
+import { authenticatedFetch } from '@/services/authService'
 
 const API_URL = 'http://localhost:3001/api'
 
 export default {
   async getReservations(): Promise<Reservation[]> {
     try {
-      const response = await fetch(`${API_URL}/reservations`)
+      const response = await authenticatedFetch(`${API_URL}/reservations`)
       if (!response.ok) {
         throw new Error('Erreur lors de la récupération des réservations')
       }
@@ -18,7 +19,7 @@ export default {
 
   async getReservation(id: number): Promise<Reservation | null> {
     try {
-      const response = await fetch(`${API_URL}/reservations/${id}`)
+      const response = await authenticatedFetch(`${API_URL}/reservations/${id}`)
       if (!response.ok) {
         throw new Error('Erreur lors de la récupération de la réservation')
       }
@@ -29,9 +30,22 @@ export default {
     }
   },
 
-  async getReservationsByUser(userId: number): Promise<Reservation[]> {
+  async getReservationsByUser(userId: number | string): Promise<Reservation[]> {
     try {
-      const response = await fetch(`${API_URL}/reservations/user/${userId}`)
+      const response = await authenticatedFetch(`${API_URL}/reservations/user/${userId}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des réservations utilisateur')
+      }
+      return await response.json()
+    } catch (error) {
+      console.error('Erreur:', error)
+      return []
+    }
+  },
+
+    async getReservationsByDetail(reservationId: number | string): Promise<Reservation[]> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/reservations/${reservationId}/details`)
       if (!response.ok) {
         throw new Error('Erreur lors de la récupération des réservations utilisateur')
       }
@@ -44,7 +58,7 @@ export default {
 
   async getReservationsByTrip(trajetId: number): Promise<Reservation[]> {
     try {
-      const response = await fetch(`${API_URL}/reservations/trajet/${trajetId}`)
+      const response = await authenticatedFetch(`${API_URL}/reservations/trajet/${trajetId}`)
       if (!response.ok) {
         throw new Error('Erreur lors de la récupération des réservations du trajet')
       }

+ 100 - 0
app_transport_front/src/types/cinetpay.ts

@@ -0,0 +1,100 @@
+// Types pour l'intégration CinetPay selon la documentation officielle
+
+export interface CinetPayPaymentRequest {
+  // Paramètres obligatoires
+  apikey: string
+  site_id: string
+  transaction_id: string
+  amount: number
+  currency: string
+  description: string
+  notify_url: string
+  return_url: string
+  channels: string
+  
+  // Paramètres optionnels
+  lang?: string
+  metadata?: string
+  invoice_data?: {
+    Donnee1?: string
+    Donnee2?: string
+    Donnee3?: string
+  }
+  lock_phone_number?: boolean
+  
+  // Données client (obligatoires pour les cartes bancaires)
+  customer_id?: string
+  customer_name: string
+  customer_surname: string
+  customer_phone_number: string
+  customer_email: string
+  customer_address: string
+  customer_city: string
+  customer_country: string
+  customer_state: string
+  customer_zip_code: string
+}
+
+export interface CinetPayPaymentResponse {
+  code: string
+  message: string
+  description?: string
+  data?: {
+    payment_token: string
+    payment_url: string
+  }
+}
+
+export interface CinetPayWebhookData {
+  transaction_id: string
+  status: string
+  amount: number
+  currency: string
+  payment_method: string
+  operator_id?: string
+  phone_number?: string
+  payment_date: string
+  signature?: string
+}
+
+export interface CinetPayStatusResponse {
+  code: string
+  message: string
+  data?: {
+    transaction_id: string
+    status: string
+    amount: number
+    currency: string
+    payment_method: string
+    payment_date: string
+  }
+}
+
+// Énumérations pour les valeurs possibles
+export enum CinetPayCurrency {
+  XOF = 'XOF', // Franc CFA Ouest Africain
+  XAF = 'XAF', // Franc CFA Central Africain
+  CDF = 'CDF', // Franc Congolais
+  GNF = 'GNF', // Franc Guinéen
+  USD = 'USD', // Dollar Américain
+  EUR = 'EUR'  // Euro
+}
+
+export enum CinetPayChannels {
+  ALL = 'ALL',
+  MOBILE_MONEY = 'MOBILE_MONEY',
+  CREDIT_CARD = 'CREDIT_CARD',
+  WALLET = 'WALLET'
+}
+
+export enum CinetPayLanguage {
+  FR = 'FR',
+  EN = 'EN'
+}
+
+export enum CinetPayStatus {
+  PENDING = 'PENDING',
+  COMPLETED = 'COMPLETED',
+  FAILED = 'FAILED',
+  CANCELLED = 'CANCELLED'
+}

+ 5 - 12
app_transport_front/src/views/HomeView.vue

@@ -49,7 +49,7 @@
                     </div>
                   </div>
                   <!-- Emplacement pour le formulaire de recherche -->
-                  <TripSearchForm v-if="showSearchForm" @search-trips="handleTripSearch" class="trip-search-on-banner" />
+                  <TripSearchForm v-if="showSearchForm" class="trip-search-on-banner" />
                 </div>
               </div>
             </div>
@@ -416,16 +416,8 @@ const reserveTrip = (trip: any) => {
   router.push({ name: 'reservation', params: { id: trip.id } })
 }
 
-const handleTripSearch = (criteria: any) => {
-  console.log('Critères de recherche:', criteria);
-  // Ici, vous naviguerez vers une page de résultats ou afficherez les résultats.
-  // Par exemple, en utilisant Vue Router :
-  // router.push({ name: 'SearchResults', query: criteria });
-  // Pour l'instant, nous allons juste logger les critères.
-  // Vous devrez implémenter la logique de recherche ou la navigation.
-  info(`Recherche pour : ${criteria.departure} à ${criteria.arrival} le ${criteria.departureDate}`);
-  showSearchForm.value = false; // Optionnel: cacher le formulaire après la recherche
-};
+// La fonction handleTripSearch a été supprimée car le composant TripSearchForm
+// gère maintenant sa propre logique de recherche et d'affichage des résultats
 
 onMounted(async () => {
   // Charger les trajets disponibles
@@ -1009,7 +1001,7 @@ html, body {
   display: inline-block;
   padding: 12px 25px;
   background: linear-gradient(135deg, #4b8ef1 0%, #7a6ad8 100%);
-  color: #fff;
+  color: #fff !important;
   border: none;
   font-size: 14px;
   font-weight: 500;
@@ -1022,6 +1014,7 @@ html, body {
 .trip-actions .border-button a:hover {
   transform: translateY(-2px);
   box-shadow: 0px 5px 15px rgba(75, 142, 241, 0.4);
+  color: #fff !important;
 }
 
 .no-trips {

+ 48 - 25
app_transport_front/src/views/LoginView.vue

@@ -83,34 +83,57 @@ const redirectionUser = async () => {
   const token = userStore.token
   const userRole = userStore.userRole
   if (userId) {
-    console.log('userId', userId)
-    //TODO:: a amélioré
-    const response = await fetch(`${(import.meta as any).env.VITE_API_BASE_URL}/companies/manager/${userId}`, {
-      method: 'GET',
-      headers: {
-        'Content-Type': 'application/json',
-        'Authorization': `Bearer ${token}`
-      }
-    })
-
-    if (response.ok) {
-      const company = await response.json()
-      userCompanyStore.setCompany({
-        id: company.data.id,
-        name: company.data.name,
-        address: company.data.address,
-        drivers: company.data.drivers || [],
-        vehicules: company.data.vehicules || []
-      })
-      router.push('/dashboard/' + company.data.id)
-    } else if (response.status === 404 && userRole === Role.Admin ) {
+    console.log('userId', userId, 'userRole', userRole)
+    
+    // Si l'utilisateur est un simple utilisateur (USER), rediriger directement vers le dashboard utilisateur
+    if (userRole === Role.User) {
+      console.log('Redirection utilisateur simple vers /user/dashboard')
+      router.push('/user/dashboard')
+      return
+    }
+    
+    // Si l'utilisateur est un admin, rediriger directement vers le dashboard admin
+    if (userRole === Role.Admin) {
+      console.log('Redirection admin vers /dashboard/')
       router.push('/dashboard/')
-    } else if (response.status === 404) {
-     console.log('pas de conpagnie')
+      return
     }
-    else {
-      router.push('/login')
+    
+    // Pour les managers, vérifier s'ils ont une compagnie associée
+    if (userRole === Role.Manager) {
+      try {
+        const response = await fetch(`${(import.meta as any).env.VITE_API_BASE_URL}/companies/manager/${userId}`, {
+          method: 'GET',
+          headers: {
+            'Content-Type': 'application/json',
+            'Authorization': `Bearer ${token}`
+          }
+        })
+
+        if (response.ok) {
+          const company = await response.json()
+          userCompanyStore.setCompany({
+            id: company.data.id,
+            name: company.data.name,
+            address: company.data.address,
+            drivers: company.data.drivers || [],
+            vehicules: company.data.vehicules || []
+          })
+          router.push('/dashboard/' + company.data.id)
+        } else {
+          // Si le manager n'a pas de compagnie, rediriger vers la page de création de compagnie
+          console.log('Manager sans compagnie')
+          router.push('/company-setup')
+        }
+      } catch (error) {
+        console.error('Erreur lors de la récupération de la compagnie:', error)
+        router.push('/login')
+      }
+      return
     }
+    
+    // Pour les autres rôles (Driver, Employee), rediriger vers le dashboard utilisateur par défaut
+    router.push('/user/dashboard')
   }
 }
 

+ 143 - 0
app_transport_front/src/views/PaymentSuccessView.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+    <div class="max-w-md w-full space-y-8">
+      <div class="text-center">
+        <!-- Icône de succès -->
+        <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-6">
+          <svg class="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
+          </svg>
+        </div>
+        
+        <h2 class="text-3xl font-extrabold text-gray-900 mb-2">
+          Paiement réussi !
+        </h2>
+        
+        <p class="text-gray-600 mb-8">
+          Votre réservation a été confirmée avec succès.
+        </p>
+      </div>
+
+      <!-- Détails de la transaction -->
+      <div v-if="transactionDetails" class="bg-white shadow rounded-lg p-6 space-y-4">
+        <h3 class="text-lg font-medium text-gray-900 mb-4">Détails de la réservation</h3>
+        
+        <div class="space-y-3">
+          <div class="flex justify-between">
+            <span class="text-gray-600">ID de transaction :</span>
+            <span class="font-medium text-gray-900">{{ transactionDetails.transaction_id }}</span>
+          </div>
+          
+          <div class="flex justify-between">
+            <span class="text-gray-600">Montant :</span>
+            <span class="font-medium text-gray-900">{{ transactionDetails.amount }} {{ transactionDetails.currency }}</span>
+          </div>
+          
+          <div class="flex justify-between">
+            <span class="text-gray-600">Statut :</span>
+            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
+              Confirmé
+            </span>
+          </div>
+          
+          <div class="flex justify-between">
+            <span class="text-gray-600">Date :</span>
+            <span class="font-medium text-gray-900">{{ formatDate(transactionDetails.payment_date) }}</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- Message d'information -->
+      <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
+        <div class="flex">
+          <div class="flex-shrink-0">
+            <svg class="h-5 w-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
+              <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
+            </svg>
+          </div>
+          <div class="ml-3">
+            <p class="text-sm text-blue-700">
+              Un email de confirmation vous sera envoyé sous peu avec tous les détails de votre réservation.
+            </p>
+          </div>
+        </div>
+      </div>
+
+      <!-- Actions -->
+      <div class="space-y-3">
+        <button
+          @click="goToReservations"
+          class="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
+        >
+          Voir mes réservations
+        </button>
+        
+        <button
+          @click="goToHome"
+          class="w-full flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
+        >
+          Retour à l'accueil
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+
+const route = useRoute()
+const router = useRouter()
+
+const transactionDetails = ref<any>(null)
+const loading = ref(true)
+const error = ref('')
+
+// Récupérer les détails de la transaction depuis les paramètres de l'URL
+onMounted(async () => {
+  try {
+    // CinetPay renvoie généralement les paramètres dans l'URL
+    const transactionId = route.query.transaction_id as string
+    const status = route.query.status as string
+    const amount = route.query.amount as string
+    const currency = route.query.currency as string
+    
+    if (transactionId && status === 'COMPLETED') {
+      transactionDetails.value = {
+        transaction_id: transactionId,
+        status: status,
+        amount: amount,
+        currency: currency,
+        payment_date: new Date().toISOString()
+      }
+    } else if (status === 'FAILED' || status === 'CANCELLED') {
+      // Rediriger vers une page d'erreur ou afficher un message d'erreur
+      error.value = 'Le paiement a échoué ou a été annulé'
+    }
+  } catch (err) {
+    console.error('Erreur lors de la récupération des détails:', err)
+    error.value = 'Erreur lors de la récupération des détails de la transaction'
+  } finally {
+    loading.value = false
+  }
+})
+
+const formatDate = (dateString: string) => {
+  return new Date(dateString).toLocaleDateString('fr-FR', {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const goToReservations = () => {
+  router.push('/reservations')
+}
+
+const goToHome = () => {
+  router.push('/')
+}
+</script>

+ 12 - 1
app_transport_front/src/views/ReservationView.vue

@@ -125,7 +125,10 @@
           </div>
           <form @submit.prevent="submitReservation" class="reservation-form">
             <div class="form-group">
-              <label for="passenger-count">Nombre de passagers</label>
+              <label for="passenger-count">
+                Nombre de passagers 
+                <span class="available-seats">({{ trip?.availableSeats || 0 }} place{{ (trip?.availableSeats || 0) > 1 ? 's' : '' }} disponible{{ (trip?.availableSeats || 0) > 1 ? 's' : '' }})</span>
+              </label>
               <select id="passenger-count" v-model="reservationData.passengerCount" required>
                 <option value="">Sélectionnez le nombre de passagers</option>
                 <option v-for="n in Math.min(trip?.availableSeats || 1, 10)" :key="n" :value="n">
@@ -414,6 +417,7 @@
     :total-amount="calculateTotal()"
     :trip-id="trip?.id || 0"
     :trip-details="getTripDetails()"
+    :payment-method="reservationData.paymentMethod"
     @close="showWorkflowModal = false"
     @success="onReservationSuccess"
   />
@@ -775,6 +779,13 @@ onBeforeUnmount(() => {
 </script>
 
 <style scoped>
+/* Style pour les places disponibles */
+.available-seats {
+  color: #059669;
+  font-weight: 500;
+  font-size: 0.9em;
+}
+
 /* Styles pour Mobile Money */
 .mobile-payment-form {
   margin-top: 2rem;

+ 352 - 0
app_transport_front/src/views/UserDashboardView.vue

@@ -0,0 +1,352 @@
+<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">Mon Espace Voyageur</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">
+              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">
+          <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',
+            ]"
+          >
+            <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-3 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">
+                      Réservations totales
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ stats.totalReservations }}
+                    </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">
+                      Voyages à venir
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ stats.upcomingTrips }}
+                    </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-euro-sign 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">
+                      Total dépensé
+                    </dt>
+                    <dd class="text-lg font-medium text-gray-900">
+                      {{ formatCurrency(stats.totalSpent) }}
+                    </dd>
+                  </dl>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Prochains voyages -->
+        <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">
+              Prochains voyages
+            </h3>
+            <div v-if="upcomingReservations.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 voyage prévu</p>
+              <router-link
+                to="/"
+                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"
+              >
+                Réserver un voyage
+              </router-link>
+            </div>
+            <div v-else class="space-y-4">
+              <div
+                v-for="reservation in upcomingReservations"
+                :key="reservation.id"
+                class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
+              >
+                <div class="flex justify-between items-start">
+                  <div class="flex-1">
+                    <div class="flex items-center space-x-2 mb-2">
+                      <span class="text-lg font-semibold text-gray-900">
+                        {{ reservation.trajet.departAt.station.name }} →
+                        {{ reservation.trajet.arriveAt.station.name }}
+                      </span>
+                      <span
+                        :class="getStatusBadgeClass(reservation.trajet.vehicule.company.name)"
+                        class="px-2 py-1 text-xs font-medium rounded-full"
+                      >
+                      <i class="fas fa-van-shuttle mr-2"></i>
+                        {{ getStatusText(reservation.trajet.vehicule.company.name) }}
+                      </span>
+                      <span
+                        :class="getStatusBadgeClass(reservation.place.isAvailable)"
+                        class="px-2 py-1 text-xs font-medium rounded-full"
+                      >
+                        {{ getStatusText(reservation.place.isAvailable) }} 
+                      </span>
+                    </div>
+                    <div class="text-sm text-gray-600 space-y-1">
+                      <p>
+                        <i class="fas fa-calendar mr-2"></i
+                        >{{ formatDateTime(reservation.trajet.departAt.date) }}
+                      </p>
+                      <p>
+                        <i class="fas fa-chair mr-2"></i
+                        >{{ reservation.place.number }} place(s)
+                      </p>
+                    </div>
+                  </div>
+                  <div class="text-right">
+                    <p class="text-lg font-semibold text-gray-900">
+                      {{ formatCurrency(reservation.trajet.pricePerSeat) }}
+                    </p>
+                    <button
+                      @click="viewReservationDetails(reservation.id)"
+                      class="mt-2 text-sm text-blue-600 hover:text-blue-800"
+                    >
+                      Voir détails
+                    </button>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Historique des réservations -->
+      <div v-if="activeTab === 'history'">
+        <ReservationHistory />
+      </div>
+
+      <!-- Tickets -->
+      <div v-if="activeTab === 'tickets'">
+        <TravelTickets />
+      </div>
+
+      <!-- Reçus -->
+      <div v-if="activeTab === 'receipts'">
+        <ReservationReceipts />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, computed } from "vue";
+import { useRouter } from "vue-router";
+import { useUserStore } from "@/stores/useUserStore";
+import ReservationHistory from "@/components/user/ReservationHistory.vue";
+import TravelTickets from "@/components/user/TravelTickets.vue";
+import ReservationReceipts from "@/components/user/ReservationReceipts.vue";
+import ReservationService  from "@/services/ReservationService";
+import type { Reservation } from "@/types/Reservation";
+
+const router = useRouter();
+const userStore = useUserStore();
+
+const activeTab = ref("overview");
+const reservations = ref<Reservation[]>([]);
+const loading = ref(false);
+
+const tabs = [
+  { id: "overview", name: "Vue d'ensemble", icon: "fas fa-home" },
+  { id: "history", name: "Historique", icon: "fas fa-history" },
+  { id: "tickets", name: "Mes tickets", icon: "fas fa-ticket-alt" },
+  { id: "receipts", name: "Reçus", icon: "fas fa-receipt" },
+];
+
+const stats = computed(() => {
+  const totalReservations = reservations.value.length;
+  const upcomingTrips = reservations.value.filter(
+    (r) => new Date(r.trajet.departAt) < new Date() 
+  ).length;
+  const totalSpent = reservations.value.reduce((sum, r) => sum + r.trajet.pricePerSeat, 0);
+
+  return {
+    totalReservations,
+    upcomingTrips,
+    totalSpent,
+  };
+});
+
+const upcomingReservations = computed(() => {
+  console.log('reser =>', reservations.value)
+  return reservations.value
+  // const test = reservations.value
+  //   .filter(
+  //     (r) => new Date(r.trajet.departAt) > new Date()
+  //   )
+  //   .sort(
+  //     (a, b) =>
+  //       new Date(a.trajet.departAt).getTime() -
+  //       new Date(b.trajet.departAt).getTime()
+  //   )
+  //   .slice(0, 3);
+  //   console.log('test', test)
+  //   return test
+});
+
+const loadUserReservations = async () => {
+  
+  if (!userStore.userId) return;
+
+  try {
+    loading.value = true;
+    // Conversion de userId en nombre pour éviter l'erreur de validation
+    const userId = parseInt(userStore.userId, 10);
+    const response = await ReservationService.getReservationsByUser(userId);
+    
+    reservations.value = response.data.data // Le service renvoie directement le tableau de réservations
+  } catch (error) {
+    console.error("Erreur lors du chargement des réservations:", error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const viewReservationDetails = (reservationId: string) => {
+  activeTab.value = "history";
+  // Logique pour afficher les détails de la réservation
+};
+
+const getStatusBadgeClass = (status: boolean | null) => {
+  switch (status) {
+    case true:
+      return "bg-green-100 text-green-800";
+    case null:
+      return "bg-yellow-100 text-yellow-800";
+    case false:
+      return "bg-red-100 text-red-800";
+    default:
+      return "bg-gray-100 text-gray-800";
+  }
+};
+
+const getStatusText = (status: boolean | null) => {
+  switch (status) {
+    case true:
+      return "Confirmé";
+    case null:
+      return "En attente";
+    case false:
+      return "Annulé";
+    default:
+      return status;
+  }
+};
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat("fr-FR", {
+    style: "currency",
+    currency: "XOF",
+  }).format(amount);
+};
+
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleDateString("fr-FR", {
+    weekday: "long",
+    year: "numeric",
+    month: "long",
+    day: "numeric",
+  });
+};
+
+const formatTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleTimeString("fr-FR", {
+    hour: "2-digit",
+    minute: "2-digit",
+  });
+};
+
+const formatDateTime = (dateTime: string) => {
+  return new Date(dateTime).toLocaleDateString("fr-FR", {
+    weekday: "short",
+    day: "2-digit",
+    month: "short",
+    year: "numeric",
+    hour: "2-digit",
+    minute: "2-digit",
+  });
+};
+
+const logout = () => {
+  userStore.clearUser();
+  router.push("/login");
+};
+
+onMounted(() => {
+  loadUserReservations();
+});
+</script>
+
+<style scoped>
+/* Styles personnalisés si nécessaire */
+</style>

+ 9 - 8
node_app/Dockerfile

@@ -1,9 +1,9 @@
-FROM node:latest
+FROM node:18
 
 # Répertoire de travail
 WORKDIR /usr/src/app
 
-# 1. Installer les dépendances système nécessaires pour compiler bcrypt
+# 1. Installer les dépendances système nécessaires
 RUN apt-get update && \
     apt-get install -y build-essential python3 && \
     rm -rf /var/lib/apt/lists/*
@@ -14,25 +14,26 @@ RUN rm -f package-lock.json
 # Copier les fichiers de dépendances
 COPY package*.json ./
 
-RUN npm install @nestjs/typeorm --save --force
 # Installer les dépendances
-RUN npm install bcrypt --build-from-source --force
 RUN npm install --force
+RUN npm rebuild
 
 # Copier le reste du code (y compris src/)
 COPY . .
+# Vérifier l'installation des dépendances
+RUN npm list --depth=0
 # Regénérer Prisma si utilisé
 RUN npx prisma generate
 # Nettoyage éventuel (optionnel, à mettre *après* la copie du code)
 RUN npm cache clean --force
-# Temporairement ignorer les erreurs TypeScript pour le démarrage
-RUN npm run build || echo "Build completed with warnings"
+# Build de l'application
+RUN npm run build
 
 # Nettoyer les outils de compilation (optionnel pour production)
 RUN apt-get purge -y build-essential python3 && \
     apt-get autoremove -y
 # Exposer le port utilisé
-EXPOSE 3000
+EXPOSE 3001
 
 # Commande de démarrage
-CMD ["npm", "run", "start"]
+CMD ["node", "dist/main.js"]

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 221 - 666
node_app/package-lock.json


+ 9 - 4
node_app/package.json

@@ -12,7 +12,7 @@
     "start:dev": "nest start --watch",
     "dev": "nest start --watch",
     "start:debug": "nest start --debug --watch",
-    "start:prod": "node dist/main",
+    "start:prod": "node dist/main.js",
     "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
     "test": "jest --config=jest.config.js",
     "test:watch": "jest --watch --config=jest.config.js",
@@ -27,12 +27,14 @@
     "@nestjs/common": "^11.0.12",
     "@nestjs/config": "^4.0.0",
     "@nestjs/core": "^11.0.12",
+    "@nestjs/cqrs": "^11.0.3",
     "@nestjs/jwt": "^11.0.0",
     "@nestjs/mapped-types": "^2.1.0",
     "@nestjs/passport": "^11.0.5",
     "@nestjs/platform-express": "^11.0.6",
     "@nestjs/swagger": "^11.1.1",
-    "bcrypt": "^5.1.1",
+    "@prisma/client": "^6.6.0",
+    "argon2": "^0.31.2",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.13.2",
     "dotenv": "^16.4.7",
@@ -42,10 +44,12 @@
     "passport": "^0.7.0",
     "passport-firebase-jwt": "^1.2.1",
     "passport-jwt": "^4.0.1",
+    "qrcode": "^1.5.4",
     "redoc": "^2.4.0",
     "reflect-metadata": "^0.2.2",
     "rxjs": "^7.8.1",
-    "swagger-ui-express": "^5.0.1"
+    "swagger-ui-express": "^5.0.1",
+    "uuid": "^8.3.2"
   },
   "devDependencies": {
     "@eslint/eslintrc": "^3.2.0",
@@ -55,7 +59,6 @@
     "@nestjs/platform-fastify": "^11.0.15",
     "@nestjs/schematics": "^11.0.0",
     "@nestjs/testing": "^11.0.1",
-    "@prisma/client": "^6.6.0",
     "@swc/cli": "^0.6.0",
     "@swc/core": "^1.10.7",
     "@types/bcrypt": "^5.0.2",
@@ -64,7 +67,9 @@
     "@types/node": "^22.13.1",
     "@types/passport": "^1.0.17",
     "@types/passport-jwt": "^4.0.1",
+    "@types/qrcode": "^1.5.5",
     "@types/supertest": "^6.0.2",
+    "@types/uuid": "^10.0.0",
     "eslint": "^9.18.0",
     "eslint-config-prettier": "^10.0.1",
     "eslint-plugin-prettier": "^5.2.2",

+ 8 - 0
node_app/prisma/migrations/20250831071439_add_employee_to_trajet/migration.sql

@@ -0,0 +1,8 @@
+-- AlterTable
+ALTER TABLE `Trajet` ADD COLUMN `employeeId` INTEGER NULL;
+
+-- CreateIndex
+CREATE INDEX `Trajet_employeeId_fkey` ON `Trajet`(`employeeId`);
+
+-- AddForeignKey
+ALTER TABLE `Trajet` ADD CONSTRAINT `Trajet_employeeId_fkey` FOREIGN KEY (`employeeId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

+ 38 - 0
node_app/prisma/migrations/20250831103923_add_cinetpay_fields/migration.sql

@@ -0,0 +1,38 @@
+/*
+  Warnings:
+
+  - A unique constraint covering the columns `[cinetpay_transaction_id]` on the table `Transaction` will be added. If there are existing duplicate values, this will fail.
+  - A unique constraint covering the columns `[cinetpay_payment_token]` on the table `Transaction` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE `Transaction` ADD COLUMN `cinetpay_operator_id` VARCHAR(191) NULL,
+    ADD COLUMN `cinetpay_payment_date` DATETIME(3) NULL,
+    ADD COLUMN `cinetpay_payment_method` VARCHAR(191) NULL,
+    ADD COLUMN `cinetpay_payment_token` VARCHAR(191) NULL,
+    ADD COLUMN `cinetpay_payment_url` VARCHAR(191) NULL,
+    ADD COLUMN `cinetpay_phone_number` VARCHAR(191) NULL,
+    ADD COLUMN `cinetpay_response` JSON NULL,
+    ADD COLUMN `cinetpay_transaction_id` VARCHAR(191) NULL,
+    ADD COLUMN `cinetpay_webhook_data` JSON NULL;
+
+-- CreateIndex
+CREATE UNIQUE INDEX `Transaction_cinetpay_transaction_id_key` ON `Transaction`(`cinetpay_transaction_id`);
+
+-- CreateIndex
+CREATE UNIQUE INDEX `Transaction_cinetpay_payment_token_key` ON `Transaction`(`cinetpay_payment_token`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_cinetpay_transaction_id_idx` ON `Transaction`(`cinetpay_transaction_id`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_cinetpay_payment_token_idx` ON `Transaction`(`cinetpay_payment_token`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_cinetpay_payment_method_idx` ON `Transaction`(`cinetpay_payment_method`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_provider_idx` ON `Transaction`(`provider`);
+
+-- CreateIndex
+CREATE INDEX `Transaction_status_idx` ON `Transaction`(`status`);

+ 11 - 0
node_app/prisma/migrations/20250831104223_add_cinetpay_fields/migration.sql

@@ -0,0 +1,11 @@
+-- DropIndex
+DROP INDEX `Transaction_cinetpay_payment_token_idx` ON `Transaction`;
+
+-- DropIndex
+DROP INDEX `Transaction_cinetpay_payment_token_key` ON `Transaction`;
+
+-- DropIndex
+DROP INDEX `Transaction_cinetpay_transaction_id_idx` ON `Transaction`;
+
+-- DropIndex
+DROP INDEX `Transaction_cinetpay_transaction_id_key` ON `Transaction`;

+ 17 - 0
node_app/prisma/schema.prisma

@@ -80,6 +80,7 @@ model User {
   reservations     Reservation[]    @relation("UserReservations")
   stationCompanies StationCompany[] @relation("UserStationChief")
   transactions     Transaction[]    @relation("UserTransactions")
+  managedTrajets   Trajet[]         @relation("TrajetEmployee") // Trajets dont l'utilisateur est responsable
 
   @@index([companyId], map: "User_companyId_fkey")
 }
@@ -125,15 +126,18 @@ model Trajet {
   arriveStationId Int           // Nouveau champ pour l'ID de la station d'arrivée
   vehiculeId      Int
   companyId       Int?          // Relation directe avec Company (optionnel temporairement)
+  employeeId      Int?          // ID de l'employé responsable du trajet
   pricePerSeat    Float         @default(0.0) // Prix par place
   reservations    Reservation[] @relation("TrajetReservations")
   vehicule        Vehicule      @relation("VehiculeTrajets", fields: [vehiculeId], references: [id])
   company         Company?      @relation("CompanyTrajets", fields: [companyId], references: [id]) // Relation directe avec Company
+  employee        User?         @relation("TrajetEmployee", fields: [employeeId], references: [id]) // Relation vers l'employé responsable
   departStation   Station       @relation("TrajetsDepartingFrom", fields: [departStationId], references: [id]) // Relation vers la station de départ
   arriveStation   Station       @relation("TrajetsArrivingAt", fields: [arriveStationId], references: [id])    // Relation vers la station d'arrivée
 
   @@index([vehiculeId], map: "Trajet_vehiculeId_fkey")
   @@index([companyId], map: "Trajet_companyId_fkey") // Index pour la relation avec Company
+  @@index([employeeId], map: "Trajet_employeeId_fkey") // Index pour la relation avec l'employé
   @@index([departStationId], map: "Trajet_departStationId_fkey") // Index pour la nouvelle clé étrangère
   @@index([arriveStationId], map: "Trajet_arriveStationId_fkey") // Index pour la nouvelle clé étrangère
 }
@@ -175,6 +179,16 @@ model Transaction {
   payment_type            String?     // "invoice", "dmp", "softpay"
   paydunya_response       Json?       // Réponse complète de PayDunya
   webhook_data            Json?       // Données reçues via webhook
+  // Champs CinetPay
+  cinetpay_transaction_id String?
+  cinetpay_payment_token  String?
+  cinetpay_payment_url    String?
+  cinetpay_response       Json?       // Réponse complète de CinetPay
+  cinetpay_webhook_data   Json?       // Données webhook CinetPay
+  cinetpay_payment_method String?     // "mobile_money", "credit_card", "wallet"
+  cinetpay_operator_id    String?     // ID de l'opérateur (Orange, MTN, etc.)
+  cinetpay_phone_number   String?     // Numéro de téléphone utilisé
+  cinetpay_payment_date   DateTime?   // Date de paiement confirmée
   reservation             Reservation @relation("ReservationTransaction", fields: [reservationId], references: [id])
   user                    User        @relation("UserTransactions", fields: [userId], references: [id])
 
@@ -183,6 +197,9 @@ model Transaction {
   @@index([paydunya_transaction_id], map: "Transaction_paydunya_transaction_id_idx")
   @@index([payment_method], map: "Transaction_payment_method_idx")
   @@index([payment_type], map: "Transaction_payment_type_idx")
+  @@index([cinetpay_payment_method], map: "Transaction_cinetpay_payment_method_idx")
+  @@index([provider], map: "Transaction_provider_idx")
+  @@index([status], map: "Transaction_status_idx")
 }
 
 model Qrcode {

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

@@ -32,6 +32,7 @@ import { FindAllTrajetsUseCase } from './application/use-cases/trajet/find-all-t
 import { FindTrajetByIdUseCase } from './application/use-cases/trajet/find-trajet-by-id.use-case'; // Ajoutez cette ligne
 import { FindTrajetsByCompanyUseCase } from './application/use-cases/trajet/find-trajets-by-company.use-case';
 import { DeleteTrajetUseCase } from './application/use-cases/trajet/delete-trajet.use-case'; // Ajoutez cette ligne
+import { SearchTrajetsUseCase } from './application/use-cases/trajet/search-trajets.use-case';
 import { TrajetMapper } from './infrastructure/mappers/trajet.mapper';
 import { TrajetRepository } from './infrastructure/database/repositories/trajet.repository';
 import { DebugMiddleware } from './middleware/debug.middleware';
@@ -56,7 +57,6 @@ import { FindOneVehicleByCompanyUseCase } from './application/use-cases/vehicle/
 import { StationMapper } from './infrastructure/mappers/station.mapper'; // Ajoutez cette ligne
 import { StationController } from './presentation/controllers/station.controller';
 import { TransactionController } from './presentation/controllers/transaction.controller';
-import { QrcodeController } from './presentation/controllers/qrcode.controller';
 import { StationCompanyController } from './presentation/controllers/station-company.controller';
 import { StationRepository } from './infrastructure/database/repositories/station.repository';
 import { FindAllStationsUseCase } from './application/use-cases/station/find-all-stations.use-case';
@@ -76,16 +76,20 @@ import { DeleteStationCompanyUseCase } from './application/use-cases/station-com
 import { StationCompanyRepository } from './infrastructure/database/repositories/station-company.repository';
 import { StationCompanyMapper } from './infrastructure/mappers/station-company.mapper';
 import { ReservationModule } from './presentation/modules/reservation.module';
+import { QrcodeModule } from './presentation/modules/qrcode.module';
+import { CqrsModule } from '@nestjs/cqrs';
 
 @Module({
   imports: [
     ConfigModule.forRoot({
       isGlobal: true,
     }),
+    CqrsModule.forRoot(),
     PrismaModule,
     ServicesModule,
     PaydunyaModule,
     ReservationModule,
+    QrcodeModule,
   ],
   controllers: [
     UserController,
@@ -97,7 +101,6 @@ import { ReservationModule } from './presentation/modules/reservation.module';
     AuthController,           // Nouveau
     StationController,           // Nouveau
     TransactionController,       // Nouveau
-    QrcodeController,           // Nouveau
     StationCompanyController,   // Nouveau
   ],
   providers: [
@@ -168,6 +171,7 @@ import { ReservationModule } from './presentation/modules/reservation.module';
     FindTrajetByIdUseCase,
     FindTrajetsByCompanyUseCase,
     DeleteTrajetUseCase,
+    SearchTrajetsUseCase,
     TrajetMapper,
     {
       provide: 'TrajetRepository',

+ 9 - 0
node_app/src/application/dto/create-trajet.dto.ts

@@ -64,4 +64,13 @@ export class CreateTrajetDto {
   @IsNumber()
   @IsOptional()
   companyId?: number;
+
+  @ApiProperty({
+    description: 'ID de l\'employé responsable du trajet (optionnel)',
+    example: 1,
+    required: false,
+  })
+  @IsNumber()
+  @IsOptional()
+  employeeId?: number;
 }

+ 61 - 0
node_app/src/application/dto/responses/reservation-details-response.dto.ts

@@ -0,0 +1,61 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { ReservationResponseDto } from './reservation-response.dto';
+import { StationResponseDto } from './station-response.dto';
+import { VehiculeResponseDto } from './vehicule-response.dto';
+import { CompanyResponseDto } from './company-response.dto';
+import { PlaceResponseDto } from './place-response.dto';
+import { TrajetResponseDto } from './trajet-response.dto';
+import { QrcodeResponseDto } from './qrcode-response.dto';
+
+export class ReservationDetailsResponseDto {
+  @ApiProperty({
+    description: 'Informations de la réservation',
+    type: ReservationResponseDto,
+  })
+  reservation: ReservationResponseDto;
+
+  @ApiProperty({
+    description: 'Informations du trajet',
+    type: TrajetResponseDto,
+  })
+  trajet: TrajetResponseDto;
+
+  @ApiProperty({
+    description: 'Informations du véhicule',
+    type: VehiculeResponseDto,
+  })
+  vehicule: VehiculeResponseDto;
+
+  @ApiProperty({
+    description: 'Informations de la station de départ',
+    type: StationResponseDto,
+  })
+  departStation: StationResponseDto;
+
+  @ApiProperty({
+    description: "Informations de la station d'arrivée",
+    type: StationResponseDto,
+  })
+  arriveStation: StationResponseDto;
+
+  @ApiProperty({ description: 'Prix du trajet par place', type: Number })
+  price: number;
+
+  @ApiProperty({
+    description: 'Informations de la compagnie',
+    type: CompanyResponseDto,
+  })
+  company: CompanyResponseDto;
+
+  @ApiProperty({
+    description: 'Informations de la place réservée',
+    type: PlaceResponseDto,
+  })
+  place: PlaceResponseDto;
+
+  @ApiProperty({
+    description: 'QR code associé à la réservation',
+    type: QrcodeResponseDto,
+  })
+  qrcode?: QrcodeResponseDto | null;
+}

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

@@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
 import { VehiculeResponseDto } from './vehicule-response.dto';
 import { ReservationResponseDto } from './reservation-response.dto';
 import { StationResponseDto } from './station-response.dto'; // Assurez-vous que ce DTO existe et est importé
+import { UserResponseDto } from './user-response.dto';
 
 export class TrajetResponseDto {
   @ApiProperty()
@@ -25,6 +26,9 @@ export class TrajetResponseDto {
   @ApiProperty({ description: "ID de la compagnie (optionnel)", required: false })
   companyId?: number;
 
+  @ApiProperty({ description: "ID de l'employé responsable du trajet (optionnel)", required: false })
+  employeeId?: number;
+
   @ApiProperty({ description: "Prix par place en FCFA" })
   pricePerSeat: number;
 
@@ -39,4 +43,10 @@ export class TrajetResponseDto {
 
   @ApiProperty({ type: () => StationResponseDto, required: false, description: "Détails de la station d'arrivée" })
   arriveStation?: StationResponseDto;
+
+  @ApiProperty({ type: () => UserResponseDto, required: false, description: "Employé responsable du trajet" })
+  employee?: UserResponseDto;
+
+  @ApiProperty({ description: "Nombre de places disponibles dans le véhicule pour ce trajet" })
+  availableSeats: number;
 }

+ 31 - 0
node_app/src/application/handlers/reservation-created.handler.ts

@@ -0,0 +1,31 @@
+import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
+import { Inject } from '@nestjs/common';
+import { ReservationCreatedEvent } from '../../domain/events/reservation-created.event';
+import { GenerateQrcodeUseCase } from '../use-cases/qrcode/generate-qrcode.use-case';
+
+@EventsHandler(ReservationCreatedEvent)
+export class ReservationCreatedHandler
+  implements IEventHandler<ReservationCreatedEvent>
+{
+  constructor(private readonly generateQrcodeUseCase: GenerateQrcodeUseCase) {}
+
+  async handle(event: ReservationCreatedEvent) {
+    console.log('Événement ReservationCreated reçu:', event.reservationId);
+
+    try {
+      // Générer automatiquement le QR code pour la réservation
+      await this.generateQrcodeUseCase.execute({
+        reservationId: event.reservationId,
+      });
+
+      console.log(
+        `QR code généré avec succès pour la réservation ${event.reservationId}`,
+      );
+    } catch (error) {
+      console.error(
+        `Erreur lors de la génération du QR code pour la réservation ${event.reservationId}:`,
+        error,
+      );
+    }
+  }
+}

+ 7 - 1
node_app/src/application/use-cases/create-user.use-case.ts

@@ -1,4 +1,4 @@
-import { Injectable, Inject } from '@nestjs/common';
+import { Injectable, Inject, ConflictException } from '@nestjs/common';
 import { IUserRepository } from '../interfaces/i-user.repository';
 import { CreateUserDto } from '../dto/create-user.dto';
 import { UserMapper } from 'src/infrastructure/mappers/user.mapper';
@@ -16,6 +16,12 @@ export class CreateUserUseCase {
   ) {}
 
   async execute(createUserDto: CreateUserDto) {
+    // Vérifier si l'email existe déjà
+    const existingUser = await this.userRepository.findByEmail(createUserDto.email);
+    if (existingUser) {
+      throw new ConflictException('Un utilisateur avec cet email existe déjà');
+    }
+
     // Hasher le mot de passe avant de créer l'utilisateur
     // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
     const hashedPassword = await this.passwordHashService.hash(

+ 43 - 0
node_app/src/application/use-cases/qrcode/find-qrcode.use-case.ts

@@ -0,0 +1,43 @@
+import { Injectable, NotFoundException, Inject } from '@nestjs/common';
+import { IQrcodeRepository } from '../../../domain/repositories/qrcode.repository.interface';
+import { Qrcode } from '../../../domain/entities/qrcode.entity';
+
+export interface FindQrcodeByIdRequest {
+  id: string;
+}
+
+export interface FindQrcodeByReservationRequest {
+  reservationId: number;
+}
+
+export interface FindQrcodeResponse {
+  qrcode: Qrcode;
+}
+
+@Injectable()
+export class FindQrcodeUseCase {
+  constructor(
+    @Inject('IQrcodeRepository')
+    private readonly qrcodeRepository: IQrcodeRepository
+  ) {}
+
+  async findById(request: FindQrcodeByIdRequest): Promise<FindQrcodeResponse> {
+    const qrcode = await this.qrcodeRepository.findById(request.id);
+    
+    if (!qrcode) {
+      throw new NotFoundException(`QR code avec l'ID ${request.id} non trouvé`);
+    }
+
+    return { qrcode };
+  }
+
+  async findByReservationId(request: FindQrcodeByReservationRequest): Promise<FindQrcodeResponse> {
+    const qrcode = await this.qrcodeRepository.findByReservationId(request.reservationId);
+    
+    if (!qrcode) {
+      throw new NotFoundException(`QR code pour la réservation ${request.reservationId} non trouvé`);
+    }
+
+    return { qrcode };
+  }
+}

+ 56 - 0
node_app/src/application/use-cases/qrcode/generate-qrcode.use-case.ts

@@ -0,0 +1,56 @@
+import { Injectable, NotFoundException, Inject } from '@nestjs/common';
+import { randomUUID } from 'crypto';
+import { IQrcodeRepository } from '../../../domain/repositories/qrcode.repository.interface';
+import { IReservationRepository } from '../../../domain/repositories/reservation.repository.interface';
+import { Qrcode } from '../../../domain/entities/qrcode.entity';
+
+export interface GenerateQrcodeRequest {
+  reservationId: number;
+}
+
+export interface GenerateQrcodeResponse {
+  qrcode: Qrcode;
+}
+
+@Injectable()
+export class GenerateQrcodeUseCase {
+  constructor(
+    @Inject('IQrcodeRepository')
+    private readonly qrcodeRepository: IQrcodeRepository,
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: IReservationRepository,
+  ) {}
+
+  async execute(request: GenerateQrcodeRequest): Promise<GenerateQrcodeResponse> {
+    // Vérifier que la réservation existe
+    const reservation = await this.reservationRepository.findById(request.reservationId);
+    if (!reservation) {
+      throw new NotFoundException(`Réservation avec l'ID ${request.reservationId} non trouvée`);
+    }
+
+    // Vérifier qu'un QR code n'existe pas déjà pour cette réservation
+    const existingQrcode = await this.qrcodeRepository.findByReservationId(request.reservationId);
+    if (existingQrcode) {
+      return { qrcode: existingQrcode };
+    }
+
+    // Générer un nouvel ID unique
+    const qrcodeId = randomUUID();
+    
+    // Créer la valeur du QR code
+    const qrcodeValue = Qrcode.generateValue(qrcodeId, request.reservationId);
+
+    // Créer l'entité QR code
+    const qrcode = new Qrcode({
+      id: qrcodeId,
+      reservationId: request.reservationId,
+      value: qrcodeValue,
+      createdAt: new Date(),
+    });
+
+    // Sauvegarder le QR code
+    const savedQrcode = await this.qrcodeRepository.create(qrcode);
+
+    return { qrcode: savedQrcode };
+  }
+}

+ 52 - 0
node_app/src/application/use-cases/qrcode/scan-qrcode.use-case.ts

@@ -0,0 +1,52 @@
+import { Injectable, NotFoundException, BadRequestException, Inject } from '@nestjs/common';
+import { IQrcodeRepository } from '../../../domain/repositories/qrcode.repository.interface';
+import { Qrcode } from '../../../domain/entities/qrcode.entity';
+
+export interface ScanQrcodeRequest {
+  value: string;
+}
+
+export interface ScanQrcodeResponse {
+  qrcode: Qrcode;
+  message: string;
+}
+
+@Injectable()
+export class ScanQrcodeUseCase {
+  constructor(
+    @Inject('IQrcodeRepository')
+    private readonly qrcodeRepository: IQrcodeRepository
+  ) {}
+
+  async execute(request: ScanQrcodeRequest): Promise<ScanQrcodeResponse> {
+    // Valider le format de la valeur du QR code
+    const parsedValue = Qrcode.parseValue(request.value);
+    if (!parsedValue) {
+      throw new BadRequestException('Format de QR code invalide');
+    }
+
+    // Trouver le QR code par sa valeur
+    const qrcode = await this.qrcodeRepository.findByValue(request.value);
+    if (!qrcode) {
+      throw new NotFoundException('QR code non trouvé ou invalide');
+    }
+
+    // Vérifier si le QR code a déjà été scanné
+    if (qrcode.isScanned()) {
+      throw new BadRequestException('QR code déjà scanné');
+    }
+
+    // Marquer le QR code comme scanné
+    qrcode.markAsScanned();
+
+    // Sauvegarder les modifications
+    const updatedQrcode = await this.qrcodeRepository.update(qrcode.id, {
+      scannedAt: qrcode.scannedAt,
+    });
+
+    return {
+      qrcode: updatedQrcode,
+      message: 'QR code scanné avec succès',
+    };
+  }
+}

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

@@ -1,7 +1,9 @@
 import { Injectable, Inject } from '@nestjs/common';
+import { EventBus } from '@nestjs/cqrs';
 import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
 import { CreateReservationDto } from '../../dto/create-reservation.dto';
 import { ReservationMapper } from 'src/infrastructure/mappers/reservation.mapper';
+import { ReservationCreatedEvent } from '../../../domain/events/reservation-created.event';
 
 @Injectable()
 export class CreateReservationUseCase {
@@ -9,11 +11,20 @@ export class CreateReservationUseCase {
     @Inject('ReservationRepository')
     private readonly reservationRepository: ReservationRepository,
     private readonly reservationMapper: ReservationMapper,
+    private readonly eventBus: EventBus,
   ) {}
 
   async execute(createReservationDto: CreateReservationDto) {
     const createReservation =
       this.reservationMapper.fromDtoToDomain(createReservationDto);
-    return this.reservationRepository.create(createReservation);
+    const reservation =
+      await this.reservationRepository.create(createReservation);
+
+    //Publier l'événement pour générer automatiquement le QR code
+    this.eventBus.publish(
+      new ReservationCreatedEvent(reservation.id, reservation),
+    );
+
+    return reservation;
   }
 }

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

@@ -0,0 +1,14 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+
+@Injectable()
+export class FindReservationsByUserUseCase {
+  constructor(
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: ReservationRepository,
+  ) {}
+
+  async execute(userId: number, page = 1, limit = 10) {
+    return this.reservationRepository.findByUserId(userId, page, limit);
+  }
+}

+ 31 - 0
node_app/src/application/use-cases/trajet/search-trajets.use-case.ts

@@ -0,0 +1,31 @@
+import { Injectable, Inject } from '@nestjs/common';
+import { TrajetRepository } from '../../../infrastructure/database/repositories/trajet.repository';
+import { TrajetMapper } from '../../../infrastructure/mappers/trajet.mapper';
+import { TrajetResponseDto } from '../../dto/responses/trajet-response.dto';
+import { PaginatedResponseDto } from '../../dto/responses/paginated-response.dto';
+import { Trajet } from '../../../domain/entities/trajet.entity';
+
+@Injectable()
+export class SearchTrajetsUseCase {
+  constructor(
+    @Inject('TrajetRepository')
+    private readonly trajetRepository: TrajetRepository,
+    private readonly trajetMapper: TrajetMapper,
+  ) {}
+
+  async execute(
+    query: string,
+    page: number = 1,
+    limit: number = 10,
+  ): Promise<PaginatedResponseDto<TrajetResponseDto>> {
+    // Pour l'instant, on utilise une recherche simple basée sur les noms des stations
+    // Dans une vraie implémentation, on pourrait utiliser une recherche full-text
+    const { data, total } = await this.trajetRepository.searchByStations(query, page, limit);
+    
+    const trajetDtos = data.map((trajet: Trajet) =>
+      this.trajetMapper.toResponseDto(trajet),
+    );
+    
+    return new PaginatedResponseDto(trajetDtos, total, page, limit);
+  }
+}

+ 1 - 1
node_app/src/config/auth.config.ts

@@ -3,5 +3,5 @@ import { registerAs } from '@nestjs/config';
 export default registerAs('auth', () => ({
   jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
   jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',
-  bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS || '10', 10),
+  // Argon2 utilise ses propres paramètres par défaut
 }));

+ 39 - 0
node_app/src/domain/entities/qrcode.entity.ts

@@ -11,4 +11,43 @@ export class Qrcode {
   constructor(partial: Partial<Qrcode>) {
     Object.assign(this, partial);
   }
+
+  /**
+   * Vérifie si le QR code a été scanné
+   */
+  isScanned(): boolean {
+    return this.scannedAt !== undefined && this.scannedAt !== null;
+  }
+
+  /**
+   * Marque le QR code comme scanné
+   */
+  markAsScanned(): void {
+    if (this.isScanned()) {
+      throw new Error('QR code déjà scanné');
+    }
+    this.scannedAt = new Date();
+  }
+
+  /**
+   * Génère la valeur du QR code à partir de l'ID et de l'ID de réservation
+   */
+  static generateValue(id: string, reservationId: number): string {
+    return `QR_${id}_${reservationId}`;
+  }
+
+  /**
+   * Parse une valeur de QR code pour extraire l'ID et l'ID de réservation
+   */
+  static parseValue(value: string): { id: string; reservationId: number } | null {
+    const match = value.match(/^QR_([a-f0-9-]+)_(\d+)$/);
+    if (!match) {
+      return null;
+    }
+
+    return {
+      id: match[1],
+      reservationId: parseInt(match[2], 10),
+    };
+  }
 }

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

@@ -1,6 +1,7 @@
 import { Vehicule } from './vehicule.entity';
 import { Reservation } from './reservation.entity';
 import { Station } from './station.entity'; // Assurez-vous que l'entité Station est importée
+import { User } from './user.entity'; // Import pour l'employé responsable
 
 export class Trajet {
   readonly id: number;
@@ -10,12 +11,15 @@ export class Trajet {
   arriveStationId: number; // Au lieu de arrive: string
   vehiculeId: number; // vehiculeId est conservé
   companyId?: number; // Relation directe avec Company (optionnel)
+  employeeId?: number; // ID de l'employé responsable du trajet
   pricePerSeat: number; // Prix par place
   // Relations
   vehicule?: Vehicule; // L'objet Vehicule peut être chargé optionnellement
+  employee?: User; // Employé responsable du trajet
   reservations?: Reservation[];
   departStation?: Station; // Relation optionnelle vers l'entité Station de départ
   arriveStation?: Station; // Relation optionnelle vers l'entité Station d'arrivée
+  availableSeats?: number; // Nombre de places disponibles calculé
 
   constructor(partial: Partial<Trajet>) {
     Object.assign(this, partial);

+ 6 - 0
node_app/src/domain/events/reservation-created.event.ts

@@ -0,0 +1,6 @@
+export class ReservationCreatedEvent {
+  constructor(
+    public readonly reservationId: number,
+    public readonly reservation: any,
+  ) {}
+}

+ 33 - 0
node_app/src/domain/repositories/qrcode.repository.interface.ts

@@ -0,0 +1,33 @@
+import { Qrcode } from '../entities/qrcode.entity';
+
+export interface IQrcodeRepository {
+  /**
+   * Crée un nouveau QR code
+   */
+  create(qrcode: Qrcode): Promise<Qrcode>;
+
+  /**
+   * Trouve un QR code par son ID
+   */
+  findById(id: string): Promise<Qrcode | null>;
+
+  /**
+   * Trouve un QR code par l'ID de réservation
+   */
+  findByReservationId(reservationId: number): Promise<Qrcode | null>;
+
+  /**
+   * Trouve un QR code par sa valeur
+   */
+  findByValue(value: string): Promise<Qrcode | null>;
+
+  /**
+   * Met à jour un QR code
+   */
+  update(id: string, qrcode: Partial<Qrcode>): Promise<Qrcode>;
+
+  /**
+   * Supprime un QR code
+   */
+  delete(id: string): Promise<void>;
+}

+ 10 - 0
node_app/src/domain/repositories/reservation.repository.interface.ts

@@ -0,0 +1,10 @@
+import { Reservation } from '../entities/reservation.entity';
+
+export interface IReservationRepository {
+  findAll(page?: number, limit?: number): Promise<{ data: Reservation[]; total: number }>;
+  findById(id: number): Promise<Reservation | null>;
+  create(data: Reservation): Promise<Reservation>;
+  update(id: number, entity: Reservation): Promise<Reservation>;
+  delete(id: number): Promise<Reservation | null>;
+  findByUserId(userId: number, page?: number, limit?: number): Promise<{ data: Reservation[]; total: number }>;
+}

+ 88 - 3
node_app/src/infrastructure/database/repositories/reservation.repository.ts

@@ -7,7 +7,7 @@ import { IBaseRepository } from './BaseRepository';
 @Injectable()
 export class ReservationRepository implements IBaseRepository<Reservation> {
   constructor(
-    private prisma: PrismaService,
+    private readonly prisma: PrismaService,
     private readonly reservationMapper: ReservationMapper,
   ) {}
   async findAll(
@@ -34,7 +34,14 @@ export class ReservationRepository implements IBaseRepository<Reservation> {
           user: true,
           trajet: {
             include: {
-              vehicule: true,
+              vehicule: {
+                include: {
+                  company: true,
+                  driver: true,
+                },
+              },
+              departStation: true,
+              arriveStation: true,
             },
           },
         },
@@ -56,6 +63,30 @@ export class ReservationRepository implements IBaseRepository<Reservation> {
   async findById(id: number): Promise<Reservation | null> {
     const prismaReservation = await this.prisma.reservation.findUnique({
       where: { id },
+      include: {
+        place: {
+          include: {
+            vehicule: {
+              include: {
+                company: true,
+                driver: true,
+              },
+            },
+          },
+        },
+        user: true,
+        trajet: {
+          include: {
+            vehicule: {
+              include: {
+                company: true,
+              },
+            },
+            departStation: true,
+            arriveStation: true,
+          },
+        },
+      },
     });
     return prismaReservation
       ? this.reservationMapper.toDomain(prismaReservation)
@@ -96,7 +127,9 @@ export class ReservationRepository implements IBaseRepository<Reservation> {
       },
     });
 
-    return this.reservationMapper.toDomain(createdReservation);
+    const reservation = this.reservationMapper.toDomain(createdReservation);
+
+    return reservation;
   }
 
   async update(id: number, entity: Reservation): Promise<Reservation> {
@@ -142,4 +175,56 @@ export class ReservationRepository implements IBaseRepository<Reservation> {
 
     return this.reservationMapper.toDomain(updatedReservation);
   }
+
+  async findByUserId(
+    userId: number,
+    page = 1,
+    limit = 10,
+  ): Promise<{ data: Reservation[]; total: number }> {
+    const skip = (page - 1) * limit;
+
+    const [data, total] = await Promise.all([
+      await this.prisma.reservation.findMany({
+        where: { userId },
+        skip, // pagination : combien sauter
+        take: limit, // pagination : combien récupérer
+        include: {
+          place: {
+            include: {
+              vehicule: {
+                include: {
+                  company: true,
+                  driver: true,
+                },
+              },
+            },
+          },
+          user: true,
+          trajet: {
+            include: {
+              vehicule: {
+                include: {
+                  company: true,
+                  driver: true,
+                },
+              },
+              departStation: true,
+              arriveStation: true,
+            },
+          },
+        },
+      }),
+
+      this.prisma.reservation.count({
+        where: { userId },
+      }),
+    ]);
+
+    return {
+      data: data.map((reservation) =>
+        this.reservationMapper.toDomain(reservation),
+      ),
+      total,
+    };
+  }
 }

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

@@ -14,6 +14,24 @@ export class TrajetRepository implements IBaseRepository<Trajet> {
   async findById(id: number): Promise<Trajet | null> {
     const prismaTrajet = await this.prisma.trajet.findUnique({
       where: { id },
+      include: {
+        departStation: true,
+        arriveStation: true,
+        vehicule: {
+          include: {
+            company: true,
+            driver: true,
+            places: true,
+          },
+        },
+        reservations: {
+          include: {
+            user: true,
+            place: true,
+          },
+        },
+        employee: true,
+      },
     });
     return prismaTrajet ? this.trajetMapper.toDomain(prismaTrajet) : null;
   }
@@ -94,6 +112,8 @@ export class TrajetRepository implements IBaseRepository<Trajet> {
         skip,
         take: limit,
         include: {
+          departStation: true,
+          arriveStation: true,
           vehicule: {
             include: {
               company: true,
@@ -166,6 +186,182 @@ export class TrajetRepository implements IBaseRepository<Trajet> {
     };
   }
 
+  // Fonction utilitaire pour normaliser les chaînes de recherche
+  private normalizeSearchString(str: string): string {
+    return str
+      .toLowerCase()
+      .normalize('NFD')
+      .replace(/[\u0300-\u036f]/g, '') // Supprime les accents
+      .replace(/[-\s]+/g, ' ') // Normalise les tirets et espaces
+      .trim();
+  }
+
+  private async getStationIdsByNames(query: string): Promise<number[]> {
+    const queryLower = query.toLowerCase();
+    const stationIds: number[] = [];
+    
+    // Rechercher toutes les stations qui correspondent aux termes de la requête
+    const stations = await this.prisma.station.findMany({
+      select: { id: true, name: true },
+    });
+    
+    for (const station of stations) {
+      const stationNameLower = station.name.toLowerCase();
+      
+      // Vérifier si le nom de la station contient des mots de la requête
+      const queryWords = queryLower.split(/\s+/).filter(word => word.length > 2);
+      const stationWords = stationNameLower.split(/\s+/);
+      
+      let matchCount = 0;
+      for (const queryWord of queryWords) {
+        for (const stationWord of stationWords) {
+          if (stationWord.includes(queryWord) || queryWord.includes(stationWord)) {
+            matchCount++;
+            break;
+          }
+        }
+      }
+      
+      // Si au moins un mot correspond, ajouter l'ID de la station
+      if (matchCount > 0) {
+        stationIds.push(station.id);
+      }
+    }
+    
+    return stationIds;
+  }
+
+  private async buildCombinedSearchConditions(query: string, normalizedQuery: string, searchTerms: string[]) {
+    const conditions = [];
+    
+    // Obtenir les IDs des stations correspondant à la requête
+    const stationIds = await this.getStationIdsByNames(query);
+    
+    if (stationIds.length > 0) {
+      // Recherche par IDs de stations comme départ ou arrivée
+      for (const stationId of stationIds) {
+        conditions.push({
+          departStationId: stationId,
+        });
+        conditions.push({
+          arriveStationId: stationId,
+        });
+      }
+      
+      // Recherche combinée pour trajets entre les stations trouvées
+      if (stationIds.length >= 2) {
+        for (let i = 0; i < stationIds.length - 1; i++) {
+          for (let j = i + 1; j < stationIds.length; j++) {
+            const stationId1 = stationIds[i];
+            const stationId2 = stationIds[j];
+            
+            // Trajet de station1 vers station2
+            conditions.push({
+              AND: [
+                { departStationId: stationId1 },
+                { arriveStationId: stationId2 },
+              ],
+            });
+            
+            // Trajet de station2 vers station1 (ordre inverse)
+            conditions.push({
+              AND: [
+                { departStationId: stationId2 },
+                { arriveStationId: stationId1 },
+              ],
+            });
+          }
+        }
+      }
+    }
+    
+    return conditions;
+  }
+
+  async searchByStations(
+    query: string,
+    page = 1,
+    limit = 10,
+  ): Promise<{ data: Trajet[]; total: number }> {
+    const skip = (page - 1) * limit;
+    const normalizedQuery = this.normalizeSearchString(query);
+    const searchTerms = normalizedQuery.split(' ').filter(term => term.length > 0);
+
+    // Construire les conditions de recherche pour les stations avec recherche insensible aux accents
+    const searchConditions = {
+      OR: [
+        // Recherche exacte avec la requête originale
+        {
+          departStation: {
+            name: {
+              contains: query,
+            },
+          },
+        },
+        {
+          arriveStation: {
+            name: {
+              contains: query,
+            },
+          },
+        },
+        // Recherche normalisée (sans accents)
+        {
+          departStation: {
+            name: {
+              contains: normalizedQuery,
+            },
+          },
+        },
+        {
+          arriveStation: {
+            name: {
+              contains: normalizedQuery,
+            },
+          },
+        },
+        // Recherche combinée intelligente pour "station1 vers station2" ou "station1 station2"
+        ...(await this.buildCombinedSearchConditions(query, normalizedQuery, searchTerms)),
+      ],
+    };
+
+    const [data, total] = await Promise.all([
+      this.prisma.trajet.findMany({
+        where: searchConditions,
+        skip,
+        take: limit,
+        include: {
+          departStation: true,
+          arriveStation: true,
+          vehicule: {
+            include: {
+              company: true,
+              driver: true,
+              places: true,
+            },
+          },
+          reservations: {
+            include: {
+              user: true,
+              place: true,
+            },
+          },
+        },
+        orderBy: {
+          departAt: 'asc',
+        },
+      }),
+      this.prisma.trajet.count({
+        where: searchConditions,
+      }),
+    ]);
+
+    return {
+      data: data.map((trajet) => this.trajetMapper.toDomain(trajet)),
+      total,
+    };
+  }
+
   delete(id: number): Promise<Trajet | null> {
     throw new Error('Method not implemented.' + id);
   }

+ 6 - 2
node_app/src/infrastructure/database/repositories/transaction.repository.ts

@@ -52,7 +52,7 @@ export class TransactionRepository { // implements IBaseRepository<Transaction>
   async create(data: Transaction): Promise<Transaction> {
     const prismaData = this.transactionMapper.toPersistence(data);
     // Exclude userId, reservationId and JSON fields from prismaData when using connect relations
-    const { userId, reservationId, metadata, paydunya_response, webhook_data, ...dataWithoutRelations } = prismaData;
+    const { userId, reservationId, metadata, paydunya_response, webhook_data, cinetpay_response, cinetpay_webhook_data, ...dataWithoutRelations } = prismaData;
     const prismaTransaction = await this.prisma.transaction.create({
       data: {
         ...dataWithoutRelations, // Champs directs comme amount, currency, status, etc.
@@ -60,6 +60,8 @@ export class TransactionRepository { // implements IBaseRepository<Transaction>
         metadata: metadata as any,
         paydunya_response: paydunya_response as any,
         webhook_data: webhook_data as any,
+        cinetpay_response: cinetpay_response as any,
+        cinetpay_webhook_data: cinetpay_webhook_data as any,
         // Assurez-vous que le mapper prépare correctement les données pour les relations
         // ou ajustez ici pour les `connect` si le mapper ne le fait pas.
         user: { connect: { id: data.userId } },
@@ -76,7 +78,7 @@ export class TransactionRepository { // implements IBaseRepository<Transaction>
   async update(id: string, entity: Transaction): Promise<Transaction> {
     const prismaData = this.transactionMapper.toPersistence(entity);
     // Exclude userId and reservationId from prismaData when using connect relations
-    const { userId, reservationId, metadata, paydunya_response, webhook_data, ...dataWithoutRelations } = prismaData;
+    const { userId, reservationId, metadata, paydunya_response, webhook_data, cinetpay_response, cinetpay_webhook_data, ...dataWithoutRelations } = prismaData;
     const prismaTransaction = await this.prisma.transaction.update({
       where: { id },
       data: {
@@ -85,6 +87,8 @@ export class TransactionRepository { // implements IBaseRepository<Transaction>
         metadata: metadata as any,
         paydunya_response: paydunya_response as any,
         webhook_data: webhook_data as any,
+        cinetpay_response: cinetpay_response as any,
+        cinetpay_webhook_data: cinetpay_webhook_data as any,
         user: entity.userId ? { connect: { id: entity.userId } } : undefined,
         reservation: entity.reservationId ? { connect: { id: entity.reservationId } } : undefined,
       },

+ 2 - 0
node_app/src/infrastructure/mappers/place.mapper.ts

@@ -1,7 +1,9 @@
+import { Injectable } from '@nestjs/common';
 import { Place } from 'src/domain/entities/place.entity';
 import { IPrismaMapper } from './prisma-mapper.interface';
 import { Place as PrismaPlace } from '@prisma/client';
 
+@Injectable()
 export class PlaceMapper implements IPrismaMapper<PrismaPlace, Place> {
   toDomain(prismaPlace: PrismaPlace): Place {
     return new Place({

+ 83 - 6
node_app/src/infrastructure/mappers/reservation.mapper.ts

@@ -1,21 +1,98 @@
+import { Trajet } from '@prisma/client';
 import { IPrismaMapper } from './prisma-mapper.interface';
-import { Reservation as PrismaReservation } from '@prisma/client';
 import { Reservation } from 'src/domain/entities/reservation.entity';
+import { User, UserRole } from 'src/domain/entities/user.entity';
 
-export class ReservationMapper
-  implements IPrismaMapper<PrismaReservation, Reservation>
-{
-  toDomain(prismaReservation: PrismaReservation): Reservation {
+export class ReservationMapper implements IPrismaMapper<any, Reservation> {
+  toDomain(prismaReservation: any): Reservation {
     return new Reservation({
       id: prismaReservation.id,
       dateAt: prismaReservation.dateAt,
+
+      // On garde les IDs pour la compatibilité
       userId: prismaReservation.userId,
       placeId: prismaReservation.placeId,
       trajetId: prismaReservation.trajetId,
+
+      // Relations complètes
+      user: prismaReservation.user
+        ? ({
+            id: prismaReservation.user.id,
+            name: prismaReservation.user.name,
+            otherName: prismaReservation.user.otherName,
+            email: prismaReservation.user.email,
+            address: prismaReservation.user.address || '',
+            telephone: prismaReservation.user.telephone || '',
+            // Valeurs par défaut pour les propriétés sensibles
+            // Ces valeurs ne seront pas exposées dans l'API
+            password: '',
+            role: UserRole.USER,
+          } as User)
+        : undefined,
+
+      place: prismaReservation.place
+        ? ({
+            id: prismaReservation.place.id,
+            number: prismaReservation.place.number,
+            isAvailable: prismaReservation.place.isAvailable,
+            vehiculeId: prismaReservation.place.vehiculeId,
+            occupiedAt: prismaReservation.place.occupiedAt,
+            vehicule: prismaReservation.place.vehicule
+              ? {
+                  id: prismaReservation.place.vehicule.id,
+                  matricule: prismaReservation.place.vehicule.matricule,
+                  company: prismaReservation.place.vehicule.company,
+                  driver: prismaReservation.place.vehicule.driver,
+                }
+              : undefined,
+          } as any)
+        : undefined,
+
+      trajet: prismaReservation.trajet
+        ? ({
+            id: prismaReservation.trajet.id,
+            departAt: {
+              date: prismaReservation.trajet.departAt,
+              station: prismaReservation.trajet.departStation
+                ? {
+                    id: prismaReservation.trajet.departStation.id,
+                    name: prismaReservation.trajet.departStation.name,
+                    address: prismaReservation.trajet.departStation.address,
+                    city: prismaReservation.trajet.departStation.city,
+                    country: prismaReservation.trajet.departStation.country,
+                  }
+                : undefined,
+            },
+            arriveAt: {
+              date: prismaReservation.trajet.arriveAt,
+              station: prismaReservation.trajet.arriveStation
+                ? {
+                    id: prismaReservation.trajet.arriveStation.id,
+                    name: prismaReservation.trajet.arriveStation.name,
+                    address: prismaReservation.trajet.arriveStation.address,
+                    city: prismaReservation.trajet.arriveStation.city,
+                    country: prismaReservation.trajet.arriveStation.country,
+                  }
+                : undefined,
+            },
+            departStationId: prismaReservation.trajet.departStationId,
+            arriveStationId: prismaReservation.trajet.arriveStationId,
+            vehiculeId: prismaReservation.trajet.vehiculeId,
+            pricePerSeat: prismaReservation.trajet.pricePerSeat,
+            vehicule: prismaReservation.trajet.vehicule
+              ? {
+                  id: prismaReservation.trajet.vehicule.id,
+                  matricule: prismaReservation.trajet.vehicule.matricule,
+                  company: prismaReservation.trajet.vehicule.company,
+                  driver: prismaReservation.trajet.vehicule.driver,
+                }
+              : undefined,
+          } as any)
+        : undefined,
     });
   }
 
-  toPrisma(reservation: Reservation): PrismaReservation {
+  toPrisma(reservation: Reservation): any {
     return {
       id: reservation.id,
       dateAt: reservation.dateAt,

+ 38 - 1
node_app/src/infrastructure/mappers/trajet.mapper.ts

@@ -4,23 +4,28 @@ import {
   Trajet as PrismaTrajet,
   Station as PrismaStation, // Si PrismaTrajet inclut les objets Station
   Vehicule as PrismaVehicule, // Si PrismaTrajet inclut l'objet Vehicule
+  User as PrismaUser, // Pour la relation employee
+  Reservation as PrismaReservation, // Pour calculer les places disponibles
 } from '@prisma/client';
 import { Trajet } from '../../domain/entities/trajet.entity';
 import { Station } from '../../domain/entities/station.entity';
 import { Vehicule } from '../../domain/entities/vehicule.entity';
+import { User } from '../../domain/entities/user.entity';
 import { CreateTrajetDto } from '../../application/dto/create-trajet.dto';
 import { UpdateTrajetDto } from '../../application/dto/update-trajet.dto';
 import { TrajetResponseDto } from '../../application/dto/responses/trajet-response.dto';
 // Supposons que vous ayez un StationMapper et VehiculeMapper
 import { StationMapper } from './station.mapper'; // À créer/importer
 import { VehiculeMapper } from './vehicule.mapper'; // À créer/importer
+import { UserMapper } from './user.mapper'; // Pour mapper l'employee
 
 // Type pour PrismaTrajet avec les relations potentiellement incluses
 type PrismaTrajetWithDetails = PrismaTrajet & {
   departStation?: PrismaStation | null;
   arriveStation?: PrismaStation | null;
   vehicule?: PrismaVehicule | null;
-  // reservations?: PrismaReservation[] | null; // Si vous gérez les réservations ici
+  employee?: PrismaUser | null; // Relation vers l'employé responsable
+  reservations?: PrismaReservation[] | null; // Pour calculer les places disponibles
 };
 
 @Injectable()
@@ -31,6 +36,7 @@ export class TrajetMapper
   constructor(
     private readonly stationMapper: StationMapper,
     private readonly vehiculeMapper: VehiculeMapper,
+    private readonly userMapper: UserMapper,
   ) {}
 
   toDomain(prismaTrajet: PrismaTrajetWithDetails): Trajet {
@@ -52,6 +58,17 @@ export class TrajetMapper
       if (mapped) vehiculeDomain = mapped;
     }
 
+    let employeeDomain: User | undefined = undefined;
+    if (prismaTrajet.employee) {
+      const mapped = this.userMapper.toDomain(prismaTrajet.employee);
+      if (mapped) employeeDomain = mapped;
+    }
+
+    // Calcul des places disponibles
+    const totalSeats = prismaTrajet.vehicule?.number_places || 0;
+    const reservedSeats = prismaTrajet.reservations?.length || 0;
+    const availableSeats = Math.max(0, totalSeats - reservedSeats);
+
     return new Trajet({
       id: prismaTrajet.id,
       departAt: prismaTrajet.departAt,
@@ -60,10 +77,13 @@ export class TrajetMapper
       arriveStationId: prismaTrajet.arriveStationId,
       vehiculeId: prismaTrajet.vehiculeId,
       companyId: prismaTrajet.companyId ?? undefined,
+      employeeId: prismaTrajet.employeeId ?? undefined,
       pricePerSeat: prismaTrajet.pricePerSeat,
       departStation: departStationDomain,
       arriveStation: arriveStationDomain,
       vehicule: vehiculeDomain,
+      employee: employeeDomain,
+      availableSeats: availableSeats,
       // reservations: prismaTrajet.reservations?.map(r => this.reservationMapper.toDomain(r)) || [], // Si géré
     });
   }
@@ -80,6 +100,7 @@ export class TrajetMapper
       arriveStationId: dto.arriveStationId,
       vehiculeId: dto.vehiculeId,
       companyId: (dto as CreateTrajetDto).companyId,
+      employeeId: (dto as CreateTrajetDto).employeeId,
       pricePerSeat: (dto as CreateTrajetDto).pricePerSeat || 0,
       // Les objets Station et Vehicule ne sont pas créés à partir du DTO ici,
       // ils seraient récupérés/assignés dans la logique de service.
@@ -95,7 +116,9 @@ export class TrajetMapper
     response.arriveStationId = trajet.arriveStationId;
     response.vehiculeId = trajet.vehiculeId;
     response.companyId = trajet.companyId;
+    response.employeeId = trajet.employeeId;
     response.pricePerSeat = trajet.pricePerSeat;
+    response.availableSeats = trajet.availableSeats || 0;
 
     // Si TrajetResponseDto doit inclure des détails sur les stations ou le véhicule
     if (trajet.departStation) {
@@ -113,6 +136,20 @@ export class TrajetMapper
     if (trajet.vehicule) {
       response.vehicule = this.vehiculeMapper.toResponseDto(trajet.vehicule);
     }
+    if (trajet.employee) {
+      // Utiliser UserResponseDto.fromEntity pour créer la réponse complète
+      response.employee = {
+        id: trajet.employee.id!,
+        name: trajet.employee.name || '',
+        otherName: trajet.employee.otherName || '',
+        email: trajet.employee.email,
+        address: trajet.employee.address || '',
+        telephone: trajet.employee.telephone || '',
+        role: trajet.employee.role || '',
+        createdAt: new Date(),
+        updatedAt: new Date(),
+      };
+    }
     // Mapper les réservations si nécessaire et si elles font partie de TrajetResponseDto
     // if (trajet.reservations) {
     //   response.reservations = trajet.reservations.map(r => this.reservationMapper.toResponseDto(r));

+ 10 - 0
node_app/src/infrastructure/mappers/transaction.mapper.ts

@@ -45,6 +45,16 @@ export class TransactionMapper implements IPrismaMapper<PrismaTransaction, Trans
       paydunya_invoice_url: null,
       paydunya_response: null,
       webhook_data: null,
+      // CinetPay specific fields
+      cinetpay_transaction_id: null,
+      cinetpay_payment_token: null,
+      cinetpay_payment_url: null,
+      cinetpay_response: null,
+      cinetpay_webhook_data: null,
+      cinetpay_payment_method: null,
+      cinetpay_operator_id: null,
+      cinetpay_phone_number: null,
+      cinetpay_payment_date: null,
     };
   }
 

+ 2 - 0
node_app/src/infrastructure/mappers/user.mapper.ts

@@ -14,6 +14,7 @@ export class UserMapper implements IPrismaMapper<PrismaUser, User> {
       address: prismaUser.address || undefined,
       telephone: prismaUser.telephone || undefined,
       role: prismaUser.role ? (prismaUser.role as UserRole) : undefined,
+      companyId: prismaUser.companyId || undefined,
     });
   }
 
@@ -27,6 +28,7 @@ export class UserMapper implements IPrismaMapper<PrismaUser, User> {
       address: user.address || '',
       telephone: user.telephone || '',
       role: user.role || '',
+      companyId: user.companyId || null,
     };
   }
 

+ 139 - 0
node_app/src/infrastructure/repositories/qrcode.repository.ts

@@ -0,0 +1,139 @@
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from '../database/prisma.service';
+import { IQrcodeRepository } from '../../domain/repositories/qrcode.repository.interface';
+import { Qrcode } from '../../domain/entities/qrcode.entity';
+import { QrcodeMapper } from '../mappers/qrcode.mapper';
+
+@Injectable()
+export class QrcodeRepository implements IQrcodeRepository {
+  constructor(
+    private readonly prisma: PrismaService,
+    private readonly qrcodeMapper: QrcodeMapper,
+  ) {}
+
+  async create(qrcode: Qrcode): Promise<Qrcode> {
+    const prismaQrcode = await this.prisma.qrcode.create({
+      data: {
+        id: qrcode.id,
+        reservationId: qrcode.reservationId,
+        value: qrcode.value,
+        createdAt: qrcode.createdAt,
+        scannedAt: qrcode.scannedAt,
+      },
+      include: {
+        reservation: {
+          include: {
+            user: true,
+            trajet: {
+              include: {
+                departStation: true,
+                arriveStation: true,
+              },
+            },
+            place: true,
+          },
+        },
+      },
+    });
+
+    return this.qrcodeMapper.toDomain(prismaQrcode);
+  }
+
+  async findById(id: string): Promise<Qrcode | null> {
+    const prismaQrcode = await this.prisma.qrcode.findUnique({
+      where: { id },
+      include: {
+        reservation: {
+          include: {
+            user: true,
+            trajet: {
+              include: {
+                departStation: true,
+                arriveStation: true,
+              },
+            },
+            place: true,
+          },
+        },
+      },
+    });
+
+    return prismaQrcode ? this.qrcodeMapper.toDomain(prismaQrcode) : null;
+  }
+
+  async findByReservationId(reservationId: number): Promise<Qrcode | null> {
+    const prismaQrcode = await this.prisma.qrcode.findUnique({
+      where: { reservationId },
+      include: {
+        reservation: {
+          include: {
+            user: true,
+            trajet: {
+              include: {
+                departStation: true,
+                arriveStation: true,
+              },
+            },
+            place: true,
+          },
+        },
+      },
+    });
+
+    return prismaQrcode ? this.qrcodeMapper.toDomain(prismaQrcode) : null;
+  }
+
+  async findByValue(value: string): Promise<Qrcode | null> {
+    const prismaQrcode = await this.prisma.qrcode.findFirst({
+      where: { value },
+      include: {
+        reservation: {
+          include: {
+            user: true,
+            trajet: {
+              include: {
+                departStation: true,
+                arriveStation: true,
+              },
+            },
+            place: true,
+          },
+        },
+      },
+    });
+
+    return prismaQrcode ? this.qrcodeMapper.toDomain(prismaQrcode) : null;
+  }
+
+  async update(id: string, qrcode: Partial<Qrcode>): Promise<Qrcode> {
+    const prismaQrcode = await this.prisma.qrcode.update({
+      where: { id },
+      data: {
+        scannedAt: qrcode.scannedAt,
+        value: qrcode.value,
+      },
+      include: {
+        reservation: {
+          include: {
+            user: true,
+            trajet: {
+              include: {
+                departStation: true,
+                arriveStation: true,
+              },
+            },
+            place: true,
+          },
+        },
+      },
+    });
+
+    return this.qrcodeMapper.toDomain(prismaQrcode);
+  }
+
+  async delete(id: string): Promise<void> {
+    await this.prisma.qrcode.delete({
+      where: { id },
+    });
+  }
+}

+ 3 - 3
node_app/src/infrastructure/services/hasher.service.ts

@@ -1,17 +1,17 @@
 import { Injectable } from '@nestjs/common';
 import { IHasherService } from '../../application/interfaces/i-hasher.service';
-import * as bcrypt from 'bcrypt';
+import * as argon2 from 'argon2';
 
 @Injectable()
 export class HasherService implements IHasherService {
   private readonly saltRounds = 10;
 
   async hash(data: string): Promise<string> {
-    const hashedData = await bcrypt.hash(data, this.saltRounds);
+    const hashedData = await argon2.hash(data);
     return hashedData;
   }
 
   async compare(data: string, encrypted: string): Promise<boolean> {
-    return bcrypt.compare(data, encrypted);
+    return argon2.verify(encrypted, data);
   }
 }

+ 28 - 0
node_app/src/infrastructure/services/paydunya.service.ts

@@ -140,6 +140,34 @@ export class PaydunyaService {
       throw error;
     }
   }
+
+  /**
+   * Traite les données de webhook PayDunya
+   * @param webhookData - Données reçues du webhook
+   * @returns Promise avec le résultat du traitement
+   */
+  async handleWebhook(webhookData: any): Promise<any> {
+    try {
+      this.logger.log('Processing PayDunya webhook:', webhookData);
+      
+      // Vérifier la signature du webhook si nécessaire
+      // TODO: Implémenter la vérification de signature
+      
+      // Traiter les données selon le type d'événement
+      const { status, invoice_token, custom_data } = webhookData;
+      
+      return {
+        success: true,
+        status,
+        invoice_token,
+        custom_data,
+        processed_at: new Date().toISOString()
+      };
+    } catch (error) {
+       this.logger.error('Error processing PayDunya webhook:', error);
+       throw new Error(`Webhook processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+     }
+  }
 }
 
 // Types et interfaces pour PayDunya

+ 71 - 11
node_app/src/presentation/controllers/payment.controller.ts

@@ -214,17 +214,77 @@ export class PaymentController {
   })
   @ApiResponse({ status: 200, description: 'Webhook traité avec succès' })
   async handlePaydunyaWebhook(@Body() webhookData: any) {
-    this.logger.log('Received PayDunya webhook:', JSON.stringify(webhookData));
-    
-    // TODO: Implémenter la logique de traitement du webhook
-    // - Vérifier la signature du webhook
-    // - Mettre à jour le statut de la transaction
-    // - Envoyer des notifications
-    
-    return {
-      status: 'received',
-      message: 'Webhook processed successfully',
-    };
+    try {
+      this.logger.log('Webhook PayDunya reçu:', JSON.stringify(webhookData));
+      
+      // Traiter le webhook PayDunya
+      const result = await this.paydunyaService.handleWebhook(webhookData);
+      
+      return {
+        status: 'success',
+        message: 'Webhook traité avec succès',
+        data: result
+      };
+    } catch (error) {
+      this.logger.error('Erreur lors du traitement du webhook PayDunya:', error);
+      throw new BadRequestException('Erreur lors du traitement du webhook');
+    }
+  }
+
+  @Post('cinetpay/webhook')
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ 
+    summary: 'Webhook CinetPay',
+    description: 'Endpoint pour recevoir les notifications de paiement CinetPay'
+  })
+  @ApiResponse({ status: 200, description: 'Webhook CinetPay traité avec succès' })
+  @Public()
+  async handleCinetPayWebhook(@Body() webhookData: any) {
+    try {
+      this.logger.log('Webhook CinetPay reçu:', JSON.stringify(webhookData));
+      
+      // Valider les données du webhook CinetPay
+      if (!webhookData.transaction_id || !webhookData.status) {
+        throw new BadRequestException('Données webhook CinetPay invalides');
+      }
+
+      // Traiter selon le statut du paiement
+      if (webhookData.status === 'COMPLETED') {
+        this.logger.log(`Paiement CinetPay confirmé pour la transaction: ${webhookData.transaction_id}`);
+        
+        // Extraire les métadonnées si disponibles
+        let metadata = null;
+        if (webhookData.metadata) {
+          try {
+            metadata = JSON.parse(webhookData.metadata);
+          } catch (e) {
+            this.logger.warn('Impossible de parser les métadonnées CinetPay');
+          }
+        }
+
+        // Ici, vous pouvez ajouter la logique pour:
+        // 1. Créer l'utilisateur et les réservations
+        // 2. Mettre à jour le statut de la transaction
+        // 3. Envoyer des notifications
+        
+        if (metadata && metadata.userData && metadata.reservationData) {
+          this.logger.log('Traitement des données utilisateur et réservation depuis les métadonnées');
+          // TODO: Implémenter la création des entités après paiement confirmé
+        }
+      } else if (webhookData.status === 'FAILED' || webhookData.status === 'CANCELLED') {
+        this.logger.log(`Paiement CinetPay échoué/annulé pour la transaction: ${webhookData.transaction_id}`);
+        // TODO: Gérer les échecs de paiement
+      }
+      
+      return {
+        status: 'success',
+        message: 'Webhook CinetPay traité avec succès',
+        transaction_id: webhookData.transaction_id
+      };
+    } catch (error) {
+      this.logger.error('Erreur lors du traitement du webhook CinetPay:', error);
+      throw new BadRequestException('Erreur lors du traitement du webhook CinetPay');
+    }
   }
 
   @Get('methods')

+ 48 - 162
node_app/src/presentation/controllers/qrcode.controller.ts

@@ -1,191 +1,77 @@
 import {
   Controller,
-  Get,
   Post,
-  Body,
+  Get,
   Param,
+  Body,
+  HttpCode,
+  HttpStatus,
   ParseIntPipe,
 } from '@nestjs/common';
-import {
-  ApiResponse,
-  ApiTags,
-  ApiOperation,
-  ApiParam,
-  ApiSecurity,
-  ApiProperty,
-} from '@nestjs/swagger';
-import { Roles } from '../decorators/roles.decorator';
-import { UserRole } from '../../domain/entities/user.entity';
+import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
+import { GenerateQrcodeUseCase } from '../../application/use-cases/qrcode/generate-qrcode.use-case';
+import { FindQrcodeUseCase } from '../../application/use-cases/qrcode/find-qrcode.use-case';
+import { ScanQrcodeUseCase } from '../../application/use-cases/qrcode/scan-qrcode.use-case';
 
-// DTO pour créer un QR code
-export class CreateQrcodeDto {
-  @ApiProperty({
-    description: 'ID de la réservation pour laquelle générer le QR code',
-    example: 123,
-    type: 'integer',
-  })
+export class GenerateQrcodeDto {
   reservationId: number;
 }
 
-// DTO pour scanner un QR code
 export class ScanQrcodeDto {
-  @ApiProperty({
-    description: 'Valeur du QR code à scanner',
-    example: 'QR_550e8400-e29b-41d4-a716-446655440000_123',
-  })
-  value: string;
-}
-
-// DTO de réponse pour QR code
-export class QrcodeResponseDto {
-  @ApiProperty({
-    description: 'ID unique du QR code (UUID)',
-    example: '550e8400-e29b-41d4-a716-446655440000',
-  })
-  id: string;
-
-  @ApiProperty({
-    description: 'ID de la réservation associée',
-    example: 123,
-    type: 'integer',
-  })
-  reservationId: number;
-
-  @ApiProperty({
-    description: 'Date de création du QR code',
-    example: '2024-01-15T10:30:00Z',
-    type: 'string',
-    format: 'date-time',
-  })
-  createdAt: Date;
-
-  @ApiProperty({
-    description: 'Date de scan du QR code (null si pas encore scanné)',
-    example: '2024-01-15T14:45:00Z',
-    type: 'string',
-    format: 'date-time',
-    required: false,
-  })
-  scannedAt?: Date;
-
-  @ApiProperty({
-    description: 'Valeur encodée du QR code',
-    example: 'QR_550e8400-e29b-41d4-a716-446655440000_123',
-  })
   value: string;
-
-  @ApiProperty({
-    description: 'Informations de la réservation associée',
-    required: false,
-  })
-  reservation?: any;
 }
 
-@ApiTags('qrcodes')
-@Controller('qrcodes')
-@ApiSecurity('access-token')
+@ApiTags('QR Codes')
+@Controller('qrcode')
 export class QrcodeController {
   constructor(
-    // Inject use cases here when they are created
+    private readonly generateQrcodeUseCase: GenerateQrcodeUseCase,
+    private readonly findQrcodeUseCase: FindQrcodeUseCase,
+    private readonly scanQrcodeUseCase: ScanQrcodeUseCase,
   ) {}
 
-  @Post()
-  @ApiOperation({ 
-    summary: 'Générer un QR code pour une réservation',
-    description: 'Crée un nouveau QR code unique associé à une réservation spécifique'
-  })
-  @ApiResponse({
-    status: 201,
-    description: 'QR code généré avec succès',
-    type: QrcodeResponseDto,
-  })
-  @ApiResponse({
-    status: 400,
-    description: 'Données invalides - ID de réservation manquant ou invalide',
-  })
-  @ApiResponse({
-    status: 404,
-    description: 'Réservation non trouvée',
-  })
-  async generate(@Body() createQrcodeDto: CreateQrcodeDto): Promise<QrcodeResponseDto> {
-    // Implementation will be added when use cases are created
-    throw new Error('Not implemented yet');
+  @Post('generate')
+  @HttpCode(HttpStatus.CREATED)
+  @ApiOperation({ summary: 'Générer un QR code pour une réservation' })
+  @ApiBody({ type: GenerateQrcodeDto })
+  @ApiResponse({ status: 201, description: 'QR code généré avec succès' })
+  @ApiResponse({ status: 404, description: 'Réservation non trouvée' })
+  async generateQrcode(@Body() generateQrcodeDto: GenerateQrcodeDto) {
+    return await this.generateQrcodeUseCase.execute({
+      reservationId: generateQrcodeDto.reservationId,
+    });
   }
 
   @Get('reservation/:reservationId')
-  @ApiOperation({ 
-    summary: 'Récupérer le QR code d\'une réservation',
-    description: 'Trouve le QR code associé à une réservation spécifique'
-  })
-  @ApiParam({ 
-    name: 'reservationId', 
-    description: 'ID unique de la réservation',
-    example: 123,
-    type: 'integer'
-  })
-  @ApiResponse({
-    status: 200,
-    description: 'QR code trouvé avec succès',
-    type: QrcodeResponseDto,
-  })
-  @ApiResponse({
-    status: 404,
-    description: 'QR code non trouvé pour cette réservation',
-  })
-  async findByReservation(@Param('reservationId', ParseIntPipe) reservationId: number): Promise<QrcodeResponseDto> {
-    // Implementation will be added when use cases are created
-    throw new Error('Not implemented yet');
+  @ApiOperation({ summary: 'Récupérer un QR code par ID de réservation' })
+  @ApiParam({ name: 'reservationId', description: 'ID de la réservation' })
+  @ApiResponse({ status: 200, description: 'QR code trouvé' })
+  @ApiResponse({ status: 404, description: 'QR code non trouvé' })
+  async findQrcodeByReservation(
+    @Param('reservationId', ParseIntPipe) reservationId: number,
+  ) {
+    return await this.findQrcodeUseCase.findByReservationId({ reservationId });
   }
 
   @Post('scan')
-  @Roles(UserRole.DRIVER, UserRole.ADMIN, UserRole.MANAGER)
-  @ApiOperation({ 
-    summary: 'Scanner un QR code',
-    description: 'Valide et marque un QR code comme scanné lors de l\'embarquement'
-  })
-  @ApiResponse({
-    status: 200,
-    description: 'QR code scanné avec succès',
-    type: QrcodeResponseDto,
-  })
-  @ApiResponse({
-    status: 404,
-    description: 'QR code non trouvé ou invalide',
-  })
-  @ApiResponse({
-    status: 400,
-    description: 'QR code déjà scanné ou expiré',
-  })
-  @ApiResponse({
-    status: 403,
-    description: 'Accès refusé - Rôle insuffisant pour scanner',
-  })
-  async scan(@Body() scanQrcodeDto: ScanQrcodeDto): Promise<QrcodeResponseDto> {
-    // Implementation will be added when use cases are created
-    throw new Error('Not implemented yet');
+  @HttpCode(HttpStatus.OK)
+  @ApiOperation({ summary: 'Scanner un QR code' })
+  @ApiBody({ type: ScanQrcodeDto })
+  @ApiResponse({ status: 200, description: 'QR code scanné avec succès' })
+  @ApiResponse({ status: 400, description: 'QR code invalide ou déjà scanné' })
+  @ApiResponse({ status: 404, description: 'QR code non trouvé' })
+  async scanQrcode(@Body() scanQrcodeDto: ScanQrcodeDto) {
+    return await this.scanQrcodeUseCase.execute({
+      value: scanQrcodeDto.value,
+    });
   }
 
   @Get(':id')
-  @ApiOperation({ 
-    summary: 'Récupérer un QR code par son ID',
-    description: 'Trouve un QR code spécifique par son identifiant unique'
-  })
-  @ApiParam({ 
-    name: 'id', 
-    description: 'ID unique du QR code (UUID)',
-    example: '550e8400-e29b-41d4-a716-446655440000'
-  })
-  @ApiResponse({
-    status: 200,
-    description: 'QR code trouvé avec succès',
-    type: QrcodeResponseDto,
-  })
-  @ApiResponse({
-    status: 404,
-    description: 'QR code non trouvé',
-  })
-  async findOne(@Param('id') id: string): Promise<QrcodeResponseDto> {
-    // Implementation will be added when use cases are created
-    throw new Error('Not implemented yet');
+  @ApiOperation({ summary: 'Récupérer un QR code par son ID' })
+  @ApiParam({ name: 'id', description: 'ID du QR code' })
+  @ApiResponse({ status: 200, description: 'QR code trouvé' })
+  @ApiResponse({ status: 404, description: 'QR code non trouvé' })
+  async findQrcodeById(@Param('id') id: string) {
+    return await this.findQrcodeUseCase.findById({ id });
   }
 }

+ 115 - 6
node_app/src/presentation/controllers/reservation.controller.ts

@@ -10,6 +10,7 @@ import {
   ParseIntPipe,
   HttpStatus,
   HttpCode,
+  DefaultValuePipe,
 } from '@nestjs/common';
 import {
   ApiSecurity,
@@ -22,12 +23,17 @@ import {
 import { CreateReservationDto } from '../../application/dto/create-reservation.dto';
 import { UpdateReservationDto } from '../../application/dto/update-reservation.dto';
 import { ReservationResponseDto } from '../../application/dto/responses/reservation-response.dto';
+import { ReservationDetailsResponseDto } from '../../application/dto/responses/reservation-details-response.dto';
 import { CreateReservationUseCase } from '../../application/use-cases/reservation/create-reservation.use-case';
 import { FindAllReservationsUseCase } from '../../application/use-cases/reservation/find-all-reservations.use-case';
 import { FindReservationByIdUseCase } from '../../application/use-cases/reservation/find-reservation-by-id.use-case';
 import { UpdateReservationUseCase } from '../../application/use-cases/reservation/update-reservation.use-case';
 import { DeleteReservationUseCase } from '../../application/use-cases/reservation/delete-reservation.use-case';
+import { FindReservationsByUserUseCase } from '../../application/use-cases/reservation/find-reservations-by-user.use-case';
+import { FindQrcodeUseCase } from '../../application/use-cases/qrcode/find-qrcode.use-case';
 import { Public } from '../decorators/public.decorator';
+import { Roles } from '../decorators/roles.decorator';
+import { UserRole } from 'src/domain/entities/user.entity';
 
 @ApiTags('reservations')
 @Controller('reservations')
@@ -39,6 +45,8 @@ export class ReservationController {
     private readonly findReservationByIdUseCase: FindReservationByIdUseCase,
     private readonly updateReservationUseCase: UpdateReservationUseCase,
     private readonly deleteReservationUseCase: DeleteReservationUseCase,
+    private readonly findReservationsByUserUseCase: FindReservationsByUserUseCase,
+    private readonly findQrcodeUseCase: FindQrcodeUseCase,
   ) {}
 
   @Post()
@@ -67,7 +75,7 @@ export class ReservationController {
     name: 'limit',
     required: false,
     type: Number,
-    description: 'Nombre d\'éléments par page (défaut: 10)',
+    description: "Nombre d'éléments par page (défaut: 10)",
   })
   @ApiResponse({
     status: 200,
@@ -84,10 +92,7 @@ export class ReservationController {
     },
   })
   @ApiResponse({ status: 401, description: 'Non autorisé' })
-  async findAll(
-    @Query('page') page?: number,
-    @Query('limit') limit?: number,
-  ) {
+  async findAll(@Query('page') page?: number, @Query('limit') limit?: number) {
     return this.findAllReservationsUseCase.execute(page || 1, limit || 10);
   }
 
@@ -127,10 +132,114 @@ export class ReservationController {
   @HttpCode(HttpStatus.NO_CONTENT)
   @ApiOperation({ summary: 'Supprimer une réservation' })
   @ApiParam({ name: 'id', description: 'ID de la réservation' })
-  @ApiResponse({ status: 204, description: 'Réservation supprimée avec succès' })
+  @ApiResponse({
+    status: 204,
+    description: 'Réservation supprimée avec succès',
+  })
   @ApiResponse({ status: 404, description: 'Réservation non trouvée' })
   @ApiResponse({ status: 401, description: 'Non autorisé' })
   async remove(@Param('id', ParseIntPipe) id: number) {
     return this.deleteReservationUseCase.execute(id);
   }
+
+  @Get('user/:userId')
+  @Roles(UserRole.USER, UserRole.ADMIN, UserRole.MANAGER)
+  @ApiOperation({ summary: "Récupérer les réservations d'un utilisateur" })
+  @ApiParam({ name: 'userId', description: "ID de l'utilisateur" })
+  @ApiQuery({
+    name: 'status',
+    required: false,
+    description: 'Filtrer par statut',
+  })
+  @ApiQuery({
+    name: 'page',
+    required: false,
+    description: 'Numéro de page',
+    type: 'number',
+  })
+  @ApiQuery({
+    name: 'limit',
+    required: false,
+    description: "Nombre d'éléments par page",
+    type: 'number',
+  })
+  @ApiResponse({
+    status: 200,
+    description: "Liste des réservations de l'utilisateur",
+    type: [ReservationResponseDto],
+  })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  async findByUser(
+    @Param('userId', ParseIntPipe) userId: number,
+    @Query('status') status?: string,
+    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1,
+    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number = 10,
+  ) {
+    // Utiliser le use case spécifique pour les réservations par utilisateur
+    const result = await this.findReservationsByUserUseCase.execute(
+      userId,
+      page,
+      limit,
+    );
+
+    return {
+      data: result.data,
+      pagination: {
+        page,
+        limit,
+        total: result.total,
+        totalPages: Math.ceil(result.total / limit),
+      },
+    };
+  }
+
+  @Get(':id/details')
+  @Public() //brin
+  @ApiOperation({ summary: "Récupérer les détails complets d'une réservation" })
+  @ApiParam({ name: 'id', description: 'ID de la réservation' })
+  @ApiResponse({
+    status: 200,
+    description: 'Détails de la réservation récupérés avec succès',
+    type: ReservationDetailsResponseDto,
+  })
+  @ApiResponse({ status: 404, description: 'Réservation non trouvée' })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  async getReservationDetails(@Param('id', ParseIntPipe) id: number) {
+    // Récupérer la réservation avec toutes ses relations
+    const reservation = await this.findReservationByIdUseCase.execute(id);
+
+    // Récupérer le QR code associé à la réservation
+    let qrcode = null;
+    try {
+      const qrcodeResult = await this.findQrcodeUseCase.findByReservationId({
+        reservationId: id,
+      });
+      qrcode = qrcodeResult.qrcode;
+    } catch (error) {
+      // QR code non trouvé, on continue sans erreur
+      console.log(`Aucun QR code trouvé pour la réservation ${id}`);
+    }
+
+    // Extraire les informations demandées
+    const trajet = reservation.trajet;
+    const vehicule = trajet?.vehicule;
+    const departStation = trajet?.departStation;
+    const arriveStation = trajet?.arriveStation;
+    const price = trajet?.pricePerSeat;
+    const company = vehicule?.company;
+    const place = reservation.place;
+
+    // Retourner toutes les informations structurées
+    return {
+      reservation,
+      trajet,
+      vehicule,
+      departStation,
+      arriveStation,
+      price,
+      company,
+      place,
+      qrcode,
+    };
+  }
 }

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

@@ -27,6 +27,7 @@ import { FindAllTrajetsUseCase } from '../../application/use-cases/trajet/find-a
 import { FindTrajetByIdUseCase } from '../../application/use-cases/trajet/find-trajet-by-id.use-case'; // Assurez-vous que ce cas d'utilisation existe
 import { FindTrajetsByCompanyUseCase } from '../../application/use-cases/trajet/find-trajets-by-company.use-case';
 import { DeleteTrajetUseCase } from '../../application/use-cases/trajet/delete-trajet.use-case'; // Assurez-vous que ce cas d'utilisation existe
+import { SearchTrajetsUseCase } from '../../application/use-cases/trajet/search-trajets.use-case';
 import { PaginationDto } from '../../application/dto/pagination.dto';
 import { PaginatedResponseDto } from '../../application/dto/responses/paginated-response.dto';
 import { Roles } from '../decorators/roles.decorator';
@@ -45,6 +46,7 @@ export class TrajetController {
     private readonly updateTrajetUseCase: UpdateTrajetUseCase,
     private readonly deleteTrajetUseCase: DeleteTrajetUseCase, // Assurez-vous que ce cas d'utilisation est injecté
     private readonly findTrajetsByCompanyUseCase: FindTrajetsByCompanyUseCase, // Nouveau cas d'utilisation pour les trajets par compagnie
+    private readonly searchTrajetsUseCase: SearchTrajetsUseCase,
   ) {}
 
   @Post()
@@ -78,6 +80,25 @@ export class TrajetController {
     return this.findAllTrajetsUseCase.execute(page, limit);
   }
 
+  @Get('search')
+  @ApiOperation({ summary: 'Rechercher des trajets par stations' })
+  @ApiResponse({
+    status: 200,
+    description: 'Liste paginée des trajets correspondant à la recherche',
+    type: PaginatedResponseDto,
+  })
+  @Public()
+  async search(
+    @Query('q') query: string,
+    @Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
+    @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10,
+  ) {
+    if (!query) {
+      throw new NotFoundException('Paramètre de recherche "q" requis');
+    }
+    return this.searchTrajetsUseCase.execute(query, page, limit);
+  }
+
   @Get('company/:companyId')
   @ApiOperation({ summary: 'Récupérer les trajets d\'une compagnie' })
   @ApiParam({ name: 'companyId', description: 'ID de la compagnie', type: 'number' })

+ 38 - 0
node_app/src/presentation/modules/qrcode.module.ts

@@ -0,0 +1,38 @@
+import { Module } from '@nestjs/common';
+import { QrcodeController } from '../controllers/qrcode.controller';
+import { GenerateQrcodeUseCase } from '../../application/use-cases/qrcode/generate-qrcode.use-case';
+import { FindQrcodeUseCase } from '../../application/use-cases/qrcode/find-qrcode.use-case';
+import { ScanQrcodeUseCase } from '../../application/use-cases/qrcode/scan-qrcode.use-case';
+import { QrcodeRepository } from '../../infrastructure/repositories/qrcode.repository';
+import { QrcodeMapper } from '../../infrastructure/mappers/qrcode.mapper';
+import { IQrcodeRepository } from '../../domain/repositories/qrcode.repository.interface';
+import { PrismaModule } from '../../infrastructure/database/prisma.module';
+import { SharedModule } from './shared.module';
+
+@Module({
+  imports: [PrismaModule, SharedModule],
+  controllers: [QrcodeController],
+  providers: [
+    // Use Cases
+    GenerateQrcodeUseCase,
+    FindQrcodeUseCase,
+    ScanQrcodeUseCase,
+    
+    // Repository
+    {
+      provide: 'IQrcodeRepository',
+      useClass: QrcodeRepository,
+    },
+    
+    // Mapper
+    QrcodeMapper,
+  ],
+  exports: [
+    GenerateQrcodeUseCase,
+    FindQrcodeUseCase,
+    ScanQrcodeUseCase,
+    'IQrcodeRepository',
+    QrcodeMapper,
+  ],
+})
+export class QrcodeModule {}

+ 23 - 2
node_app/src/presentation/modules/reservation.module.ts

@@ -5,13 +5,21 @@ import { FindAllReservationsUseCase } from '../../application/use-cases/reservat
 import { FindReservationByIdUseCase } from '../../application/use-cases/reservation/find-reservation-by-id.use-case';
 import { UpdateReservationUseCase } from '../../application/use-cases/reservation/update-reservation.use-case';
 import { DeleteReservationUseCase } from '../../application/use-cases/reservation/delete-reservation.use-case';
+import { FindReservationsByUserUseCase } from '../../application/use-cases/reservation/find-reservations-by-user.use-case';
 import { ReservationRepository } from '../../infrastructure/database/repositories/reservation.repository';
 import { ReservationMapper } from '../../infrastructure/mappers/reservation.mapper';
 import { PrismaModule } from '../../infrastructure/database/prisma.module';
 import { ServicesModule } from '../../infrastructure/services/services.module';
+import { UserMapper } from 'src/infrastructure/mappers/user.mapper';
+import { PlaceMapper } from 'src/infrastructure/mappers/place.mapper';
+import { TrajetMapper } from 'src/infrastructure/mappers/trajet.mapper';
+import { StationMapper } from 'src/infrastructure/mappers/station.mapper';
+import { VehiculeMapper } from 'src/infrastructure/mappers/vehicule.mapper';
+import { SharedModule } from './shared.module';
+import { CqrsModule } from '@nestjs/cqrs';
 
 @Module({
-  imports: [PrismaModule, ServicesModule],
+  imports: [PrismaModule, ServicesModule, SharedModule, CqrsModule],
   controllers: [ReservationController],
   providers: [
     CreateReservationUseCase,
@@ -19,11 +27,17 @@ import { ServicesModule } from '../../infrastructure/services/services.module';
     FindReservationByIdUseCase,
     UpdateReservationUseCase,
     DeleteReservationUseCase,
+    FindReservationsByUserUseCase,
     {
       provide: 'ReservationRepository',
       useClass: ReservationRepository,
     },
     ReservationMapper,
+    UserMapper,
+    PlaceMapper,
+    TrajetMapper,
+    StationMapper,
+    VehiculeMapper,
   ],
   exports: [
     CreateReservationUseCase,
@@ -31,7 +45,14 @@ import { ServicesModule } from '../../infrastructure/services/services.module';
     FindReservationByIdUseCase,
     UpdateReservationUseCase,
     DeleteReservationUseCase,
+    FindReservationsByUserUseCase,
     'ReservationRepository',
+    ReservationMapper,
+    UserMapper,
+    PlaceMapper,
+    TrajetMapper,
+    StationMapper,
+    VehiculeMapper,
   ],
 })
-export class ReservationModule {}
+export class ReservationModule {}

+ 40 - 0
node_app/src/presentation/modules/shared.module.ts

@@ -0,0 +1,40 @@
+import { Module } from '@nestjs/common';
+import { CqrsModule } from '@nestjs/cqrs';
+import { PrismaModule } from '../../infrastructure/database/prisma.module';
+import { GenerateQrcodeUseCase } from '../../application/use-cases/qrcode/generate-qrcode.use-case';
+import { FindQrcodeUseCase } from '../../application/use-cases/qrcode/find-qrcode.use-case';
+import { CreateReservationUseCase } from '../../application/use-cases/reservation/create-reservation.use-case';
+import { QrcodeRepository } from '../../infrastructure/repositories/qrcode.repository';
+import { ReservationRepository } from '../../infrastructure/database/repositories/reservation.repository';
+import { QrcodeMapper } from '../../infrastructure/mappers/qrcode.mapper';
+import { ReservationMapper } from '../../infrastructure/mappers/reservation.mapper';
+import { ReservationCreatedHandler } from '../../application/handlers/reservation-created.handler';
+
+@Module({
+  imports: [PrismaModule, CqrsModule],
+  providers: [
+    GenerateQrcodeUseCase,
+    FindQrcodeUseCase,
+    ReservationCreatedHandler,
+    {
+      provide: 'IQrcodeRepository',
+      useClass: QrcodeRepository,
+    },
+    {
+      provide: 'ReservationRepository',
+      useClass: ReservationRepository,
+    },
+    QrcodeMapper,
+    ReservationMapper,
+  ],
+  exports: [
+    GenerateQrcodeUseCase,
+    FindQrcodeUseCase,
+    ReservationCreatedHandler,
+    'IQrcodeRepository',
+    'ReservationRepository',
+    QrcodeMapper,
+    ReservationMapper,
+  ],
+})
+export class SharedModule {}

+ 6 - 5
node_app/test/user.service.spec.ts

@@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
 import { CreateUserUseCase } from '../src/application/use-cases/create-user.use-case';
 import { IUserRepository } from '../src/application/interfaces/i-user.repository';
 import { IHasherService } from '../src/application/interfaces/i-hasher.service';
+import { UserRole } from '../src/domain/entities/user.entity';
 
 describe('CreateUserUseCase', () => {
   let useCase: CreateUserUseCase;
@@ -13,14 +14,14 @@ describe('CreateUserUseCase', () => {
       providers: [
         CreateUserUseCase,
         {
-          provide: IUserRepository,
+          provide: 'IUserRepository',
           useValue: {
             create: jest.fn(),
             findByEmail: jest.fn(),
           },
         },
         {
-          provide: IHasherService,
+          provide: 'IHasherService',
           useValue: {
             hash: jest.fn(),
           },
@@ -29,8 +30,8 @@ describe('CreateUserUseCase', () => {
     }).compile();
 
     useCase = module.get<CreateUserUseCase>(CreateUserUseCase);
-    userRepository = module.get<IUserRepository>(IUserRepository);
-    hasherService = module.get<IHasherService>(IHasherService);
+    userRepository = module.get<IUserRepository>('IUserRepository');
+    hasherService = module.get<IHasherService>('IHasherService');
   });
 
   it('should be defined', () => {
@@ -48,7 +49,7 @@ describe('CreateUserUseCase', () => {
     };
 
     const hashedPassword = 'hashedPassword123';
-    const createdUser = { ...createUserDto, id: 1, password: hashedPassword };
+    const createdUser = { ...createUserDto, id: 1, password: hashedPassword, role: UserRole.USER };
 
     jest.spyOn(hasherService, 'hash').mockResolvedValue(hashedPassword);
     jest.spyOn(userRepository, 'create').mockResolvedValue(createdUser);

+ 2 - 1
node_app/tsconfig.json

@@ -20,6 +20,7 @@
     "noFallthroughCasesInSwitch": false,
     "strictPropertyInitialization": false,
     "moduleResolution": "node",
-    "esModuleInterop": true
+    "esModuleInterop": true,
+  
   }
 }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio