Răsfoiți Sursa

chore: sync local changes

Brin 1 lună în urmă
părinte
comite
03f8d87513
31 a modificat fișierele cu 1355 adăugiri și 288 ștergeri
  1. 1 1
      .gitignore
  2. 0 0
      app_transport_front/src/assets/images/.gitkeep
  3. 3 0
      app_transport_front/src/assets/main.css
  4. 1 0
      app_transport_front/src/services/BudgetService.ts
  5. 24 1
      app_transport_front/src/services/ReservationService.ts
  6. 1 1
      app_transport_front/src/services/UserService.ts
  7. 16 0
      app_transport_front/src/types/lottie.d.ts
  8. 52 24
      app_transport_front/src/views/BudgetView.vue
  9. 122 0
      app_transport_front/src/views/ResultsView.vue
  10. 79 0
      app_transport_front/src/views/SearchView.vue
  11. 134 0
      app_transport_front/src/views/TicketView.vue
  12. 76 0
      docker-compose.override.yml
  13. 45 60
      docker-compose.yml
  14. 6 6
      gitignore
  15. 6 6
      node_app/.prettierrc
  16. 99 99
      node_app/README.md
  17. 36 36
      node_app/eslint.config.mjs
  18. 8 8
      node_app/nest-cli.json
  19. 38 0
      node_app/src/application/dto/responses/company-reservations-sum-response.dto.ts
  20. 19 0
      node_app/src/application/dto/user-response.dto.ts
  21. 25 0
      node_app/src/application/use-cases/reservation/get-company-reservations-sum.use-case.ts
  22. 70 0
      node_app/src/infrastructure/database/repositories/reservation.repository.ts
  23. 33 0
      node_app/src/presentation/controllers/reservation.controller.ts
  24. 3 0
      node_app/src/presentation/modules/reservation.module.ts
  25. 88 0
      node_app/test-auth-status.js
  26. 178 0
      node_app/test-frontend-integration.js
  27. 146 0
      node_app/test-reservation-sum.js
  28. 9 9
      node_app/test/jest-e2e.json
  29. 4 4
      node_app/tsconfig.build.json
  30. 10 10
      postgres-init.sql
  31. 23 23
      prometheus/prometheus.yml

+ 1 - 1
.gitignore

@@ -1 +1 @@
-node_modules
+node_modules

+ 0 - 0
app_transport_front/src/assets/images/.gitkeep


+ 3 - 0
app_transport_front/src/assets/main.css

@@ -0,0 +1,3 @@
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';

+ 1 - 0
app_transport_front/src/services/BudgetService.ts

