|
@@ -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>
|