feat: add rental income dashboard with comprehensive analytics
- Add new /api/revenus endpoints for rental income data - GET /summary: KPIs, monthly trends, breakdown by property, top unpaid - GET /details: detailed rental records with filtering - GET /by-lot: revenue aggregation by unit - GET /immeubles: properties list with revenue stats - Add RevenusPage with full dashboard layout - 6 KPI cards: total revenue, payments, unpaid, collection rate, active tenants, occupied units - Monthly evolution chart (rent, payments, unpaid) - Property breakdown donut chart - Top unpaid tenants list - Detailed revenue table with pagination and CSV export - Add filters panel (property, type, dates, unpaid only, history months) - Display property address instead of code in all components - Add navigation link in header (Revenus)
This commit is contained in:
173
frontend/src/pages/RevenusPage.vue
Normal file
173
frontend/src/pages/RevenusPage.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white">Dashboard Revenus Locatifs</h1>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
Suivi et analyse des loyers, reglements et impayes
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div v-if="lastUpdate" class="text-xs text-gray-500">
|
||||
Derniere mise a jour: {{ lastUpdate }}
|
||||
</div>
|
||||
<button
|
||||
@click="loadData"
|
||||
:disabled="isLoading"
|
||||
class="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span v-if="isLoading">Chargement...</span>
|
||||
<span v-else>Actualiser</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<RevenusFilterPanel @filter-change="onFiltersChange" />
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<RevenusKpiCards :kpis="summary.kpis" />
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<RevenusMonthlyChart :data="summary.by_month" />
|
||||
<RevenusImmeubleChart :data="summary.by_immeuble" />
|
||||
</div>
|
||||
|
||||
<!-- Impayes Section -->
|
||||
<TopImpayesList :data="summary.top_impayes" />
|
||||
|
||||
<!-- Detailed Table -->
|
||||
<RevenusTable
|
||||
:revenus="revenus"
|
||||
:is-loading="isLoadingRevenus"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import RevenusFilterPanel from '../components/revenus/RevenusFilterPanel.vue'
|
||||
import RevenusKpiCards from '../components/revenus/RevenusKpiCards.vue'
|
||||
import RevenusMonthlyChart from '../components/revenus/RevenusMonthlyChart.vue'
|
||||
import RevenusImmeubleChart from '../components/revenus/RevenusImmeubleChart.vue'
|
||||
import TopImpayesList from '../components/revenus/TopImpayesList.vue'
|
||||
import RevenusTable from '../components/revenus/RevenusTable.vue'
|
||||
|
||||
// Current filters state
|
||||
let currentFilters = {
|
||||
immeuble_id: null,
|
||||
type_ligne: null,
|
||||
date_debut: null,
|
||||
date_fin: null,
|
||||
impayes_only: false,
|
||||
months: 12
|
||||
}
|
||||
|
||||
const summary = ref({
|
||||
kpis: {
|
||||
total_revenus: 0,
|
||||
total_loyers: 0,
|
||||
total_taxes: 0,
|
||||
total_provisions: 0,
|
||||
total_regles: 0,
|
||||
total_impayes: 0,
|
||||
taux_recouvrement: 0,
|
||||
nb_locataires_actifs: 0,
|
||||
nb_lots_occupes: 0
|
||||
},
|
||||
by_month: [],
|
||||
by_immeuble: [],
|
||||
top_impayes: []
|
||||
})
|
||||
|
||||
const revenus = ref([])
|
||||
const isLoading = ref(false)
|
||||
const isLoadingRevenus = ref(false)
|
||||
const lastUpdate = ref(null)
|
||||
|
||||
// Debounce timer
|
||||
let debounceTimer = null
|
||||
|
||||
function onFiltersChange(newFilters) {
|
||||
currentFilters = { ...newFilters }
|
||||
|
||||
// Debounce
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
loadData()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function buildSummaryParams() {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (currentFilters.months) params.append('months', currentFilters.months)
|
||||
if (currentFilters.immeuble_id !== null) params.append('immeuble_id', currentFilters.immeuble_id)
|
||||
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
function buildDetailsParams() {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (currentFilters.immeuble_id !== null) params.append('immeuble_id', currentFilters.immeuble_id)
|
||||
if (currentFilters.type_ligne !== null) params.append('type_ligne', currentFilters.type_ligne)
|
||||
if (currentFilters.date_debut) params.append('date_debut', currentFilters.date_debut)
|
||||
if (currentFilters.date_fin) params.append('date_fin', currentFilters.date_fin)
|
||||
if (currentFilters.impayes_only) params.append('impayes_only', 'true')
|
||||
params.append('limit', '500')
|
||||
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
isLoadingRevenus.value = true
|
||||
|
||||
const summaryParams = buildSummaryParams()
|
||||
const detailsParams = buildDetailsParams()
|
||||
|
||||
const summaryUrl = summaryParams ? `/api/revenus/summary?${summaryParams}` : '/api/revenus/summary'
|
||||
const detailsUrl = detailsParams ? `/api/revenus/details?${detailsParams}` : '/api/revenus/details'
|
||||
|
||||
console.log('Loading revenus data with filters:', currentFilters)
|
||||
|
||||
try {
|
||||
const [summaryRes, detailsRes] = await Promise.all([
|
||||
fetch(summaryUrl),
|
||||
fetch(detailsUrl)
|
||||
])
|
||||
|
||||
if (summaryRes.ok) {
|
||||
summary.value = await summaryRes.json()
|
||||
console.log('Summary loaded:', summary.value)
|
||||
} else {
|
||||
console.error('Failed to load summary:', await summaryRes.text())
|
||||
}
|
||||
|
||||
if (detailsRes.ok) {
|
||||
revenus.value = await detailsRes.json()
|
||||
console.log('Details loaded:', revenus.value.length, 'revenus')
|
||||
} else {
|
||||
console.error('Failed to load details:', await detailsRes.text())
|
||||
}
|
||||
|
||||
lastUpdate.value = new Date().toLocaleTimeString('fr-FR')
|
||||
} catch (err) {
|
||||
console.error('Failed to load revenus data:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoadingRevenus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user