@@ -117,6 +117,7 @@ export default {
   // Gestion du solde
   async getBalance(companyId: number): Promise<Balance | null> {
     try {
+      console.log('loadBalance - companyId:', companyId)
       const response = await authenticatedFetch(`${API_URL}/budget/balance/${companyId}`)
       if (!response.ok) {
         throw new Error('Erreur lors de la récupération du solde')

+ 24 - 1
app_transport_front/src/services/ReservationService.ts

@@ -1,7 +1,7 @@
 import type { Reservation } from '@/types'
 import { authenticatedFetch } from '@/services/authService'
 
-const API_URL = 'http://localhost:3001/api'
+const API_URL = 'http://localhost:3000/api'
 
 export default {
   async getReservations(): Promise<Reservation[]> {
@@ -163,5 +163,28 @@ export default {
       console.error('Erreur:', error)
       return null
     }
+  },
+
+  async getCompanyReservationsSum(companyId: number): Promise<{data: { companyId: number, totalReservations: number, totalAmount: number }} | null> {
+    try {
+      console.log('🌐 ReservationService.getCompanyReservationsSum - Début')
+      console.log('🌐 companyId:', companyId)
+      console.log('🌐 URL appelée:', `${API_URL}/reservations/company/${companyId}/sum`)
+      
+      const response = await authenticatedFetch(`${API_URL}/reservations/company/${companyId}/sum`)
+      console.log('🌐 Réponse reçue:', response.status, response.statusText)
+      
+      if (!response.ok) {
+        console.log('❌ Réponse non OK:', response.status, response.statusText)
+        throw new Error('Erreur lors de la récupération de la somme des réservations de la compagnie')
+      }
+      
+      const result = await response.json()
+      console.log('🌐 Données reçues:', result)
+      return result
+    } catch (error) {
+      console.error('❌ Erreur dans ReservationService.getCompanyReservationsSum:', error)
+      return null
+    }
   }
 }

+ 1 - 1
app_transport_front/src/services/UserService.ts

@@ -2,7 +2,7 @@ import { User } from '@/types/index'
 import { authenticatedFetch } from '@/services/authService'
 import { Role } from '@/Enum/Role'
 
-const API_URL = 'http://localhost:3001/api'
+const API_URL = 'http://localhost:3000/api'
 
 export default {
   async getUsers(page: number = 1, limit: number = 100): Promise<{ users: User[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {

+ 16 - 0
app_transport_front/src/types/lottie.d.ts

@@ -0,0 +1,16 @@
+declare module '@lottiefiles/vue-lottie-player' {
+  import { DefineComponent } from 'vue'
+
+  export const LottieAnimation: DefineComponent<{
+    path?: string
+    autoplay?: boolean
+    loop?: boolean
+    speed?: number
+    direction?: number
+    background?: string
+    height?: number | string
+    width?: number | string
+    hover?: boolean
+    controls?: boolean
+  }>
+}

+ 52 - 24
app_transport_front/src/views/BudgetView.vue

@@ -150,8 +150,8 @@
               <div class="bg-gradient-to-r from-green-400 to-green-600 rounded-lg p-6 text-white">
                 <div class="flex items-center justify-between">
                   <div>
-                    <p class="text-green-100 text-sm">Solde Actuel</p>
-                    <p class="text-2xl font-bold">{{ formatCurrency(currentBalance?.currentBalance || 0) }}</p>
+                    <p class="text-green-100 text-sm">Solde Total Généré</p>
+                    <p class="text-2xl font-bold text-white">{{ reservationsSum }}  CFA</p>
                   </div>
                   <i class="fas fa-wallet text-3xl text-green-200"></i>
                 </div>
@@ -160,7 +160,7 @@
                 <div class="flex items-center justify-between">
                   <div>
                     <p class="text-blue-100 text-sm">Transactions ce mois</p>
-                    <p class="text-2xl font-bold">{{ monthlyTransactions }}</p>
+                    <p class="text-2xl font-bold text-white">{{ monthlyTransactions }}</p>
                   </div>
                   <i class="fas fa-exchange-alt text-3xl text-blue-200"></i>
                 </div>
@@ -169,7 +169,7 @@
                 <div class="flex items-center justify-between">
                   <div>
                     <p class="text-purple-100 text-sm">Dernière mise à jour</p>
-                    <p class="text-lg font-semibold">{{ formatDate(currentBalance?.lastUpdated) }}</p>
+                    <p class="text-lg font-semibold text-white">{{ formatDate(currentBalance?.lastUpdated) }}</p>
                   </div>
                   <i class="fas fa-clock text-3xl text-purple-200"></i>
                 </div>
@@ -453,12 +453,14 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, onBeforeMount } from 'vue'
 import Sidebar from '@/components/Sidebar.vue'
 import TopBar from '@/components/TopBar.vue'
 import { useUserStore } from '@/stores/useUserStore'
 import { useToast } from '@/composables/useToast'
 import BudgetService, { type Subscription, type Balance, type Transaction, type PaymentRequest, type Receipt } from '@/services/BudgetService'
+import ReservationService from '@/services/ReservationService'
+import { useUserCompanyStore } from '@/stores/useCompanyStore'
 
 // Stores et composables
 const userStore = useUserStore()
@@ -480,6 +482,7 @@ const currentBalance = ref<Balance | null>(null)
 const transactions = ref<Transaction[]>([])
 const paymentRequests = ref<PaymentRequest[]>([])
 const receipts = ref<Receipt[]>([])
+const reservationsSum = ref<number>(0)
 
 // Filtres
 const transactionFilter = ref('')
@@ -545,7 +548,7 @@ const formatDate = (dateString: string | undefined) => {
 const formatCurrency = (amount: number) => {
   return new Intl.NumberFormat('fr-FR', {
     style: 'currency',
-    currency: 'EUR'
+    currency: 'CFA'
   }).format(amount)
 }
 
@@ -570,7 +573,7 @@ const getPaymentStatusLabel = (status: string) => {
 
 const subscribeToService = async (subscriptionId: number) => {
   try {
-    const companyId = userStore.userCompany?.id
+    const companyId = useUserCompanyStore().id
     if (!companyId) {
       error('Entreprise non trouvée')
       return
@@ -590,7 +593,7 @@ const subscribeToService = async (subscriptionId: number) => {
 
 const addFunds = async () => {
   try {
-    const companyId = userStore.userCompany?.id
+    const companyId = useUserCompanyStore().id
     if (!companyId) {
       error('Entreprise non trouvée')
       return
@@ -612,7 +615,7 @@ const addFunds = async () => {
 
 const createPaymentRequest = async () => {
   try {
-    const companyId = userStore.userCompany?.id
+    const companyId = useUserCompanyStore().id
     if (!companyId) {
       error('Entreprise non trouvée')
       return
@@ -662,7 +665,7 @@ const downloadReceipt = async (receiptId: number) => {
 // Chargement des données
 const loadSubscriptions = async () => {
   try {
-    const companyId = userStore.userCompany?.id
+    const companyId = useUserCompanyStore().id
     if (!companyId) return
 
     const [active, available] = await Promise.all([
@@ -679,24 +682,47 @@ const loadSubscriptions = async () => {
 
 const loadBalance = async () => {
   try {
-    const companyId = userStore.userCompany?.id
-    if (!companyId) return
+    console.log('🔍 loadBalance - Début de la fonction')
+    console.log('🔍 userStore:', userStore)
+    console.log('🔍 userStore.userCompany:', userStore.userCompany)
+    
+    const companyId = useUserCompanyStore().id
+    console.log('🔍 companyId récupéré:', companyId)
+    
+    if (!companyId) {
+      console.log('❌ Aucun companyId trouvé, arrêt de la fonction', useUserCompanyStore().id)
+      return
+    }
 
-    const [balance, transactionsResult] = await Promise.all([
+    console.log('🚀 Appel des APIs avec companyId:', companyId)
+    const [balance, transactionsResult, reservationsSumResult] = await Promise.all([
       BudgetService.getBalance(companyId),
-      BudgetService.getTransactions(companyId, 1, 50)
+      BudgetService.getTransactions(companyId, 1, 50),
+      ReservationService.getCompanyReservationsSum(companyId)
     ])
 
+    const test = await ReservationService.getCompanyReservationsSum(companyId)
+
+    console.log('📊 Résultats reçus:', test)
+    console.log('  - balance:', balance)
+    console.log('  - transactionsResult:', transactionsResult)
+    console.log('  - reservationsSumResult:', reservationsSumResult)
+
     currentBalance.value = balance
     transactions.value = transactionsResult.transactions
+    reservationsSum.value = reservationsSumResult?.data?.totalAmount || 0
+    
+    console.log('✅ loadBalance terminé avec succès')
+    console.log('  - currentBalance.value:', currentBalance.value)
+    console.log('  - reservationsSum.value:', reservationsSum.value)
   } catch (err) {
-    console.error('Erreur lors du chargement du solde:', err)
+    console.error('❌ Erreur lors du chargement du solde:', err)
   }
 }
 
 const loadPaymentRequests = async () => {
   try {
-    const companyId = userStore.userCompany?.id
+    const companyId = useUserCompanyStore().id
     if (!companyId) return
 
     const result = await BudgetService.getPaymentRequests(companyId, 1, 50)
@@ -708,7 +734,7 @@ const loadPaymentRequests = async () => {
 
 const loadReceipts = async () => {
   try {
-    const companyId = userStore.userCompany?.id
+    const companyId = useUserCompanyStore().id
     if (!companyId) return
 
     const result = await BudgetService.getReceipts(companyId, 1, 50)
@@ -719,13 +745,15 @@ const loadReceipts = async () => {
 }
 
 // Chargement initial
-onMounted(async () => {
-  await Promise.all([
-    loadSubscriptions(),
-    loadBalance(),
-    loadPaymentRequests(),
-    loadReceipts()
-  ])
+onBeforeMount(async () => {
+  
+    await loadSubscriptions(),
+    await loadBalance(),
+    await loadPaymentRequests(),
+    await loadReceipts()
+  
+
+  console.log('Reservations sum:', reservationsSum.value)
 })
 </script>
 

+ 122 - 0
app_transport_front/src/views/ResultsView.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="min-h-screen bg-gray-100 py-6">
+    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+      <div class="bg-white shadow-xl rounded-lg overflow-hidden">
+        <div class="px-4 py-5 sm:p-6">
+          <h2 class="text-2xl font-bold text-gray-900 mb-4">Résultats de recherche</h2>
+          
+          <!-- Filtres et tri -->
+          <div class="mb-6 flex justify-between items-center">
+            <div class="text-sm text-gray-600">
+              Trajets disponibles pour votre recherche
+            </div>
+            <select
+              v-model="sortBy"
+              class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
+            >
+              <option value="price">Prix croissant</option>
+              <option value="-price">Prix décroissant</option>
+              <option value="duration">Durée</option>
+            </select>
+          </div>
+
+          <!-- Liste des trajets -->
+          <div class="space-y-4">
+            <div
+              v-for="trajet in trajets"
+              :key="trajet.id"
+              class="border rounded-lg p-4 hover:shadow-md transition-shadow duration-200"
+            >
+              <div class="flex justify-between items-center">
+                <div class="flex-1">
+                  <div class="flex items-center space-x-4">
+                    <div class="flex flex-col">
+                      <span class="text-lg font-semibold">{{ trajet.departureTime }}</span>
+                      <span class="text-sm text-gray-500">{{ trajet.departure }}</span>
+                    </div>
+                    <div class="flex-1 border-t-2 border-gray-300 mx-4"></div>
+                    <div class="flex flex-col">
+                      <span class="text-lg font-semibold">{{ trajet.arrivalTime }}</span>
+                      <span class="text-sm text-gray-500">{{ trajet.arrival }}</span>
+                    </div>
+                  </div>
+                  <div class="mt-2 flex items-center space-x-4 text-sm text-gray-500">
+                    <span>{{ trajet.duration }}</span>
+                    <span>•</span>
+                    <span>{{ trajet.companyName }}</span>
+                  </div>
+                </div>
+                <div class="flex flex-col items-end justify-between ml-4">
+                  <span class="text-2xl font-bold text-indigo-600">{{ trajet.price }}€</span>
+                  <button
+                    @click="selectTrajet(trajet)"
+                    class="mt-2 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
+                  >
+                    Sélectionner
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </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 sortBy = ref('price')
+const trajets = ref([])
+
+interface Trajet {
+  id: number
+  departure: string
+  arrival: string
+  departureTime: string
+  arrivalTime: string
+  duration: string
+  price: number
+  companyName: string
+}
+
+const fetchTrajets = async () => {
+  try {
+    const { departure, arrival, date } = route.query
+    const response = await fetch(
+      `http://localhost:3000/trajets/search?departure=${departure}&arrival=${arrival}&date=${date}`,
+      {
+        headers: {
+          Authorization: `Bearer ${localStorage.getItem('token')}`,
+        },
+      }
+    )
+
+    if (response.ok) {
+      const data = await response.json()
+      trajets.value = data
+    } else {
+      console.error('Erreur lors de la récupération des trajets')
+    }
+  } catch (error) {
+    console.error('Erreur:', error)
+  }
+}
+
+const selectTrajet = (trajet: Trajet) => {
+  router.push({
+    name: 'ticket',
+    query: {
+      trajetId: trajet.id
+    }
+  })
+}
+
+onMounted(() => {
+  fetchTrajets()
+})
+</script>

+ 79 - 0
app_transport_front/src/views/SearchView.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
+    <div class="relative py-3 sm:max-w-xl sm:mx-auto">
+      <div class="relative px-4 py-10 bg-white mx-8 md:mx-0 shadow rounded-3xl sm:p-10">
+        <div class="max-w-md mx-auto">
+          <div class="divide-y divide-gray-200">
+            <div class="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
+              <h2 class="text-2xl font-bold text-center text-gray-900 mb-8">Rechercher un trajet</h2>
+              <form @submit.prevent="handleSearch" class="space-y-6">
+                <div class="space-y-4">
+                  <div>
+                    <label for="departure" class="block text-sm font-medium text-gray-700">Ville de départ</label>
+                    <input
+                      id="departure"
+                      v-model="departure"
+                      type="text"
+                      required
+                      class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
+                    />
+                  </div>
+                  <div>
+                    <label for="arrival" class="block text-sm font-medium text-gray-700">Ville d'arrivée</label>
+                    <input
+                      id="arrival"
+                      v-model="arrival"
+                      type="text"
+                      required
+                      class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
+                    />
+                  </div>
+                  <div>
+                    <label for="date" class="block text-sm font-medium text-gray-700">Date du voyage</label>
+                    <input
+                      id="date"
+                      v-model="travelDate"
+                      type="date"
+                      required
+                      class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
+                    />
+                  </div>
+                </div>
+                <div class="pt-4">
+                  <button
+                    type="submit"
+                    class="w-full flex justify-center py-2 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"
+                  >
+                    Rechercher
+                  </button>
+                </div>
+              </form>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const departure = ref('')
+const arrival = ref('')
+const travelDate = ref('')
+
+const handleSearch = () => {
+  // Naviguer vers la page des résultats avec les paramètres de recherche
+  router.push({
+    name: 'results',
+    query: {
+      departure: departure.value,
+      arrival: arrival.value,
+      date: travelDate.value
+    }
+  })
+}
+</script>

+ 134 - 0
app_transport_front/src/views/TicketView.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="min-h-screen bg-gray-100 py-6">
+    <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
+      <div class="bg-white shadow-xl rounded-lg overflow-hidden">
+        <!-- En-tête du billet -->
+        <div class="bg-indigo-600 px-4 py-5 sm:p-6">
+          <h2 class="text-2xl font-bold text-white">Votre billet</h2>
+        </div>
+
+        <!-- Informations du trajet -->
+        <div class="px-4 py-5 sm:p-6 border-b border-gray-200">
+          <div class="flex justify-between items-center mb-6">
+            <div class="flex-1">
+              <div class="flex items-center space-x-4">
+                <div class="flex flex-col">
+                  <span class="text-2xl font-semibold">{{ ticket?.departureTime }}</span>
+                  <span class="text-lg text-gray-600">{{ ticket?.departure }}</span>
+                </div>
+                <div class="flex-1 border-t-2 border-gray-300 mx-4 relative">
+                  <div class="absolute -top-2 left-1/2 transform -translate-x-1/2 bg-white px-2 text-gray-500">
+                    {{ ticket?.duration }}
+                  </div>
+                </div>
+                <div class="flex flex-col">
+                  <span class="text-2xl font-semibold">{{ ticket?.arrivalTime }}</span>
+                  <span class="text-lg text-gray-600">{{ ticket?.arrival }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- Détails supplémentaires -->
+          <div class="grid grid-cols-2 gap-4 text-sm text-gray-600">
+            <div>
+              <p class="font-medium">Compagnie</p>
+              <p>{{ ticket?.companyName }}</p>
+            </div>
+            <div>
+              <p class="font-medium">Prix</p>
+              <p class="text-lg font-semibold text-indigo-600">{{ ticket?.price }}€</p>
+            </div>
+            <div>
+              <p class="font-medium">Numéro de billet</p>
+              <p>{{ ticket?.ticketNumber }}</p>
+            </div>
+            <div>
+              <p class="font-medium">Statut</p>
+              <p class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
+                Confirmé
+              </p>
+            </div>
+          </div>
+        </div>
+
+        <!-- Actions -->
+        <div class="px-4 py-5 sm:p-6 bg-gray-50 flex justify-between items-center">
+          <button
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
+            @click="downloadTicket"
+          >
+            Télécharger le billet
+          </button>
+          <button
+            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-indigo-500"
+            @click="addToWallet"
+          >
+            Ajouter au wallet
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+
+interface Ticket {
+  id: number
+  departure: string
+  arrival: string
+  departureTime: string
+  arrivalTime: string
+  duration: string
+  price: number
+  companyName: string
+  ticketNumber: string
+}
+
+const route = useRoute()
+const ticket = ref<Ticket | null>(null)
+
+const fetchTicket = async () => {
+  try {
+    const trajetId = route.query.trajetId
+    const response = await fetch(`http://localhost:3000/trajets/${trajetId}`, {
+      headers: {
+        Authorization: `Bearer ${localStorage.getItem('token')}`,
+      },
+    })
+
+    if (response.ok) {
+      const data = await response.json()
+      ticket.value = {
+        ...data,
+        ticketNumber: generateTicketNumber() // Fonction utilitaire pour générer un numéro de billet
+      }
+    } else {
+      console.error('Erreur lors de la récupération du billet')
+    }
+  } catch (error) {
+    console.error('Erreur:', error)
+  }
+}
+
+const generateTicketNumber = () => {
+  return 'TKT-' + Math.random().toString(36).substr(2, 9).toUpperCase()
+}
+
+const downloadTicket = () => {
+  // Implémenter la logique de téléchargement du billet
+  console.log('Téléchargement du billet...')
+}
+
+const addToWallet = () => {
+  // Implémenter la logique d'ajout au wallet
+  console.log('Ajout au wallet...')
+}
+
+onMounted(() => {
+  fetchTicket()
+})
+</script>

+ 76 - 0
docker-compose.override.yml

@@ -0,0 +1,76 @@
+services:
+  app_transport_front:
+    build:
+      context: ./app_transport_front
+      dockerfile: Dockerfile
+    container_name: vue_frontend_dev
+    volumes:
+      - ./app_transport_front:/app
+      - vue_node_modules:/app/node_modules
+    environment:
+      - VITE_API_URL=http://localhost:3000
+    command: ["npm", "run", "dev"]
+    ports:
+      - "5174:5173"
+
+  node_app:
+    container_name: node_app_dev
+    environment:
+      - NODE_ENV=development
+    volumes:
+      - ./node_app:/usr/src/app
+      - node_app_node_modules:/usr/src/app/node_modules
+    command: ["npm", "run", "dev"]
+    ports:
+      - "3001:3000"
+
+  traefik:
+    command:
+      - "--log.level=DEBUG"
+
+  mysql:
+    environment:
+      - MYSQL_ROOT_PASSWORD=rootpassword
+      - MYSQL_DATABASE=transport
+      - MYSQL_USER=user
+      - MYSQL_PASSWORD=userpassword
+    ports:
+      - "3306:3306"
+
+  redis:
+    ports:
+      - "6379:6379"
+
+  sonarqube:
+    ports:
+      - "9000:9000"
+
+  sonarqube_db:
+    ports:
+      - "5432:5432"
+
+  gogs:
+    ports:
+      - "3000:3000"
+      - "10022:22"
+
+  prometheus:
+    ports:
+      - "9090:9090"
+
+  grafana:
+    ports:
+      - "3003:3000"
+
+  node-exporter:
+    ports:
+      - "9100:9100"
+
+  jenkins:
+    ports:
+      - "8081:8080"
+      - "50000:50000"
+
+volumes:
+  vue_node_modules:
+  node_app_node_modules:

+ 45 - 60
docker-compose.yml

@@ -53,21 +53,6 @@ services:
       - SMTP_PASS=mot_de_passe
       - SMS_API_KEY=ta_clé_api_sms
 
-  payment_service:
-    build:
-      context: ./payment_service
-      dockerfile: Dockerfile
-    container_name: payment_service
-    ports:
-      - "4002:4002"
-    environment:
-      - PAYMENT_PROVIDER=cinetpay
-      - CINETPAY_API_KEY=xxx
-      - CINETPAY_SITE_ID=xxx
-      - CALLBACK_URL=http://localhost:4002/webhook
-    networks:
-      - gogs-network
-
   redis:
     image: redis:7
     container_name: redis_cache
@@ -107,51 +92,51 @@ services:
       - n8n_data:/home/node/.n8n
       - ./local-files:/files
 
-  # traefik:
-  #   image: traefik:v2.10
-  #   container_name: traefik
-  #   restart: always
-  #   command:
-  #     - "--api.dashboard=true"
-  #     - "--api.insecure=true"
-  #     - "--providers.docker=true"
-  #     - "--entrypoints.web.address=:80"
-  #     - "--entrypoints.websecure.address=:443"
-  #     - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
-  #     - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
-  #     - "--certificatesresolvers.myresolver.acme.email=brin.amonkonan@gmail.com"
-  #     - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
-  #     - "--log.level=DEBUG"
-  #   labels:
-  #     - com.host.description=label
-  #     # Router pour accéder au dashboard via traefik.localhost
-  #     - "traefik.enable=true"
-  #     - "traefik.http.routers.traefik.rule=Host(`localhost`)"
-  #     - "traefik.http.routers.traefik.entrypoints=web"
-  #     - "traefik.http.routers.traefik-secure.entrypoints=websecure"
-  #     - "traefik.http.routers.traefik.service=api@internal"  # Utilise le service interne de Traefik
-  #     - "traefik.http.routers.traefik.middlewares=dashboard-auth"  # (Optionnel) protection du dashboard
-  #     - "traefik.http.routers.traefik-secure.tls=true"
-  #     # Middleware pour protéger le dashboard avec une authentification basique (user: admin, mdp: admin)
-  #     #- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/"  
-  #   ports:
-  #     - "80:80"
-  #     - "443:443"
-  #   volumes:
-  #     - "/var/run/docker.sock:/var/run/docker.sock:ro"
-  #     - "./letsencrypt:/letsencrypt"
-  #   networks:
-  #     - gogs-network
+  traefik:
+    image: traefik:v2.10
+    container_name: traefik
+    restart: always
+    command:
+      - "--api.dashboard=true"
+      - "--api.insecure=true"
+      - "--providers.docker=true"
+      - "--entrypoints.web.address=:80"
+      - "--entrypoints.websecure.address=:443"
+      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
+      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
+      - "--certificatesresolvers.myresolver.acme.email=brin.amonkonan@gmail.com"
+      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
+      - "--log.level=DEBUG"
+    labels:
+      - com.host.description=label
+      # Router pour accéder au dashboard via traefik.localhost
+      - "traefik.enable=true"
+      - "traefik.http.routers.traefik.rule=Host(`localhost`)"
+      - "traefik.http.routers.traefik.entrypoints=web"
+      - "traefik.http.routers.traefik-secure.entrypoints=websecure"
+      - "traefik.http.routers.traefik.service=api@internal"  # Utilise le service interne de Traefik
+      - "traefik.http.routers.traefik.middlewares=dashboard-auth"  # (Optionnel) protection du dashboard
+      - "traefik.http.routers.traefik-secure.tls=true"
+      # Middleware pour protéger le dashboard avec une authentification basique (user: admin, mdp: admin)
+      #- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/"  
+    ports:
+      - "80:80"
+      - "443:443"
+    volumes:
+      - "/var/run/docker.sock:/var/run/docker.sock:ro"
+      - "./letsencrypt:/letsencrypt"
+    networks:
+      - gogs-network
 
-  # whoami:
-  #   image: traefik/whoami
-  #   labels:
-  #     - "traefik.enable=true"
-  #     - "traefik.http.routers.whoami.rule=Host(`localhost`)"
-  #     - "traefik.http.routers.whoami.entrypoints=web"
-  #     - "traefik.http.services.whoami.loadbalancer.server.port=80"
-  #   networks:
-  #     - gogs-network
+  whoami:
+    image: traefik/whoami
+    labels:
+      - "traefik.enable=true"
+      - "traefik.http.routers.whoami.rule=Host(`localhost`)"
+      - "traefik.http.routers.whoami.entrypoints=web"
+      - "traefik.http.services.whoami.loadbalancer.server.port=80"
+    networks:
+      - gogs-network
 
   mysql:
     image: mysql:5.7
@@ -206,7 +191,7 @@ services:
       DATABASE_URL: mysql://user:userpassword@mysql:3306/transport?connectTimeout=30000
       NODE_ENV: development
     ports:
-      - "3001:3000"
+      - "3000:3000"
     volumes:
       - ./node_app:/usr/src/app
       - /usr/src/app/node_modules

+ 6 - 6
gitignore

@@ -1,7 +1,7 @@
-/node_module
-/dist
-/node_app/node_module
-/node_app/dist
-monhopital-firebase-adminsdk.json
-.env
+/node_module
+/dist
+/node_app/node_module
+/node_app/dist
+monhopital-firebase-adminsdk.json
+.env
 .env.prod

+ 6 - 6
node_app/.prettierrc

@@ -1,7 +1,7 @@
-{
-  "singleQuote": true,
-  "trailingComma": "all",
-  "tabWidth": 2,
-  "semi": true,
-  "printWidth": 80
+{
+  "singleQuote": true,
+  "trailingComma": "all",
+  "tabWidth": 2,
+  "semi": true,
+  "printWidth": 80
 }

+ 99 - 99
node_app/README.md

@@ -1,99 +1,99 @@
-<p align="center">
-  <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
-</p>
-
-[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
-[circleci-url]: https://circleci.com/gh/nestjs/nest
-
-  <p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
-    <p align="center">
-<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
-<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
-<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
-<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
-<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
-<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
-<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
-<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
-  <a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
-    <a href="https://opencollective.com/nest#sponsor"  target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
-  <a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
-</p>
-  <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
-  [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
-
-## Description
-
-[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
-
-## Project setup
-
-```bash
-$ npm install
-```
-
-## Compile and run the project
-
-```bash
-# development
-$ npm run start
-
-# watch mode
-$ npm run start:dev
-
-# production mode
-$ npm run start:prod
-```
-
-## Run tests
-
-```bash
-# unit tests
-$ npm run test
-
-# e2e tests
-$ npm run test:e2e
-
-# test coverage
-$ npm run test:cov
-```
-
-## Deployment
-
-When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
-
-If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
-
-```bash
-$ npm install -g mau
-$ mau deploy
-```
-
-With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
-
-## Resources
-
-Check out a few resources that may come in handy when working with NestJS:
-
-- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
-- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
-- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
-- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
-- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
-- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
-- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
-- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
-
-## Support
-
-Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
-
-## Stay in touch
-
-- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
-- Website - [https://nestjs.com](https://nestjs.com/)
-- Twitter - [@nestframework](https://twitter.com/nestframework)
-
-## License
-
-Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
+<p align="center">
+  <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
+</p>
+
+[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
+[circleci-url]: https://circleci.com/gh/nestjs/nest
+
+  <p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
+    <p align="center">
+<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
+<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
+<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
+<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
+<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
+<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
+<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
+<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
+  <a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
+    <a href="https://opencollective.com/nest#sponsor"  target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
+  <a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
+</p>
+  <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
+  [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
+
+## Description
+
+[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
+
+## Project setup
+
+```bash
+$ npm install
+```
+
+## Compile and run the project
+
+```bash
+# development
+$ npm run start
+
+# watch mode
+$ npm run start:dev
+
+# production mode
+$ npm run start:prod
+```
+
+## Run tests
+
+```bash
+# unit tests
+$ npm run test
+
+# e2e tests
+$ npm run test:e2e
+
+# test coverage
+$ npm run test:cov
+```
+
+## Deployment
+
+When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
+
+If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
+
+```bash
+$ npm install -g mau
+$ mau deploy
+```
+
+With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
+
+## Resources
+
+Check out a few resources that may come in handy when working with NestJS:
+
+- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
+- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
+- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
+- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
+- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
+- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
+- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
+- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
+
+## Support
+
+Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
+
+## Stay in touch
+
+- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
+- Website - [https://nestjs.com](https://nestjs.com/)
+- Twitter - [@nestframework](https://twitter.com/nestframework)
+
+## License
+
+Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

+ 36 - 36
node_app/eslint.config.mjs

@@ -1,36 +1,36 @@
-// @ts-check
-import eslint from '@eslint/js';
-import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
-import globals from 'globals';
-import tseslint from 'typescript-eslint';
-
-export default tseslint.config(
-  {
-    ignores: ['eslint.config.mjs'],
-  },
-  eslint.configs.recommended,
-  ...tseslint.configs.recommendedTypeChecked,
-  eslintPluginPrettierRecommended,
-  {
-    languageOptions: {
-      globals: {
-        ...globals.node,
-        ...globals.jest,
-      },
-      ecmaVersion: 5,
-      sourceType: 'module',
-      parserOptions: {
-        projectService: true,
-        tsconfigRootDir: import.meta.dirname,
-      },
-    },
-  },
-  {
-    rules: {
-      '@typescript-eslint/no-explicit-any': 'off',
-      '@typescript-eslint/no-floating-promises': 'warn',
-      '@typescript-eslint/no-unsafe-argument': 'warn',
-      '@typescript-eslint/no-unsafe-member-access': 'warn',
-    },
-  },
-);
+// @ts-check
+import eslint from '@eslint/js';
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+  {
+    ignores: ['eslint.config.mjs'],
+  },
+  eslint.configs.recommended,
+  ...tseslint.configs.recommendedTypeChecked,
+  eslintPluginPrettierRecommended,
+  {
+    languageOptions: {
+      globals: {
+        ...globals.node,
+        ...globals.jest,
+      },
+      ecmaVersion: 5,
+      sourceType: 'module',
+      parserOptions: {
+        projectService: true,
+        tsconfigRootDir: import.meta.dirname,
+      },
+    },
+  },
+  {
+    rules: {
+      '@typescript-eslint/no-explicit-any': 'off',
+      '@typescript-eslint/no-floating-promises': 'warn',
+      '@typescript-eslint/no-unsafe-argument': 'warn',
+      '@typescript-eslint/no-unsafe-member-access': 'warn',
+    },
+  },
+);

+ 8 - 8
node_app/nest-cli.json

@@ -1,8 +1,8 @@
-{
-  "$schema": "https://json.schemastore.org/nest-cli",
-  "collection": "@nestjs/schematics",
-  "sourceRoot": "src",
-  "compilerOptions": {
-    "deleteOutDir": true
-  }
-}
+{
+  "$schema": "https://json.schemastore.org/nest-cli",
+  "collection": "@nestjs/schematics",
+  "sourceRoot": "src",
+  "compilerOptions": {
+    "deleteOutDir": true
+  }
+}

+ 38 - 0
node_app/src/application/dto/responses/company-reservations-sum-response.dto.ts

@@ -0,0 +1,38 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class CompanyReservationsSumResponseDto {
+  @ApiProperty({
+    description: 'ID de la compagnie',
+    example: 1,
+  })
+  companyId: number;
+
+  @ApiProperty({
+    description: 'Montant total de toutes les réservations de la compagnie',
+    example: 125000.50,
+  })
+  totalAmount: number;
+
+  @ApiProperty({
+    description: 'Nombre total de réservations de la compagnie',
+    example: 45,
+  })
+  totalReservations: number;
+
+  @ApiProperty({
+    description: 'Montant moyen par réservation',
+    example: 2777.78,
+  })
+  averageAmountPerReservation: number;
+
+  constructor(partial: Partial<CompanyReservationsSumResponseDto>) {
+    Object.assign(this, partial);
+    
+    // Calcul automatique de la moyenne
+    if (this.totalAmount && this.totalReservations && this.totalReservations > 0) {
+      this.averageAmountPerReservation = Number((this.totalAmount / this.totalReservations).toFixed(2));
+    } else {
+      this.averageAmountPerReservation = 0;
+    }
+  }
+}

+ 19 - 0
node_app/src/application/dto/user-response.dto.ts

@@ -0,0 +1,19 @@
+export class UserResponseDto {
+  id: number;
+  email: string;
+  name: string;
+  otherName: string;
+  address: string;
+  telephone: string;
+  createdAt: Date;
+  updatedAt: Date;
+
+  constructor(partial: Partial<UserResponseDto>) {
+    Object.assign(this, partial);
+  }
+
+  static fromEntity(user: any): UserResponseDto {
+    const { password, ...userData } = user;
+    return new UserResponseDto(userData);
+  }
+}

+ 25 - 0
node_app/src/application/use-cases/reservation/get-company-reservations-sum.use-case.ts

@@ -0,0 +1,25 @@
+import { Injectable, Inject, NotFoundException } from '@nestjs/common';
+import { ReservationRepository } from '../../../infrastructure/database/repositories/reservation.repository';
+import { CompanyReservationsSumResponseDto } from '../../dto/responses/company-reservations-sum-response.dto';
+
+@Injectable()
+export class GetCompanyReservationsSumUseCase {
+  constructor(
+    @Inject('ReservationRepository')
+    private readonly reservationRepository: ReservationRepository,
+  ) {}
+
+  async execute(companyId: number): Promise<CompanyReservationsSumResponseDto> {
+    if (!companyId || companyId <= 0) {
+      throw new NotFoundException('ID de compagnie invalide');
+    }
+
+    const result = await this.reservationRepository.getCompanyReservationsSum(companyId);
+    
+    return new CompanyReservationsSumResponseDto({
+      companyId: result.companyId,
+      totalAmount: result.totalAmount,
+      totalReservations: result.totalReservations,
+    });
+  }
+}

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

@@ -289,4 +289,74 @@ export class ReservationRepository implements IBaseRepository<Reservation> {
       total,
     };
   }
+
+  /**
+   * Calcule la somme totale des réservations pour une compagnie donnée
+   * @param companyId ID de la compagnie
+   * @returns Objet contenant la somme totale et le nombre de réservations
+   */
+  async getCompanyReservationsSum(companyId: number): Promise<{
+    totalAmount: number;
+    totalReservations: number;
+    companyId: number;
+  }> {
+    // Récupération des réservations avec les informations du trajet et du véhicule
+    // Les informations de la compagnie se trouvent dans vehicule.company
+    // Seuls les trajets avec registrationClosed: true sont pris en compte
+    const reservations = await this.prisma.reservation.findMany({
+      where: {
+        trajet: {
+          vehicule: {
+            companyId: companyId,
+          },
+          registrationClosed: true, // Uniquement les trajets fermés
+        },
+      },
+      include: {
+        trajet: {
+          include: {
+            vehicule: {
+              include: {
+                company: true,
+              },
+            },
+          },
+        },
+      },
+    });
+
+    // Debug: Afficher les réservations trouvées
+    console.log(
+      `[DEBUG] Réservations trouvées pour companyId ${companyId}:`,
+      reservations.length,
+    );
+    if (reservations.length > 0) {
+      console.log(`[DEBUG] Première réservation:`, {
+        id: reservations[0].id,
+        trajetId: reservations[0].trajetId,
+        pricePerSeat: reservations[0].trajet?.pricePerSeat,
+        companyName: reservations[0].trajet?.vehicule?.company?.name,
+      });
+    }
+
+    // Calcul de la somme totale et du nombre de réservations
+    const totalAmount = reservations.reduce((sum, reservation) => {
+      const price = reservation.trajet?.pricePerSeat || 0;
+      console.log(
+        `[DEBUG] Réservation ID ${reservation.id}, pricePerSeat: ${price}`,
+      );
+      return sum + price;
+    }, 0);
+
+    const totalReservations = reservations.length;
+    console.log(
+      `[DEBUG] Total calculé: ${totalAmount}, Nombre de réservations: ${totalReservations}`,
+    );
+
+    return {
+      totalAmount,
+      totalReservations,
+      companyId,
+    };
+  }
 }

+ 33 - 0
node_app/src/presentation/controllers/reservation.controller.ts

@@ -30,7 +30,9 @@ import { FindReservationByIdUseCase } from '../../application/use-cases/reservat
 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 { GetCompanyReservationsSumUseCase } from '../../application/use-cases/reservation/get-company-reservations-sum.use-case';
 import { FindQrcodeUseCase } from '../../application/use-cases/qrcode/find-qrcode.use-case';
+import { CompanyReservationsSumResponseDto } from '../../application/dto/responses/company-reservations-sum-response.dto';
 import { Public } from '../decorators/public.decorator';
 import { Roles } from '../decorators/roles.decorator';
 import { UserRole } from 'src/domain/entities/user.entity';
@@ -47,6 +49,7 @@ export class ReservationController {
     private readonly updateReservationUseCase: UpdateReservationUseCase,
     private readonly deleteReservationUseCase: DeleteReservationUseCase,
     private readonly findReservationsByUserUseCase: FindReservationsByUserUseCase,
+    private readonly getCompanyReservationsSumUseCase: GetCompanyReservationsSumUseCase,
     private readonly findQrcodeUseCase: FindQrcodeUseCase,
     private readonly reservationMapper: ReservationMapper,
   ) {}
@@ -263,4 +266,34 @@ export class ReservationController {
       qrcode,
     };
   }
+
+  @Get('company/:companyId/sum')
+  @Roles(UserRole.ADMIN, UserRole.MANAGER)
+  @ApiOperation({
+    summary: "Récupérer la somme de toutes les réservations d'une compagnie",
+    description:
+      'Calcule le montant total, le nombre de réservations et la moyenne par réservation pour une compagnie donnée',
+  })
+  @ApiParam({
+    name: 'companyId',
+    description: 'ID de la compagnie',
+    type: 'number',
+    example: 1,
+  })
+  @ApiResponse({
+    status: 200,
+    description: 'Somme des réservations de la compagnie récupérée avec succès',
+    type: CompanyReservationsSumResponseDto,
+  })
+  @ApiResponse({
+    status: 404,
+    description: 'Compagnie non trouvée ou aucune réservation',
+  })
+  @ApiResponse({ status: 401, description: 'Non autorisé' })
+  @ApiResponse({ status: 403, description: 'Accès refusé - Rôle insuffisant' })
+  async getCompanyReservationsSum(
+    @Param('companyId', ParseIntPipe) companyId: number,
+  ): Promise<CompanyReservationsSumResponseDto> {
+    return await this.getCompanyReservationsSumUseCase.execute(companyId);
+  }
 }

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

@@ -6,6 +6,7 @@ import { FindReservationByIdUseCase } from '../../application/use-cases/reservat
 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 { GetCompanyReservationsSumUseCase } from '../../application/use-cases/reservation/get-company-reservations-sum.use-case';
 import { ReservationRepository } from '../../infrastructure/database/repositories/reservation.repository';
 import { TrajetRepository } from '../../infrastructure/database/repositories/trajet.repository';
 import { ReservationMapper } from '../../infrastructure/mappers/reservation.mapper';
@@ -29,6 +30,7 @@ import { CqrsModule } from '@nestjs/cqrs';
     UpdateReservationUseCase,
     DeleteReservationUseCase,
     FindReservationsByUserUseCase,
+    GetCompanyReservationsSumUseCase,
     {
       provide: 'ReservationRepository',
       useClass: ReservationRepository,
@@ -51,6 +53,7 @@ import { CqrsModule } from '@nestjs/cqrs';
     UpdateReservationUseCase,
     DeleteReservationUseCase,
     FindReservationsByUserUseCase,
+    GetCompanyReservationsSumUseCase,
     'ReservationRepository',
     'TrajetRepository',
     ReservationMapper,

+ 88 - 0
node_app/test-auth-status.js

@@ -0,0 +1,88 @@
+const { PrismaClient } = require('@prisma/client');
+
+const prisma = new PrismaClient();
+
+async function testAuthStatus() {
+  try {
+    console.log('🔍 Test de l\'état d\'authentification et des utilisateurs');
+    
+    // 1. Vérifier les utilisateurs existants
+    const users = await prisma.user.findMany({
+      include: {
+        company: true,
+      },
+    });
+    
+    console.log(`\n👥 Utilisateurs trouvés: ${users.length}`);
+    users.forEach(user => {
+      console.log(`  - User ${user.id}: ${user.email} (${user.name})`);
+      console.log(`    - Rôle: ${user.role}`);
+      console.log(`    - Compagnie: ${user.company ? `${user.company.name} (ID: ${user.company.id})` : 'Aucune'}`);
+      console.log(`    - Actif: ${user.isActive}`);
+      console.log('');
+    });
+    
+    // 2. Vérifier les compagnies
+    const companies = await prisma.company.findMany({
+      include: {
+        _count: {
+          select: {
+            employees: true,
+            vehicules: true,
+          },
+        },
+      },
+    });
+    
+    console.log(`\n🏢 Compagnies trouvées: ${companies.length}`);
+    companies.forEach(company => {
+      console.log(`  - Compagnie ${company.id}: ${company.name}`);
+      console.log(`    - Utilisateurs: ${company._count.users}`);
+      console.log(`    - Véhicules: ${company._count.vehicules}`);
+      console.log('');
+    });
+    
+    // 3. Suggestions pour les tests
+    console.log('💡 Suggestions pour tester:');
+    
+    if (users.length > 0) {
+      const userWithCompany = users.find(u => u.company);
+      if (userWithCompany) {
+        console.log(`  - Connectez-vous avec: ${userWithCompany.email}`);
+        console.log(`  - Compagnie associée: ${userWithCompany.company.name} (ID: ${userWithCompany.company.id})`);
+        console.log(`  - L'endpoint sera appelé: /api/reservations/company/${userWithCompany.company.id}/sum`);
+      } else {
+        console.log('  - Aucun utilisateur n\'a de compagnie associée');
+        console.log('  - Vous devez associer un utilisateur à une compagnie pour tester');
+      }
+    } else {
+      console.log('  - Aucun utilisateur trouvé');
+      console.log('  - Créez un utilisateur et associez-le à une compagnie');
+    }
+    
+    // 4. Test spécifique pour la compagnie 5
+    const company5 = companies.find(c => c.id === 5);
+    if (company5) {
+      console.log(`\n🎯 Test spécifique pour la compagnie 5 (${company5.name}):`);
+      const usersInCompany5 = users.filter(u => u.companyId === 5);
+      console.log(`  - Utilisateurs dans cette compagnie: ${usersInCompany5.length}`);
+      usersInCompany5.forEach(user => {
+        console.log(`    - ${user.email} (${user.name})`);
+      });
+      
+      if (usersInCompany5.length === 0) {
+        console.log('  - ⚠️  Aucun utilisateur associé à la compagnie 5');
+        console.log('  - Pour tester, connectez-vous avec un utilisateur de cette compagnie');
+      }
+    } else {
+      console.log('\n❌ Compagnie 5 non trouvée');
+    }
+    
+  } catch (error) {
+    console.error('❌ Erreur lors du test:', error);
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+testAuthStatus();

+ 178 - 0
node_app/test-frontend-integration.js

@@ -0,0 +1,178 @@
+const { PrismaClient } = require('@prisma/client');
+
+const prisma = new PrismaClient();
+
+async function testCompany5Integration() {
+  try {
+    console.log('🔍 Test d\'intégration frontend - endpoint reservations/company/5/sum');
+    
+    const companyId = 5; // test-company
+    
+    // 1. Vérifier la compagnie 5
+    const company = await prisma.company.findUnique({
+      where: { id: companyId },
+      include: {
+        vehicules: {
+          include: {
+            trajets: {
+              include: {
+                _count: {
+                  select: {
+                    reservations: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    });
+    
+    if (!company) {
+      console.log('❌ Compagnie 5 non trouvée');
+      return;
+    }
+    
+    console.log(`\n🏢 Compagnie ${companyId}: ${company.name}`);
+    console.log(`  - Véhicules: ${company.vehicules.length}`);
+    
+    if (company.vehicules.length > 0) {
+      company.vehicules.forEach(vehicule => {
+        console.log(`  - Véhicule ${vehicule.id}: ${vehicule.trajets.length} trajets`);
+        vehicule.trajets.forEach(trajet => {
+          console.log(`    - Trajet ${trajet.id}: ${trajet._count.reservations} réservations, fermé=${trajet.registrationClosed}`);
+        });
+      });
+    }
+    
+    // 2. Simuler l'appel de l'endpoint (comme le ferait le frontend)
+    console.log(`\n🧪 Test de l'endpoint pour la compagnie ${companyId}:`);
+    
+    const reservations = await prisma.reservation.findMany({
+      where: {
+        trajet: {
+          vehicule: {
+            companyId: companyId,
+          },
+          registrationClosed: true, // Uniquement les trajets fermés
+        },
+      },
+      include: {
+        trajet: {
+          select: {
+            id: true,
+            pricePerSeat: true,
+            registrationClosed: true,
+            vehicule: {
+              select: {
+                company: {
+                  select: {
+                    name: true,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    });
+    
+    const totalAmount = reservations.reduce(
+      (sum, reservation) => sum + (reservation.trajet?.pricePerSeat || 0),
+      0,
+    );
+    
+    const result = {
+      companyId: companyId,
+      totalReservations: reservations.length,
+      totalAmount: totalAmount,
+    };
+    
+    console.log('📊 Résultat de l\'endpoint pour la compagnie 5:');
+    console.log(JSON.stringify(result, null, 2));
+    
+    // 3. Si aucune réservation, créer un trajet fermé pour le test
+    if (reservations.length === 0) {
+      console.log('\n🔧 Aucune réservation trouvée, création d\'un test...');
+      
+      // Vérifier s'il y a des trajets pour cette compagnie
+      const allTrajets = await prisma.trajet.findMany({
+        where: {
+          vehicule: {
+            companyId: companyId,
+          },
+        },
+      });
+      
+      if (allTrajets.length > 0) {
+        // Fermer le premier trajet et créer quelques réservations de test
+        const trajetId = allTrajets[0].id;
+        
+        console.log(`  - Fermeture du trajet ${trajetId} pour le test...`);
+        await prisma.trajet.update({
+          where: { id: trajetId },
+          data: { registrationClosed: true },
+        });
+        
+        // Refaire le test
+        const newReservations = await prisma.reservation.findMany({
+          where: {
+            trajet: {
+              vehicule: {
+                companyId: companyId,
+              },
+              registrationClosed: true,
+            },
+          },
+          include: {
+            trajet: {
+              select: {
+                pricePerSeat: true,
+              },
+            },
+          },
+        });
+        
+        const newTotalAmount = newReservations.reduce(
+          (sum, reservation) => sum + (reservation.trajet?.pricePerSeat || 0),
+          0,
+        );
+        
+        const newResult = {
+          companyId: companyId,
+          totalReservations: newReservations.length,
+          totalAmount: newTotalAmount,
+        };
+        
+        console.log('📊 Nouveau résultat après fermeture du trajet:');
+        console.log(JSON.stringify(newResult, null, 2));
+        
+        // Remettre en ouvert
+        await prisma.trajet.update({
+          where: { id: trajetId },
+          data: { registrationClosed: false },
+        });
+        
+        console.log('  - Trajet remis en ouvert');
+      } else {
+        console.log('  - Aucun trajet trouvé pour cette compagnie');
+      }
+    }
+    
+    // 4. Test de la logique frontend
+    console.log('\n✅ Test de la logique frontend pour la compagnie 5:');
+    console.log(`  - URL appelée: /api/reservations/company/5/sum`);
+    console.log(`  - reservationsSumResult?.totalAmount: ${result.totalAmount}`);
+    console.log(`  - reservationsSum.value sera défini à: ${result.totalAmount}`);
+    console.log(`  - Type: ${typeof result.totalAmount} (attendu: number)`);
+    
+    console.log('\n✅ Test d\'intégration terminé avec succès !');
+    
+  } catch (error) {
+    console.error('❌ Erreur lors du test:', error);
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+testCompany5Integration();

+ 146 - 0
node_app/test-reservation-sum.js

@@ -0,0 +1,146 @@
+const { PrismaClient } = require('@prisma/client');
+
+const prisma = new PrismaClient();
+
+async function testReservationSum() {
+  try {
+    console.log('🔍 Test de la condition registrationClosed avec fermeture de trajet...');
+    
+    const companyId = 1; // IMANA TRANSPORT
+    
+    // 1. Fermer le trajet 1 pour le test
+    console.log('\n🔒 Fermeture du trajet 1 pour le test...');
+    await prisma.trajet.update({
+      where: { id: 1 },
+      data: { registrationClosed: true },
+    });
+    
+    // 2. Voir tous les trajets de la compagnie avec leur statut
+    const allTrajets = await prisma.trajet.findMany({
+      where: {
+        vehicule: {
+          companyId: companyId,
+        },
+      },
+      include: {
+        vehicule: {
+          include: {
+            company: true,
+          },
+        },
+        _count: {
+          select: {
+            reservations: true,
+          },
+        },
+      },
+    });
+    
+    console.log(`\n🚌 Trajets de la compagnie ${companyId} (${allTrajets[0]?.vehicule?.company?.name}):`);
+    allTrajets.forEach(trajet => {
+      console.log(`  - Trajet ${trajet.id}: fermé=${trajet.registrationClosed}, prix=${trajet.pricePerSeat}€, réservations=${trajet._count.reservations}`);
+    });
+    
+    // 3. Test SANS condition registrationClosed (tous les trajets)
+    const allReservations = await prisma.reservation.findMany({
+      where: {
+        trajet: {
+          vehicule: {
+            companyId: companyId,
+          },
+        },
+      },
+      include: {
+        trajet: {
+          select: {
+            pricePerSeat: true,
+            registrationClosed: true,
+          },
+        },
+      },
+    });
+    
+    const totalAmountAll = allReservations.reduce(
+      (sum, reservation) => sum + (reservation.trajet?.pricePerSeat || 0),
+      0,
+    );
+    
+    console.log(`\n📊 TOUS les trajets (ouverts + fermés):`);
+    console.log(`  💰 ${allReservations.length} réservations, Total: ${totalAmountAll}€`);
+    
+    // 4. Test AVEC condition registrationClosed: true (trajets fermés uniquement)
+    const closedReservations = await prisma.reservation.findMany({
+      where: {
+        trajet: {
+          vehicule: {
+            companyId: companyId,
+          },
+          registrationClosed: true, // Uniquement les trajets fermés
+        },
+      },
+      include: {
+        trajet: {
+          select: {
+            pricePerSeat: true,
+            registrationClosed: true,
+          },
+        },
+      },
+    });
+    
+    const totalAmountClosed = closedReservations.reduce(
+      (sum, reservation) => sum + (reservation.trajet?.pricePerSeat || 0),
+      0,
+    );
+    
+    console.log(`\n📊 TRAJETS FERMÉS uniquement (registrationClosed: true):`);
+    console.log(`  💰 ${closedReservations.length} réservations, Total: ${totalAmountClosed}€`);
+    
+    // 5. Test AVEC condition registrationClosed: false (trajets ouverts uniquement)
+    const openReservations = await prisma.reservation.findMany({
+      where: {
+        trajet: {
+          vehicule: {
+            companyId: companyId,
+          },
+          registrationClosed: false, // Uniquement les trajets ouverts
+        },
+      },
+      include: {
+        trajet: {
+          select: {
+            pricePerSeat: true,
+            registrationClosed: true,
+          },
+        },
+      },
+    });
+    
+    const totalAmountOpen = openReservations.reduce(
+      (sum, reservation) => sum + (reservation.trajet?.pricePerSeat || 0),
+      0,
+    );
+    
+    console.log(`\n📊 TRAJETS OUVERTS uniquement (registrationClosed: false):`);
+    console.log(`  💰 ${openReservations.length} réservations, Total: ${totalAmountOpen}€`);
+    
+    // 6. Vérification
+    console.log(`\n✅ Vérification: ${totalAmountClosed + totalAmountOpen} = ${totalAmountAll} (fermés + ouverts = tous)`);
+    
+    // 7. Remettre le trajet 1 en ouvert pour ne pas affecter les autres tests
+    console.log('\n🔓 Remise en ouvert du trajet 1...');
+    await prisma.trajet.update({
+      where: { id: 1 },
+      data: { registrationClosed: false },
+    });
+    
+    console.log('✅ Test terminé, trajet 1 remis en ouvert.');
+    
+  } catch (error) {
+    console.error('❌ Erreur:', error);
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+testReservationSum();

+ 9 - 9
node_app/test/jest-e2e.json

@@ -1,9 +1,9 @@
-{
-  "moduleFileExtensions": ["js", "json", "ts"],
-  "rootDir": ".",
-  "testEnvironment": "node",
-  "testRegex": ".e2e-spec.ts$",
-  "transform": {
-    "^.+\\.(t|j)s$": "ts-jest"
-  }
-}
+{
+  "moduleFileExtensions": ["js", "json", "ts"],
+  "rootDir": ".",
+  "testEnvironment": "node",
+  "testRegex": ".e2e-spec.ts$",
+  "transform": {
+    "^.+\\.(t|j)s$": "ts-jest"
+  }
+}

+ 4 - 4
node_app/tsconfig.build.json

@@ -1,4 +1,4 @@
-{
-  "extends": "./tsconfig.json",
-  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
-}
+{
+  "extends": "./tsconfig.json",
+  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
+}

+ 10 - 10
postgres-init.sql

@@ -1,10 +1,10 @@
--- init.sql
-
--- Créer un rôle 'root' avec un mot de passe
-CREATE ROLE root WITH LOGIN PASSWORD 'rootpassword';
-
--- Donner des permissions d'administrateur au rôle 'root'
-ALTER ROLE root CREATEDB CREATEROLE SUPERUSER;
-
--- Optionnellement, créer la base de données et l'attribuer au rôle 'root'
-CREATE DATABASE sonar OWNER root;
+-- init.sql
+
+-- Créer un rôle 'root' avec un mot de passe
+CREATE ROLE root WITH LOGIN PASSWORD 'rootpassword';
+
+-- Donner des permissions d'administrateur au rôle 'root'
+ALTER ROLE root CREATEDB CREATEROLE SUPERUSER;
+
+-- Optionnellement, créer la base de données et l'attribuer au rôle 'root'
+CREATE DATABASE sonar OWNER root;

+ 23 - 23
prometheus/prometheus.yml

@@ -1,24 +1,24 @@
-global:
-  scrape_interval: 15s  # Fréquence de collecte des métriques (par défaut : 15s)
-
-scrape_configs:
-  # 1️⃣ Scraper Prometheus lui-même
-  - job_name: 'prometheus'
-    static_configs:
-      - targets: ['localhost:9090']
-
-  # 2️⃣ Scraper Node Exporter (collecte des métriques système)
-  - job_name: 'node_exporter'
-    static_configs:
-      - targets: ['node-exporter:9100']
-
-  # 3️⃣ Scraper Grafana (optionnel, pour surveiller Grafana)
-  - job_name: 'grafana'
-    static_configs:
-      - targets: ['grafana:3000']
-
-  # 4️⃣ Scraper d'autres applications (exemple)
-  # Ajoute ici tes applications qui exposent des métriques
-  # - job_name: 'my_app'
-  #   static_configs:
+global:
+  scrape_interval: 15s  # Fréquence de collecte des métriques (par défaut : 15s)
+
+scrape_configs:
+  # 1️⃣ Scraper Prometheus lui-même
+  - job_name: 'prometheus'
+    static_configs:
+      - targets: ['localhost:9090']
+
+  # 2️⃣ Scraper Node Exporter (collecte des métriques système)
+  - job_name: 'node_exporter'
+    static_configs:
+      - targets: ['node-exporter:9100']
+
+  # 3️⃣ Scraper Grafana (optionnel, pour surveiller Grafana)
+  - job_name: 'grafana'
+    static_configs:
+      - targets: ['grafana:3000']
+
+  # 4️⃣ Scraper d'autres applications (exemple)
+  # Ajoute ici tes applications qui exposent des métriques
+  # - job_name: 'my_app'
+  #   static_configs:
   #     - targets: ['my_app:8080']