Chaque page plafonnait sa largeur differemment (max-w-4xl pour Config, 5xl pour IA, 6xl pour l'accueil et les documents, 7xl pour les revenus et les depenses) : sur un ecran large, les donnees se tassaient au centre. Les pages n'ont plus de plafond, le contenu occupe l'ecran avec une marge laterale fixe. Pour tirer parti de la place gagnee : Config affiche ses deux sections cote a cote, et le split apercu PDF / donnees passe de 50-50 a 40-60 en faveur des donnees. Pendant le tagging, le bouton « Annuler » de l'en-tete d'edition est masque : il faisait doublon avec celui du bandeau de tagging, qui a un sens different (retour a l'edition, et non abandon). La sortie vers la liste reste le fil d'Ariane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
268 lines
8.5 KiB
Vue
268 lines
8.5 KiB
Vue
<template>
|
|
<div class="page">
|
|
<div class="page-content">
|
|
|
|
<!-- Zone d'upload PDF visible -->
|
|
<div
|
|
class="border-2 border-dashed border-gray-700 rounded-lg p-6 text-center hover:border-blue-500 transition-colors cursor-pointer bg-gray-900/50"
|
|
@click="triggerFileInput"
|
|
@dragover.prevent="isDragging = true"
|
|
@dragleave.prevent="isDragging = false"
|
|
@drop.prevent="onDrop"
|
|
:class="{ 'border-blue-500 bg-blue-500/10': isDragging }"
|
|
>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept="application/pdf,.pdf"
|
|
class="hidden"
|
|
@change="onFileSelect"
|
|
/>
|
|
<div class="text-gray-400">
|
|
<svg class="mx-auto h-10 w-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
</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">Deposer le fichier ici</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions rapides -->
|
|
<QuickActions @import-pdf="triggerFileInput" />
|
|
|
|
<!-- Resume financier -->
|
|
<FinancialSummary :data="financialSummary" />
|
|
|
|
<!-- Grille principale -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
<!-- Colonne gauche: Graphique tendance + Revenus recents -->
|
|
<div class="space-y-6">
|
|
<MiniTrendChart :trends="monthlyTrends" :loading="isLoadingTrends" />
|
|
<RecentRevenus :revenus="recentRevenus" :loading="isLoadingRevenus" />
|
|
</div>
|
|
|
|
<!-- 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="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">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="spinner h-6 w-6"></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>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
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)
|
|
|
|
// 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 '-'
|
|
const [year, month, day] = dateStr.split('-')
|
|
return `${day}/${month}/${year}`
|
|
}
|
|
|
|
function formatAmount(amount) {
|
|
if (amount == null) return '-'
|
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount)
|
|
}
|
|
|
|
async function loadData() {
|
|
// Charger les documents
|
|
isLoadingDocs.value = true
|
|
try {
|
|
const docsRes = await fetch('/api/documents?limit=5')
|
|
if (docsRes.ok) {
|
|
documents.value = await docsRes.json()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load documents:', err)
|
|
} finally {
|
|
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
|
|
}
|
|
}
|
|
|
|
function triggerFileInput() {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
function onFileSelect(e) {
|
|
const file = e.target.files[0]
|
|
if (file && file.name.toLowerCase().endsWith('.pdf')) {
|
|
pendingFile.value = file
|
|
router.push('/extract')
|
|
}
|
|
e.target.value = ''
|
|
}
|
|
|
|
function onDrop(e) {
|
|
isDragging.value = false
|
|
const file = e.dataTransfer.files[0]
|
|
if (file && file.name.toLowerCase().endsWith('.pdf')) {
|
|
pendingFile.value = file
|
|
router.push('/extract')
|
|
}
|
|
}
|
|
|
|
function handleImmeubleSelect(immeuble) {
|
|
router.push(`/analytics?immeuble_id=${immeuble.id}`)
|
|
}
|
|
|
|
function goToAnalytics() {
|
|
router.push('/analytics')
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
</script>
|