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:
2026-01-20 04:53:04 +01:00
parent cae8d5b963
commit 281f69f39e
9 changed files with 1275 additions and 70 deletions

View File

@@ -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()
})