Files
pdf_oralia_vibe/frontend/src/pages/HomePage.vue
Bertrand Benjamin aa365dbb63 refactor(ui): passe les pages en pleine largeur
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>
2026-07-25 19:49:43 +02:00

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>