Bläddra i källkod

(feat budget)

Brin AMONKONAN 1 år sedan
förälder
incheckning
1e6b8c8543

+ 6 - 6
app_transport_front/src/router/index.ts

@@ -67,12 +67,12 @@ const routes: Array<RouteRecordRaw> = [
     component: () => import('../views/StationCompanyView.vue'),
     meta: { requiresAuth: true, roles: [Role.Admin, Role.Manager] }
   },
-  // {
-  //   path: '/budget',
-  //   name: 'budget',
-  //   component: () => import('../views/BudgetView.vue'),
-  //   meta: { requiresAuth: true }
-  // },
+  {
+    path: '/budget',
+    name: 'budget',
+    component: () => import('../views/BudgetView.vue'),
+    meta: { requiresAuth: true, roles: [Role.Admin, Role.Manager] }
+  },
   // {
   //   path: '/settings',
   //   name: 'settings',

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

@@ -0,0 +1,291 @@
+import { authenticatedFetch } from '@/services/authService'
+
+const API_URL = 'http://localhost:3001/api'
+
+export interface Subscription {
+  id: number
+  name: string
+  description: string
+  price: number
+  duration: number // en jours
+  features: string[]
+  isActive: boolean
+  startDate?: string
+  endDate?: string
+  companyId: number
+}
+
+export interface Balance {
+  id: number
+  companyId: number
+  currentBalance: number
+  currency: string
+  lastUpdated: string
+  transactions: Transaction[]
+}
+
+export interface Transaction {
+  id: number
+  type: 'credit' | 'debit'
+  amount: number
+  description: string
+  date: string
+  reference?: string
+  status: 'pending' | 'completed' | 'failed'
+}
+
+export interface PaymentRequest {
+  id: number
+  companyId: number
+  amount: number
+  description: string
+  requestDate: string
+  status: 'pending' | 'approved' | 'rejected' | 'paid'
+  approvedBy?: string
+  approvedDate?: string
+  paymentMethod?: string
+  reference?: string
+}
+
+export interface Receipt {
+  id: number
+  companyId: number
+  transactionId: number
+  receiptNumber: string
+  amount: number
+  date: string
+  description: string
+  paymentMethod: string
+  downloadUrl?: string
+}
+
+export default {
+  // Gestion des abonnements
+  async getSubscriptions(companyId?: number): Promise<Subscription[]> {
+    try {
+      const url = companyId 
+        ? `${API_URL}/budget/subscriptions?companyId=${companyId}`
+        : `${API_URL}/budget/subscriptions`
+      
+      const response = await authenticatedFetch(url)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des abonnements')
+      }
+      const result = await response.json()
+      return result.data || []
+    } catch (error) {
+      console.error('Erreur:', error)
+      return []
+    }
+  },
+
+  async getActiveSubscription(companyId: number): Promise<Subscription | null> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/subscriptions/active/${companyId}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération de l\'abonnement actif')
+      }
+      const result = await response.json()
+      return result.data || null
+    } catch (error) {
+      console.error('Erreur:', error)
+      return null
+    }
+  },
+
+  async subscribeToService(companyId: number, subscriptionId: number): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/subscriptions/subscribe`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ companyId, subscriptionId })
+      })
+      
+      const result = await response.json()
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Abonnement souscrit avec succès' : 'Erreur lors de la souscription')
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return { success: false, message: 'Erreur lors de la souscription' }
+    }
+  },
+
+  // Gestion du solde
+  async getBalance(companyId: number): Promise<Balance | null> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/balance/${companyId}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération du solde')
+      }
+      const result = await response.json()
+      return result.data || null
+    } catch (error) {
+      console.error('Erreur:', error)
+      return null
+    }
+  },
+
+  async getTransactions(companyId: number, page: number = 1, limit: number = 10): Promise<{ transactions: Transaction[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/transactions/${companyId}?page=${page}&limit=${limit}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des transactions')
+      }
+      const result = await response.json()
+      return {
+        transactions: result.data?.data || [],
+        pagination: {
+          total: result.data?.total || 0,
+          page: result.data?.page || 1,
+          limit: result.data?.limit || 10,
+          totalPages: result.data?.totalPages || 1
+        }
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return {
+        transactions: [],
+        pagination: { total: 0, page: 1, limit: 10, totalPages: 1 }
+      }
+    }
+  },
+
+  // Gestion des demandes de paiement
+  async getPaymentRequests(companyId: number, page: number = 1, limit: number = 10): Promise<{ requests: PaymentRequest[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/payment-requests/${companyId}?page=${page}&limit=${limit}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des demandes de paiement')
+      }
+      const result = await response.json()
+      return {
+        requests: result.data?.data || [],
+        pagination: {
+          total: result.data?.total || 0,
+          page: result.data?.page || 1,
+          limit: result.data?.limit || 10,
+          totalPages: result.data?.totalPages || 1
+        }
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return {
+        requests: [],
+        pagination: { total: 0, page: 1, limit: 10, totalPages: 1 }
+      }
+    }
+  },
+
+  async createPaymentRequest(request: Omit<PaymentRequest, 'id' | 'requestDate' | 'status'>): Promise<{ success: boolean, message: string, data?: PaymentRequest }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/payment-requests`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(request)
+      })
+      
+      const result = await response.json()
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Demande de paiement créée avec succès' : 'Erreur lors de la création'),
+        data: result.data || null
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return { success: false, message: 'Erreur lors de la création de la demande' }
+    }
+  },
+
+  async updatePaymentRequestStatus(requestId: number, status: PaymentRequest['status'], approvedBy?: string): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/payment-requests/${requestId}/status`, {
+        method: 'PATCH',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ status, approvedBy })
+      })
+      
+      const result = await response.json()
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Statut mis à jour avec succès' : 'Erreur lors de la mise à jour')
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return { success: false, message: 'Erreur lors de la mise à jour du statut' }
+    }
+  },
+
+  // Gestion des reçus
+  async getReceipts(companyId: number, page: number = 1, limit: number = 10): Promise<{ receipts: Receipt[], pagination: { total: number, page: number, limit: number, totalPages: number } }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/receipts/${companyId}?page=${page}&limit=${limit}`)
+      if (!response.ok) {
+        throw new Error('Erreur lors de la récupération des reçus')
+      }
+      const result = await response.json()
+      return {
+        receipts: result.data?.data || [],
+        pagination: {
+          total: result.data?.total || 0,
+          page: result.data?.page || 1,
+          limit: result.data?.limit || 10,
+          totalPages: result.data?.totalPages || 1
+        }
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return {
+        receipts: [],
+        pagination: { total: 0, page: 1, limit: 10, totalPages: 1 }
+      }
+    }
+  },
+
+  async downloadReceipt(receiptId: number): Promise<{ success: boolean, message: string, url?: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/receipts/${receiptId}/download`)
+      if (!response.ok) {
+        throw new Error('Erreur lors du téléchargement du reçu')
+      }
+      const result = await response.json()
+      return {
+        success: true,
+        message: 'Reçu téléchargé avec succès',
+        url: result.downloadUrl
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return { success: false, message: 'Erreur lors du téléchargement' }
+    }
+  },
+
+  // Méthodes utilitaires
+  async addFunds(companyId: number, amount: number, paymentMethod: string): Promise<{ success: boolean, message: string }> {
+    try {
+      const response = await authenticatedFetch(`${API_URL}/budget/balance/add-funds`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ companyId, amount, paymentMethod })
+      })
+      
+      const result = await response.json()
+      return {
+        success: response.ok,
+        message: result.message || (response.ok ? 'Fonds ajoutés avec succès' : 'Erreur lors de l\'ajout de fonds')
+      }
+    } catch (error) {
+      console.error('Erreur:', error)
+      return { success: false, message: 'Erreur lors de l\'ajout de fonds' }
+    }
+  }
+}

+ 773 - 0
app_transport_front/src/views/BudgetView.vue

@@ -0,0 +1,773 @@
+<template>
+  <div class="flex flex-row h-full bg-white">
+    <!-- Sidebar -->
+    <div class="flex">
+      <Sidebar 
+        :userName="useUserStore().name" 
+        :userRole="useUserStore().userRole" 
+        userAvatar="https://randomuser.me/api/portraits/men/1.jpg"
+      />
+    </div>
+    
+    <div class="flex-1 mx-8">
+      <!-- TopBar -->
+      <TopBar 
+        userAvatar="https://randomuser.me/api/portraits/men/1.jpg"
+        :notificationCount="3"
+        :messageCount="5"
+        @search="handleSearch"
+        @toggle-notifications="handleNotifications"
+        @toggle-messages="handleMessages"
+        @toggle-user-menu="handleUserMenu"
+      />
+      
+      <!-- Contenu principal -->
+      <div class="p-5 max-w-7xl mx-auto">
+        <div class="flex justify-between items-center mb-6">
+          <h1 class="text-3xl font-bold text-gray-800">Gestion Budget</h1>
+          <div class="flex gap-3">
+            <button 
+              class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
+              @click="showAddFundsModal = true"
+            >
+              <i class="fas fa-plus"></i>
+              Ajouter des fonds
+            </button>
+            <button 
+              class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
+              @click="showPaymentRequestModal = true"
+            >
+              <i class="fas fa-money-bill-wave"></i>
+              Demande de paiement
+            </button>
+          </div>
+        </div>
+
+        <!-- Onglets -->
+        <div class="mb-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="[
+                  'py-2 px-1 border-b-2 font-medium text-sm transition-colors',
+                  activeTab === tab.id
+                    ? 'border-blue-500 text-blue-600'
+                    : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+                ]"
+              >
+                <i :class="tab.icon" class="mr-2"></i>
+                {{ tab.name }}
+              </button>
+            </nav>
+          </div>
+        </div>
+
+        <!-- Contenu des onglets -->
+        <div class="tab-content">
+          <!-- Onglet Abonnements -->
+          <div v-if="activeTab === 'subscriptions'" class="space-y-6">
+            <div class="bg-white rounded-lg shadow-sm border p-6">
+              <h2 class="text-xl font-semibold text-gray-800 mb-4">Abonnement Actuel</h2>
+              <div v-if="activeSubscription" class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 border border-blue-200">
+                <div class="flex justify-between items-start">
+                  <div>
+                    <h3 class="text-lg font-semibold text-blue-900">{{ activeSubscription.name }}</h3>
+                    <p class="text-blue-700 mt-1">{{ activeSubscription.description }}</p>
+                    <div class="mt-3 space-y-1">
+                      <p class="text-sm text-blue-600">
+                        <i class="fas fa-calendar mr-2"></i>
+                        Du {{ formatDate(activeSubscription.startDate) }} au {{ formatDate(activeSubscription.endDate) }}
+                      </p>
+                      <p class="text-sm text-blue-600">
+                        <i class="fas fa-euro-sign mr-2"></i>
+                        {{ activeSubscription.price }}€ / {{ activeSubscription.duration }} jours
+                      </p>
+                    </div>
+                  </div>
+                  <span class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
+                    Actif
+                  </span>
+                </div>
+                <div class="mt-4">
+                  <h4 class="text-sm font-medium text-blue-900 mb-2">Fonctionnalités incluses :</h4>
+                  <ul class="text-sm text-blue-700 space-y-1">
+                    <li v-for="feature in activeSubscription.features" :key="feature" class="flex items-center">
+                      <i class="fas fa-check text-green-500 mr-2"></i>
+                      {{ feature }}
+                    </li>
+                  </ul>
+                </div>
+              </div>
+              <div v-else class="text-center py-8 text-gray-500">
+                <i class="fas fa-subscription text-4xl mb-3 text-gray-300"></i>
+                <p class="text-lg">Aucun abonnement actif</p>
+                <p class="text-sm">Souscrivez à un plan pour accéder aux fonctionnalités premium</p>
+              </div>
+            </div>
+
+            <div class="bg-white rounded-lg shadow-sm border p-6">
+              <h2 class="text-xl font-semibold text-gray-800 mb-4">Plans Disponibles</h2>
+              <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+                <div 
+                  v-for="subscription in availableSubscriptions" 
+                  :key="subscription.id"
+                  class="border rounded-lg p-6 hover:shadow-md transition-shadow"
+                  :class="subscription.isActive ? 'border-blue-500 bg-blue-50' : 'border-gray-200'"
+                >
+                  <h3 class="text-lg font-semibold text-gray-800">{{ subscription.name }}</h3>
+                  <p class="text-gray-600 mt-2 text-sm">{{ subscription.description }}</p>
+                  <div class="mt-4">
+                    <span class="text-2xl font-bold text-gray-900">{{ subscription.price }}€</span>
+                    <span class="text-gray-500">/ {{ subscription.duration }} jours</span>
+                  </div>
+                  <ul class="mt-4 space-y-2">
+                    <li v-for="feature in subscription.features" :key="feature" class="text-sm text-gray-600 flex items-center">
+                      <i class="fas fa-check text-green-500 mr-2"></i>
+                      {{ feature }}
+                    </li>
+                  </ul>
+                  <button 
+                    v-if="!subscription.isActive"
+                    @click="subscribeToService(subscription.id)"
+                    class="w-full mt-6 bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-lg transition-colors"
+                  >
+                    Souscrire
+                  </button>
+                  <div v-else class="w-full mt-6 bg-green-100 text-green-800 py-2 px-4 rounded-lg text-center text-sm font-medium">
+                    Abonnement actuel
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- Onglet Solde -->
+          <div v-if="activeTab === 'balance'" class="space-y-6">
+            <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
+              <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>
+                  </div>
+                  <i class="fas fa-wallet text-3xl text-green-200"></i>
+                </div>
+              </div>
+              <div class="bg-gradient-to-r from-blue-400 to-blue-600 rounded-lg p-6 text-white">
+                <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>
+                  </div>
+                  <i class="fas fa-exchange-alt text-3xl text-blue-200"></i>
+                </div>
+              </div>
+              <div class="bg-gradient-to-r from-purple-400 to-purple-600 rounded-lg p-6 text-white">
+                <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>
+                  </div>
+                  <i class="fas fa-clock text-3xl text-purple-200"></i>
+                </div>
+              </div>
+            </div>
+
+            <div class="bg-white rounded-lg shadow-sm border p-6">
+              <div class="flex justify-between items-center mb-4">
+                <h2 class="text-xl font-semibold text-gray-800">Historique des Transactions</h2>
+                <div class="flex gap-2">
+                  <select v-model="transactionFilter" class="border border-gray-300 rounded-lg px-3 py-2 text-sm">
+                    <option value="">Tous les types</option>
+                    <option value="credit">Crédits</option>
+                    <option value="debit">Débits</option>
+                  </select>
+                </div>
+              </div>
+              
+              <div class="overflow-x-auto">
+                <table class="w-full">
+                  <thead>
+                    <tr class="border-b border-gray-200">
+                      <th class="text-left py-3 px-4 font-semibold text-gray-700">Date</th>
+                      <th class="text-left py-3 px-4 font-semibold text-gray-700">Type</th>
+                      <th class="text-left py-3 px-4 font-semibold text-gray-700">Description</th>
+                      <th class="text-right py-3 px-4 font-semibold text-gray-700">Montant</th>
+                      <th class="text-center py-3 px-4 font-semibold text-gray-700">Statut</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr v-for="transaction in filteredTransactions" :key="transaction.id" class="border-b border-gray-100 hover:bg-gray-50">
+                      <td class="py-3 px-4 text-sm text-gray-600">{{ formatDate(transaction.date) }}</td>
+                      <td class="py-3 px-4">
+                        <span :class="[
+                          'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
+                          transaction.type === 'credit' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
+                        ]">
+                          <i :class="transaction.type === 'credit' ? 'fas fa-arrow-up' : 'fas fa-arrow-down'" class="mr-1"></i>
+                          {{ transaction.type === 'credit' ? 'Crédit' : 'Débit' }}
+                        </span>
+                      </td>
+                      <td class="py-3 px-4 text-sm text-gray-900">{{ transaction.description }}</td>
+                      <td class="py-3 px-4 text-sm text-right font-medium" :class="transaction.type === 'credit' ? 'text-green-600' : 'text-red-600'">
+                        {{ transaction.type === 'credit' ? '+' : '-' }}{{ formatCurrency(transaction.amount) }}
+                      </td>
+                      <td class="py-3 px-4 text-center">
+                        <span :class="[
+                          'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
+                          transaction.status === 'completed' ? 'bg-green-100 text-green-800' :
+                          transaction.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
+                          'bg-red-100 text-red-800'
+                        ]">
+                          {{ getStatusLabel(transaction.status) }}
+                        </span>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+              
+              <div v-if="filteredTransactions.length === 0" class="text-center py-8 text-gray-500">
+                <i class="fas fa-receipt text-4xl mb-3 text-gray-300"></i>
+                <p>Aucune transaction trouvée</p>
+              </div>
+            </div>
+          </div>
+
+          <!-- Onglet Demandes de Paiement -->
+          <div v-if="activeTab === 'payment-requests'" class="space-y-6">
+            <div class="bg-white rounded-lg shadow-sm border p-6">
+              <div class="flex justify-between items-center mb-4">
+                <h2 class="text-xl font-semibold text-gray-800">Demandes de Paiement</h2>
+                <div class="flex gap-2">
+                  <select v-model="paymentRequestFilter" class="border border-gray-300 rounded-lg px-3 py-2 text-sm">
+                    <option value="">Tous les statuts</option>
+                    <option value="pending">En attente</option>
+                    <option value="approved">Approuvées</option>
+                    <option value="rejected">Rejetées</option>
+                    <option value="paid">Payées</option>
+                  </select>
+                </div>
+              </div>
+              
+              <div class="overflow-x-auto">
+                <table class="w-full">
+                  <thead>
+                    <tr class="border-b border-gray-200">
+                      <th class="text-left py-3 px-4 font-semibold text-gray-700">Date</th>
+                      <th class="text-left py-3 px-4 font-semibold text-gray-700">Description</th>
+                      <th class="text-right py-3 px-4 font-semibold text-gray-700">Montant</th>
+                      <th class="text-center py-3 px-4 font-semibold text-gray-700">Statut</th>
+                      <th class="text-center py-3 px-4 font-semibold text-gray-700">Actions</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr v-for="request in filteredPaymentRequests" :key="request.id" class="border-b border-gray-100 hover:bg-gray-50">
+                      <td class="py-3 px-4 text-sm text-gray-600">{{ formatDate(request.requestDate) }}</td>
+                      <td class="py-3 px-4 text-sm text-gray-900">{{ request.description }}</td>
+                      <td class="py-3 px-4 text-sm text-right font-medium text-gray-900">{{ formatCurrency(request.amount) }}</td>
+                      <td class="py-3 px-4 text-center">
+                        <span :class="[
+                          'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
+                          request.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
+                          request.status === 'approved' ? 'bg-blue-100 text-blue-800' :
+                          request.status === 'paid' ? 'bg-green-100 text-green-800' :
+                          'bg-red-100 text-red-800'
+                        ]">
+                          {{ getPaymentStatusLabel(request.status) }}
+                        </span>
+                      </td>
+                      <td class="py-3 px-4 text-center">
+                        <div class="flex justify-center gap-2">
+                          <button 
+                            @click="viewPaymentRequest(request)"
+                            class="text-blue-600 hover:text-blue-800 text-sm"
+                          >
+                            <i class="fas fa-eye"></i>
+                          </button>
+                          <button 
+                            v-if="request.status === 'pending'"
+                            @click="editPaymentRequest(request)"
+                            class="text-yellow-600 hover:text-yellow-800 text-sm"
+                          >
+                            <i class="fas fa-edit"></i>
+                          </button>
+                        </div>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+              
+              <div v-if="filteredPaymentRequests.length === 0" class="text-center py-8 text-gray-500">
+                <i class="fas fa-money-bill-wave text-4xl mb-3 text-gray-300"></i>
+                <p>Aucune demande de paiement trouvée</p>
+              </div>
+            </div>
+          </div>
+
+          <!-- Onglet Reçus -->
+          <div v-if="activeTab === 'receipts'" class="space-y-6">
+            <div class="bg-white rounded-lg shadow-sm border p-6">
+              <h2 class="text-xl font-semibold text-gray-800 mb-4">Reçus et Factures</h2>
+              
+              <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+                <div 
+                  v-for="receipt in receipts" 
+                  :key="receipt.id"
+                  class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
+                >
+                  <div class="flex items-start justify-between mb-3">
+                    <div class="flex items-center">
+                      <i class="fas fa-file-invoice text-blue-500 text-2xl mr-3"></i>
+                      <div>
+                        <h3 class="font-medium text-gray-900">{{ receipt.receiptNumber }}</h3>
+                        <p class="text-sm text-gray-500">{{ formatDate(receipt.date) }}</p>
+                      </div>
+                    </div>
+                    <span class="text-lg font-semibold text-gray-900">{{ formatCurrency(receipt.amount) }}</span>
+                  </div>
+                  
+                  <p class="text-sm text-gray-600 mb-3">{{ receipt.description }}</p>
+                  
+                  <div class="flex items-center justify-between text-xs text-gray-500 mb-3">
+                    <span>{{ receipt.paymentMethod }}</span>
+                  </div>
+                  
+                  <button 
+                    @click="downloadReceipt(receipt.id)"
+                    class="w-full bg-blue-50 hover:bg-blue-100 text-blue-600 py-2 px-3 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
+                  >
+                    <i class="fas fa-download"></i>
+                    Télécharger
+                  </button>
+                </div>
+              </div>
+              
+              <div v-if="receipts.length === 0" class="text-center py-8 text-gray-500">
+                <i class="fas fa-file-invoice text-4xl mb-3 text-gray-300"></i>
+                <p>Aucun reçu disponible</p>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Modal Ajout de fonds -->
+    <div v-if="showAddFundsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+      <div class="bg-white rounded-lg p-6 w-full max-w-md">
+        <h3 class="text-lg font-semibold text-gray-800 mb-4">Ajouter des fonds</h3>
+        <form @submit.prevent="addFunds">
+          <div class="mb-4">
+            <label class="block text-sm font-medium text-gray-700 mb-2">Montant (€)</label>
+            <input 
+              type="number" 
+              v-model="addFundsForm.amount" 
+              min="1" 
+              step="0.01" 
+              required 
+              class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+            >
+          </div>
+          <div class="mb-6">
+            <label class="block text-sm font-medium text-gray-700 mb-2">Méthode de paiement</label>
+            <select 
+              v-model="addFundsForm.paymentMethod" 
+              required 
+              class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+            >
+              <option value="">Sélectionner une méthode</option>
+              <option value="card">Carte bancaire</option>
+              <option value="transfer">Virement bancaire</option>
+              <option value="paypal">PayPal</option>
+            </select>
+          </div>
+          <div class="flex gap-3">
+            <button 
+              type="button" 
+              @click="showAddFundsModal = false"
+              class="flex-1 bg-gray-100 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-200 transition-colors"
+            >
+              Annuler
+            </button>
+            <button 
+              type="submit"
+              class="flex-1 bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
+            >
+              Ajouter
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+
+    <!-- Modal Demande de paiement -->
+    <div v-if="showPaymentRequestModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+      <div class="bg-white rounded-lg p-6 w-full max-w-md">
+        <h3 class="text-lg font-semibold text-gray-800 mb-4">Nouvelle demande de paiement</h3>
+        <form @submit.prevent="createPaymentRequest">
+          <div class="mb-4">
+            <label class="block text-sm font-medium text-gray-700 mb-2">Montant (€)</label>
+            <input 
+              type="number" 
+              v-model="paymentRequestForm.amount" 
+              min="1" 
+              step="0.01" 
+              required 
+              class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+            >
+          </div>
+          <div class="mb-6">
+            <label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
+            <textarea 
+              v-model="paymentRequestForm.description" 
+              required 
+              rows="3"
+              class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+              placeholder="Décrivez l'objet de la demande..."
+            ></textarea>
+          </div>
+          <div class="flex gap-3">
+            <button 
+              type="button" 
+              @click="showPaymentRequestModal = false"
+              class="flex-1 bg-gray-100 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-200 transition-colors"
+            >
+              Annuler
+            </button>
+            <button 
+              type="submit"
+              class="flex-1 bg-green-500 text-white py-2 px-4 rounded-lg hover:bg-green-600 transition-colors"
+            >
+              Créer
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } 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'
+
+// Stores et composables
+const userStore = useUserStore()
+const { success, error } = useToast()
+
+// État des onglets
+const activeTab = ref('subscriptions')
+const tabs = [
+  { id: 'subscriptions', name: 'Abonnements', icon: 'fas fa-crown' },
+  { id: 'balance', name: 'Solde', icon: 'fas fa-wallet' },
+  { id: 'payment-requests', name: 'Demandes de paiement', icon: 'fas fa-money-bill-wave' },
+  { id: 'receipts', name: 'Reçus', icon: 'fas fa-file-invoice' }
+]
+
+// Données réactives
+const activeSubscription = ref<Subscription | null>(null)
+const availableSubscriptions = ref<Subscription[]>([])
+const currentBalance = ref<Balance | null>(null)
+const transactions = ref<Transaction[]>([])
+const paymentRequests = ref<PaymentRequest[]>([])
+const receipts = ref<Receipt[]>([])
+
+// Filtres
+const transactionFilter = ref('')
+const paymentRequestFilter = ref('')
+
+// Modals
+const showAddFundsModal = ref(false)
+const showPaymentRequestModal = ref(false)
+
+// Formulaires
+const addFundsForm = ref({
+  amount: 0,
+  paymentMethod: ''
+})
+
+const paymentRequestForm = ref({
+  amount: 0,
+  description: ''
+})
+
+// Computed
+const filteredTransactions = computed(() => {
+  if (!transactionFilter.value) return transactions.value
+  return transactions.value.filter(t => t.type === transactionFilter.value)
+})
+
+const filteredPaymentRequests = computed(() => {
+  if (!paymentRequestFilter.value) return paymentRequests.value
+  return paymentRequests.value.filter(r => r.status === paymentRequestFilter.value)
+})
+
+const monthlyTransactions = computed(() => {
+  const currentMonth = new Date().getMonth()
+  const currentYear = new Date().getFullYear()
+  return transactions.value.filter(t => {
+    const transactionDate = new Date(t.date)
+    return transactionDate.getMonth() === currentMonth && transactionDate.getFullYear() === currentYear
+  }).length
+})
+
+// Méthodes
+const handleSearch = (query: string) => {
+  console.log('Recherche:', query)
+}
+
+const handleNotifications = () => {
+  console.log('Notifications')
+}
+
+const handleMessages = () => {
+  console.log('Messages')
+}
+
+const handleUserMenu = () => {
+  console.log('Menu utilisateur')
+}
+
+const formatDate = (dateString: string | undefined) => {
+  if (!dateString) return 'N/A'
+  return new Date(dateString).toLocaleDateString('fr-FR')
+}
+
+const formatCurrency = (amount: number) => {
+  return new Intl.NumberFormat('fr-FR', {
+    style: 'currency',
+    currency: 'EUR'
+  }).format(amount)
+}
+
+const getStatusLabel = (status: string) => {
+  const labels = {
+    pending: 'En attente',
+    completed: 'Terminé',
+    failed: 'Échoué'
+  }
+  return labels[status as keyof typeof labels] || status
+}
+
+const getPaymentStatusLabel = (status: string) => {
+  const labels = {
+    pending: 'En attente',
+    approved: 'Approuvé',
+    rejected: 'Rejeté',
+    paid: 'Payé'
+  }
+  return labels[status as keyof typeof labels] || status
+}
+
+const subscribeToService = async (subscriptionId: number) => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) {
+      error('Entreprise non trouvée')
+      return
+    }
+
+    const result = await BudgetService.subscribeToService(companyId, subscriptionId)
+    if (result.success) {
+      success(result.message)
+      await loadSubscriptions()
+    } else {
+      error(result.message)
+    }
+  } catch (err) {
+    error('Erreur lors de la souscription')
+  }
+}
+
+const addFunds = async () => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) {
+      error('Entreprise non trouvée')
+      return
+    }
+
+    const result = await BudgetService.addFunds(companyId, addFundsForm.value.amount, addFundsForm.value.paymentMethod)
+    if (result.success) {
+      success(result.message)
+      showAddFundsModal.value = false
+      addFundsForm.value = { amount: 0, paymentMethod: '' }
+      await loadBalance()
+    } else {
+      error(result.message)
+    }
+  } catch (err) {
+    error('Erreur lors de l\'ajout de fonds')
+  }
+}
+
+const createPaymentRequest = async () => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) {
+      error('Entreprise non trouvée')
+      return
+    }
+
+    const result = await BudgetService.createPaymentRequest({
+      companyId,
+      amount: paymentRequestForm.value.amount,
+      description: paymentRequestForm.value.description
+    })
+
+    if (result.success) {
+      success(result.message)
+      showPaymentRequestModal.value = false
+      paymentRequestForm.value = { amount: 0, description: '' }
+      await loadPaymentRequests()
+    } else {
+      error(result.message)
+    }
+  } catch (err) {
+    error('Erreur lors de la création de la demande')
+  }
+}
+
+const viewPaymentRequest = (request: PaymentRequest) => {
+  console.log('Voir demande:', request)
+}
+
+const editPaymentRequest = (request: PaymentRequest) => {
+  console.log('Modifier demande:', request)
+}
+
+const downloadReceipt = async (receiptId: number) => {
+  try {
+    const result = await BudgetService.downloadReceipt(receiptId)
+    if (result.success && result.url) {
+      window.open(result.url, '_blank')
+      success(result.message)
+    } else {
+      error(result.message)
+    }
+  } catch (err) {
+    error('Erreur lors du téléchargement')
+  }
+}
+
+// Chargement des données
+const loadSubscriptions = async () => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) return
+
+    const [active, available] = await Promise.all([
+      BudgetService.getActiveSubscription(companyId),
+      BudgetService.getSubscriptions(companyId)
+    ])
+
+    activeSubscription.value = active
+    availableSubscriptions.value = available
+  } catch (err) {
+    console.error('Erreur lors du chargement des abonnements:', err)
+  }
+}
+
+const loadBalance = async () => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) return
+
+    const [balance, transactionsResult] = await Promise.all([
+      BudgetService.getBalance(companyId),
+      BudgetService.getTransactions(companyId, 1, 50)
+    ])
+
+    currentBalance.value = balance
+    transactions.value = transactionsResult.transactions
+  } catch (err) {
+    console.error('Erreur lors du chargement du solde:', err)
+  }
+}
+
+const loadPaymentRequests = async () => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) return
+
+    const result = await BudgetService.getPaymentRequests(companyId, 1, 50)
+    paymentRequests.value = result.requests
+  } catch (err) {
+    console.error('Erreur lors du chargement des demandes:', err)
+  }
+}
+
+const loadReceipts = async () => {
+  try {
+    const companyId = userStore.userCompany?.id
+    if (!companyId) return
+
+    const result = await BudgetService.getReceipts(companyId, 1, 50)
+    receipts.value = result.receipts
+  } catch (err) {
+    console.error('Erreur lors du chargement des reçus:', err)
+  }
+}
+
+// Chargement initial
+onMounted(async () => {
+  await Promise.all([
+    loadSubscriptions(),
+    loadBalance(),
+    loadPaymentRequests(),
+    loadReceipts()
+  ])
+})
+</script>
+
+<style scoped>
+.tab-content {
+  min-height: 400px;
+}
+
+/* Animations pour les transitions d'onglets */
+.tab-content > div {
+  animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* Styles pour les cartes de statistiques */
+.bg-gradient-to-r {
+  background: linear-gradient(135deg, var(--tw-gradient-from), var(--tw-gradient-to));
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+  .grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-3 {
+    grid-template-columns: 1fr;
+  }
+  
+  .grid-cols-1.md\\:grid-cols-3 {
+    grid-template-columns: 1fr;
+  }
+  
+  .flex.gap-3 {
+    flex-direction: column;
+    gap: 0.5rem;
+  }
+}
+</style>