feat: enhance homepage with financial summary and sparklines
- Add dashboard API endpoints for financial summary, recent revenus, monthly trends, and immeuble shortcuts - Create new dashboard components: FinancialSummary with sparklines, QuickActions, RecentRevenus, MiniTrendChart, ImmeubleShortcuts - Refactor HomePage with prominent drag & drop zone and data-driven cards - Financial cards now show last document data with 6-month trend sparklines in background - Remove redundant import button, keep single upload zone at top
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-800">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<div class="max-w-6xl mx-auto space-y-6">
|
||||
|
||||
<!-- Upload zone compacte -->
|
||||
<!-- Zone d'upload PDF visible -->
|
||||
<div
|
||||
class="border-2 border-dashed border-gray-600 rounded-lg p-6 text-center hover:border-blue-500 transition-colors cursor-pointer bg-gray-900/50"
|
||||
@click="triggerFileInput"
|
||||
@@ -24,58 +24,93 @@
|
||||
</svg>
|
||||
<p class="text-sm">
|
||||
<span v-if="!isDragging">Glisser un PDF ici ou <span class="text-blue-400">cliquer pour importer</span></span>
|
||||
<span v-else class="text-blue-400 font-medium">Déposer le fichier ici</span>
|
||||
<span v-else class="text-blue-400 font-medium">Deposer le fichier ici</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats rapides -->
|
||||
<div class="grid grid-cols-3 md:grid-cols-6 gap-3">
|
||||
<div v-for="stat in statsDisplay" :key="stat.key" class="bg-gray-900 rounded-lg p-3 text-center">
|
||||
<div class="text-2xl font-bold text-white">{{ stats[stat.key] ?? '-' }}</div>
|
||||
<div class="text-xs text-gray-400">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions rapides -->
|
||||
<QuickActions @import-pdf="triggerFileInput" />
|
||||
|
||||
<!-- Derniers documents -->
|
||||
<div class="bg-gray-900 rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-700">
|
||||
<h2 class="text-sm font-medium text-gray-300">Derniers documents importes</h2>
|
||||
</div>
|
||||
<!-- Resume financier -->
|
||||
<FinancialSummary :data="financialSummary" />
|
||||
|
||||
<!-- Grille principale -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<div v-if="isLoading" class="p-8 text-center text-gray-500">
|
||||
Chargement...
|
||||
<!-- Colonne gauche: Graphique tendance + Revenus recents -->
|
||||
<div class="space-y-6">
|
||||
<MiniTrendChart :trends="monthlyTrends" :loading="isLoadingTrends" />
|
||||
<RecentRevenus :revenus="recentRevenus" :loading="isLoadingRevenus" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="documents.length === 0" class="p-8 text-center text-gray-500">
|
||||
Aucun document importe pour le moment.
|
||||
|
||||
<!-- Colonne droite: Immeubles + Documents -->
|
||||
<div class="space-y-6">
|
||||
<ImmeubleShortcuts
|
||||
:immeubles="immeubleShortcuts"
|
||||
:loading="isLoadingImmeubles"
|
||||
@select="handleImmeubleSelect"
|
||||
@view-all="goToAnalytics"
|
||||
/>
|
||||
|
||||
<!-- Derniers documents -->
|
||||
<div id="recent-documents" class="bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
||||
<div class="px-4 py-3 border-b border-gray-700 flex items-center justify-between">
|
||||
<h2 class="text-sm font-medium text-gray-300">Derniers documents importes</h2>
|
||||
<span class="text-xs text-gray-500">{{ documents.length }} documents</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingDocs" class="p-8 text-center text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-6 w-6 border-2 border-gray-600 border-t-blue-400"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="documents.length === 0" class="p-8 text-center text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 mb-3 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p>Aucun document importe.</p>
|
||||
<button
|
||||
@click="triggerFileInput"
|
||||
class="mt-3 text-sm text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
Importer votre premier PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="divide-y divide-gray-800">
|
||||
<div
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
class="px-4 py-3 hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-mono text-white">{{ doc.reference }}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-gray-700 text-gray-400">
|
||||
{{ doc.immeuble_code }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5">
|
||||
{{ formatDate(doc.date) }}
|
||||
<span v-if="doc.immeuble_adresse" class="ml-2">{{ doc.immeuble_adresse }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right ml-4">
|
||||
<span
|
||||
:class="[
|
||||
'text-sm font-medium',
|
||||
doc.solde_type === 'crediteur' ? 'text-green-400' : 'text-red-400'
|
||||
]"
|
||||
>
|
||||
{{ formatAmount(doc.solde_montant) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full">
|
||||
<thead class="bg-gray-800/50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">Reference</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">Date</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">Immeuble</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-400 uppercase">Solde</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
<tr v-for="doc in documents" :key="doc.id" class="hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-sm text-white font-mono">{{ doc.reference }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-300">{{ formatDate(doc.date) }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-300">
|
||||
<span class="text-gray-500">{{ doc.immeuble_code }}</span>
|
||||
<span v-if="doc.immeuble_adresse" class="ml-2">{{ doc.immeuble_adresse }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-right">
|
||||
<span :class="doc.solde_type === 'crediteur' ? 'text-green-400' : 'text-red-400'">
|
||||
{{ formatAmount(doc.solde_montant) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -87,21 +122,39 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { pendingFile } from '../store'
|
||||
|
||||
import QuickActions from '../components/dashboard/QuickActions.vue'
|
||||
import FinancialSummary from '../components/dashboard/FinancialSummary.vue'
|
||||
import RecentRevenus from '../components/dashboard/RecentRevenus.vue'
|
||||
import MiniTrendChart from '../components/dashboard/MiniTrendChart.vue'
|
||||
import ImmeubleShortcuts from '../components/dashboard/ImmeubleShortcuts.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const fileInput = ref(null)
|
||||
const isDragging = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const stats = ref({})
|
||||
const documents = ref([])
|
||||
|
||||
const statsDisplay = [
|
||||
{ key: 'documents', label: 'Documents' },
|
||||
{ key: 'immeubles', label: 'Immeubles' },
|
||||
{ key: 'lots', label: 'Lots' },
|
||||
{ key: 'locataires', label: 'Locataires' },
|
||||
{ key: 'revenus', label: 'Revenus' },
|
||||
{ key: 'depenses', label: 'Depenses' },
|
||||
]
|
||||
// Loading states
|
||||
const isLoadingDocs = ref(true)
|
||||
const isLoadingRevenus = ref(true)
|
||||
const isLoadingTrends = ref(true)
|
||||
const isLoadingImmeubles = ref(true)
|
||||
|
||||
// Data
|
||||
const documents = ref([])
|
||||
const financialSummary = ref({
|
||||
last_document_date: null,
|
||||
last_document_reference: null,
|
||||
revenus: 0,
|
||||
impayes: 0,
|
||||
depenses: 0,
|
||||
solde: 0,
|
||||
revenus_history: [],
|
||||
impayes_history: [],
|
||||
depenses_history: [],
|
||||
solde_history: []
|
||||
})
|
||||
const recentRevenus = ref([])
|
||||
const monthlyTrends = ref([])
|
||||
const immeubleShortcuts = ref([])
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
@@ -115,24 +168,66 @@ function formatAmount(amount) {
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
// Charger les documents
|
||||
isLoadingDocs.value = true
|
||||
try {
|
||||
const [statsRes, docsRes] = await Promise.all([
|
||||
fetch('/api/stats'),
|
||||
fetch('/api/documents?limit=10')
|
||||
])
|
||||
|
||||
if (statsRes.ok) {
|
||||
stats.value = await statsRes.json()
|
||||
}
|
||||
|
||||
const docsRes = await fetch('/api/documents?limit=5')
|
||||
if (docsRes.ok) {
|
||||
documents.value = await docsRes.json()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load dashboard data:', err)
|
||||
console.error('Failed to load documents:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoadingDocs.value = false
|
||||
}
|
||||
|
||||
// Charger le resume financier
|
||||
try {
|
||||
const summaryRes = await fetch('/api/dashboard/financial-summary')
|
||||
if (summaryRes.ok) {
|
||||
financialSummary.value = await summaryRes.json()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load financial summary:', err)
|
||||
}
|
||||
|
||||
// Charger les revenus recents
|
||||
isLoadingRevenus.value = true
|
||||
try {
|
||||
const revenusRes = await fetch('/api/dashboard/recent-revenus?limit=5')
|
||||
if (revenusRes.ok) {
|
||||
recentRevenus.value = await revenusRes.json()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load recent revenus:', err)
|
||||
} finally {
|
||||
isLoadingRevenus.value = false
|
||||
}
|
||||
|
||||
// Charger les tendances mensuelles
|
||||
isLoadingTrends.value = true
|
||||
try {
|
||||
const trendsRes = await fetch('/api/dashboard/monthly-trends?months=6')
|
||||
if (trendsRes.ok) {
|
||||
monthlyTrends.value = await trendsRes.json()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load monthly trends:', err)
|
||||
} finally {
|
||||
isLoadingTrends.value = false
|
||||
}
|
||||
|
||||
// Charger les raccourcis immeubles
|
||||
isLoadingImmeubles.value = true
|
||||
try {
|
||||
const immeublesRes = await fetch('/api/dashboard/immeubles-shortcuts?limit=6')
|
||||
if (immeublesRes.ok) {
|
||||
immeubleShortcuts.value = await immeublesRes.json()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load immeubles shortcuts:', err)
|
||||
} finally {
|
||||
isLoadingImmeubles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +253,14 @@ function onDrop(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleImmeubleSelect(immeuble) {
|
||||
router.push(`/analytics?immeuble_id=${immeuble.id}`)
|
||||
}
|
||||
|
||||
function goToAnalytics() {
|
||||
router.push('/analytics')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user