Compare commits
6 Commits
0e30ed8501
...
94b885aedb
| Author | SHA1 | Date | |
|---|---|---|---|
| 94b885aedb | |||
| 14b39efe3b | |||
| 02a3459291 | |||
| aa365dbb63 | |||
| baeabb72d2 | |||
| 420a856e41 |
7
Makefile
7
Makefile
@@ -1,4 +1,9 @@
|
||||
.PHONY: dev_back dev_front dev docker docker-build
|
||||
.PHONY: install dev_back dev_front dev docker docker-build
|
||||
|
||||
# Installe les dependances backend + frontend (a refaire par worktree)
|
||||
install:
|
||||
uv sync
|
||||
cd frontend && npm install
|
||||
|
||||
# Backend avec auto-reload uvicorn
|
||||
dev_back:
|
||||
|
||||
@@ -1,68 +1,27 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-gray-900">
|
||||
<div class="h-screen flex flex-col bg-gray-950">
|
||||
<!-- Header compact -->
|
||||
<header class="flex-shrink-0 bg-gray-800 border-b border-gray-700 px-4 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<router-link to="/" class="text-lg font-semibold text-white hover:text-blue-400 transition-colors">
|
||||
<header class="flex-shrink-0 bg-gray-900 border-b border-gray-700 px-6 py-2">
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<router-link to="/" class="text-lg font-semibold text-white hover:text-blue-400 transition-colors whitespace-nowrap">
|
||||
Plesna Gerance
|
||||
<span class="text-gray-400 font-normal text-sm ml-2">Extracteur de comptes rendus</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex items-center gap-3">
|
||||
<nav class="flex items-center gap-1">
|
||||
<router-link
|
||||
to="/"
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors"
|
||||
:class="$route.path === '/' ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'"
|
||||
v-for="link in navLinks"
|
||||
:key="link.to"
|
||||
:to="link.to"
|
||||
class="px-3 py-1.5 text-sm rounded-lg transition-colors"
|
||||
:class="$route.path === link.to ? 'bg-gray-800 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-800'"
|
||||
>
|
||||
Accueil
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/documents"
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors"
|
||||
:class="$route.path === '/documents' ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'"
|
||||
>
|
||||
Documents
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/revenus"
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors"
|
||||
:class="$route.path === '/revenus' ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'"
|
||||
>
|
||||
Revenus
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/analytics"
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors"
|
||||
:class="$route.path === '/analytics' ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'"
|
||||
>
|
||||
Depenses
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/ia"
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors"
|
||||
:class="$route.path === '/ia' ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'"
|
||||
>
|
||||
IA
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/config"
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors"
|
||||
:class="$route.path === '/config' ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'"
|
||||
>
|
||||
Config
|
||||
{{ link.label }}
|
||||
</router-link>
|
||||
|
||||
<!-- Import button with file input -->
|
||||
<label
|
||||
class="px-3 py-1.5 text-sm rounded transition-colors cursor-pointer bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white"
|
||||
:class="$route.path === '/extract' ? 'bg-blue-600 text-white' : ''"
|
||||
>
|
||||
<label class="btn btn-primary ml-2 cursor-pointer">
|
||||
Importer un PDF
|
||||
<input
|
||||
ref="fileInput"
|
||||
@@ -91,6 +50,15 @@ import { pendingFile } from './store'
|
||||
const router = useRouter()
|
||||
const fileInput = ref(null)
|
||||
|
||||
const navLinks = [
|
||||
{ to: '/', label: 'Accueil' },
|
||||
{ to: '/documents', label: 'Documents' },
|
||||
{ to: '/revenus', label: 'Revenus' },
|
||||
{ to: '/analytics', label: 'Depenses' },
|
||||
{ to: '/ia', label: 'IA' },
|
||||
{ to: '/config', label: 'Config' }
|
||||
]
|
||||
|
||||
function onFileSelect(e) {
|
||||
const file = e.target.files[0]
|
||||
if (file && file.name.toLowerCase().endsWith('.pdf')) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2 flex items-center gap-2">
|
||||
<div class="subcard p-3">
|
||||
<h4 class="text-sm font-medium text-gray-300 mb-2 flex items-center gap-2">
|
||||
<!-- Icons -->
|
||||
<svg v-if="icon === 'building'" class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@update:modelValue="onUpdate"
|
||||
:type="type"
|
||||
:placeholder="'-'"
|
||||
:displayClass="highlight ? 'text-blue-600 text-base font-medium' : 'text-gray-800 font-medium'"
|
||||
:displayClass="highlight ? 'text-blue-400 text-base font-medium' : 'text-gray-200 font-medium'"
|
||||
:fullWidth="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div class="card">
|
||||
<!-- Header -->
|
||||
<button
|
||||
@click="$emit('toggle')"
|
||||
class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
class="w-full flex items-center justify-between px-4 py-3 bg-gray-900 hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -15,15 +15,15 @@
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span class="font-semibold text-gray-800">{{ title }}</span>
|
||||
<span v-if="count !== undefined" class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
|
||||
<span class="font-semibold text-white">{{ title }}</span>
|
||||
<span v-if="count !== undefined" class="badge badge-info rounded-full">
|
||||
{{ count }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-show="expanded" class="p-4 bg-white">
|
||||
<div v-show="expanded" class="p-4 border-t border-gray-700">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<span
|
||||
v-if="!isEditing"
|
||||
@click="startEditing"
|
||||
class="cursor-pointer border-b border-dashed border-transparent hover:border-gray-400 hover:bg-yellow-50 px-1 py-0.5 rounded transition-colors min-w-[2rem]"
|
||||
class="cursor-pointer border-b border-dashed border-transparent hover:border-gray-500 hover:bg-gray-700/60 px-1 py-0.5 rounded transition-colors min-w-[2rem]"
|
||||
:class="[
|
||||
displayClass,
|
||||
{ 'text-gray-400 italic': isEmpty }
|
||||
{ 'text-gray-500 italic': isEmpty }
|
||||
]"
|
||||
:title="'Cliquer pour modifier'"
|
||||
>
|
||||
@@ -28,7 +28,7 @@
|
||||
@blur="saveAndClose"
|
||||
@keydown.enter="saveAndClose"
|
||||
@keydown.escape="cancelEditing"
|
||||
class="px-1 py-0.5 border border-blue-400 rounded bg-blue-50 focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm"
|
||||
class="px-1 py-0.5 border border-blue-500 rounded bg-gray-950 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm [color-scheme:dark]"
|
||||
:class="[
|
||||
inputClass,
|
||||
{ 'w-full': fullWidth, 'w-24': !fullWidth && (type === 'currency' || type === 'number'), 'w-32': !fullWidth && type === 'date' }
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<div v-if="isCollapsible" class="group">
|
||||
<div
|
||||
@click="toggle"
|
||||
class="flex items-center gap-1 cursor-pointer hover:bg-gray-100 rounded px-1 -mx-1"
|
||||
class="flex items-center gap-1 cursor-pointer hover:bg-gray-800 rounded px-1 -mx-1"
|
||||
>
|
||||
<!-- Expand/collapse icon -->
|
||||
<svg
|
||||
class="w-3 h-3 text-gray-400 transition-transform"
|
||||
class="w-3 h-3 text-gray-500 transition-transform"
|
||||
:class="{ 'rotate-90': isExpanded }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -18,11 +18,11 @@
|
||||
</svg>
|
||||
|
||||
<!-- Key name -->
|
||||
<span v-if="!isRoot" class="text-purple-600">{{ name }}</span>
|
||||
<span v-if="!isRoot" class="text-gray-400">: </span>
|
||||
<span v-if="!isRoot" class="text-purple-400">{{ name }}</span>
|
||||
<span v-if="!isRoot" class="text-gray-500">: </span>
|
||||
|
||||
<!-- Preview when collapsed -->
|
||||
<span class="text-gray-400">
|
||||
<span class="text-gray-500">
|
||||
{{ isArray ? '[' : '{' }}
|
||||
<span v-if="!isExpanded" class="text-gray-500">
|
||||
{{ itemCount }} {{ isArray ? 'elements' : 'cles' }}
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Children -->
|
||||
<div v-if="isExpanded" class="ml-4 border-l border-gray-200 pl-2">
|
||||
<div v-if="isExpanded" class="ml-4 border-l border-gray-700 pl-2">
|
||||
<JsonNode
|
||||
v-for="(value, key) in data"
|
||||
:key="key"
|
||||
@@ -40,14 +40,14 @@
|
||||
:name="String(key)"
|
||||
:is-root="false"
|
||||
/>
|
||||
<span class="text-gray-400">{{ isArray ? ']' : '}' }}</span>
|
||||
<span class="text-gray-500">{{ isArray ? ']' : '}' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primitive value -->
|
||||
<div v-else class="flex items-start gap-1 py-0.5">
|
||||
<span class="text-purple-600">{{ name }}</span>
|
||||
<span class="text-gray-400">: </span>
|
||||
<span class="text-purple-400">{{ name }}</span>
|
||||
<span class="text-gray-500">: </span>
|
||||
<span :class="valueClass">{{ formattedValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,11 +84,11 @@ const itemCount = computed(() => {
|
||||
})
|
||||
|
||||
const valueClass = computed(() => {
|
||||
if (props.data === null) return 'text-gray-400 italic'
|
||||
if (typeof props.data === 'string') return 'text-green-600'
|
||||
if (typeof props.data === 'number') return 'text-blue-600'
|
||||
if (typeof props.data === 'boolean') return 'text-orange-600'
|
||||
return 'text-gray-700'
|
||||
if (props.data === null) return 'text-gray-500 italic'
|
||||
if (typeof props.data === 'string') return 'text-green-400'
|
||||
if (typeof props.data === 'number') return 'text-blue-400'
|
||||
if (typeof props.data === 'boolean') return 'text-orange-400'
|
||||
return 'text-gray-300'
|
||||
})
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-white overflow-hidden">
|
||||
<div class="h-full flex flex-col bg-gray-950 overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 flex items-center justify-between px-4 py-2 bg-gray-100 border-b border-gray-200">
|
||||
<span class="text-sm font-semibold text-gray-700">Donnees extraites</span>
|
||||
<div class="flex-shrink-0 flex items-center justify-between px-4 py-2 bg-gray-900 border-b border-gray-700">
|
||||
<span class="card-title">Donnees extraites</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-if="data"
|
||||
@click="expandAll"
|
||||
class="px-2 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded transition-colors"
|
||||
class="btn btn-sm btn-ghost"
|
||||
>
|
||||
Tout deplier
|
||||
</button>
|
||||
<button
|
||||
v-if="data"
|
||||
@click="collapseAll"
|
||||
class="px-2 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded transition-colors"
|
||||
class="btn btn-sm btn-ghost"
|
||||
>
|
||||
Tout replier
|
||||
</button>
|
||||
<button
|
||||
v-if="data"
|
||||
@click="copyJson"
|
||||
class="flex items-center gap-1 px-2 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded transition-colors"
|
||||
class="btn btn-sm btn-ghost"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
@@ -34,8 +34,8 @@
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-auto custom-scrollbar">
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<svg class="animate-spin h-10 w-10 mb-3 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<svg class="animate-spin h-10 w-10 mb-3 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
@@ -43,8 +43,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="!data" class="flex flex-col items-center justify-center h-full text-gray-400 p-8">
|
||||
<svg class="w-16 h-16 mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-else-if="!data" class="flex flex-col items-center justify-center h-full text-gray-500 p-8">
|
||||
<svg class="w-16 h-16 mb-3 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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 class="text-sm">En attente de l'extraction...</p>
|
||||
@@ -54,7 +54,7 @@
|
||||
<div v-else class="p-4 space-y-3">
|
||||
<!-- Source file -->
|
||||
<div class="text-xs text-gray-500 mb-4">
|
||||
Fichier: <span class="font-medium text-gray-700">{{ data.source_file }}</span>
|
||||
Fichier: <span class="font-medium text-gray-300">{{ data.source_file }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Section -->
|
||||
@@ -63,7 +63,8 @@
|
||||
:expanded="expandedSections.metadata"
|
||||
@toggle="toggleSection('metadata')"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<!-- Les fiches metadata se repartissent sur la largeur disponible -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
|
||||
<!-- Editeur -->
|
||||
<DataCard title="Editeur" icon="building">
|
||||
<DataRow label="Nom" :value="data.data?.metadata?.editeur?.nom" @update:value="updateMetadata('editeur.nom', $event)" />
|
||||
@@ -124,7 +125,7 @@
|
||||
<!-- Bouton ajouter locataire -->
|
||||
<button
|
||||
@click="addLocataire"
|
||||
class="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 border border-dashed border-blue-300 rounded-lg transition-colors"
|
||||
class="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm text-blue-400 hover:bg-blue-500/10 border border-dashed border-blue-500/40 rounded-lg transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
@@ -155,7 +156,7 @@
|
||||
<!-- Bouton ajouter operation -->
|
||||
<button
|
||||
@click="addOperation"
|
||||
class="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 border border-dashed border-blue-300 rounded-lg transition-colors"
|
||||
class="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm text-blue-400 hover:bg-blue-500/10 border border-dashed border-blue-500/40 rounded-lg transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div ref="rootEl" class="border rounded-lg overflow-hidden transition-all duration-300" :class="changed ? 'border-amber-400 ring-2 ring-amber-400 bg-amber-50' : (isHighlighted ? 'border-blue-400 ring-2 ring-blue-400 bg-blue-50' : 'border-gray-200')">
|
||||
<div ref="rootEl" class="border rounded-lg overflow-hidden transition-all duration-300" :class="changed ? 'border-amber-500 ring-2 ring-amber-500/40 bg-amber-500/5' : (isHighlighted ? 'border-blue-500 ring-2 ring-blue-500/40 bg-blue-500/5' : 'border-gray-700')">
|
||||
<!-- Header -->
|
||||
<div class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 transition-colors">
|
||||
<div class="w-full flex items-center justify-between px-3 py-2 bg-gray-900 hover:bg-gray-800 transition-colors">
|
||||
<button
|
||||
@click="expanded = !expanded"
|
||||
class="flex items-center gap-3 flex-1 text-left"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-gray-400 transition-transform flex-shrink-0"
|
||||
class="w-3 h-3 text-gray-500 transition-transform flex-shrink-0"
|
||||
:class="{ 'rotate-90': expanded }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -21,10 +21,10 @@
|
||||
:modelValue="locataire.lot?.numero"
|
||||
@update:modelValue="updateField('lot.numero', $event)"
|
||||
type="text"
|
||||
displayClass="text-xs text-gray-700 font-medium"
|
||||
displayClass="text-xs text-gray-300 font-medium"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="mx-1 text-gray-300">|</span>
|
||||
<span class="mx-1 text-gray-600">|</span>
|
||||
<EditableField
|
||||
:modelValue="locataire.lot?.type"
|
||||
@update:modelValue="updateField('lot.type', $event)"
|
||||
@@ -37,7 +37,7 @@
|
||||
:modelValue="locataire.locataire?.nom"
|
||||
@update:modelValue="updateField('locataire.nom', $event)"
|
||||
type="text"
|
||||
displayClass="font-medium text-gray-800 text-sm"
|
||||
displayClass="font-medium text-white text-sm"
|
||||
placeholder="Sans nom"
|
||||
@click.stop
|
||||
/>
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="changed"
|
||||
class="text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 border border-amber-300"
|
||||
class="badge badge-warning text-[10px] uppercase tracking-wide"
|
||||
>
|
||||
Modifié
|
||||
</span>
|
||||
@@ -53,14 +53,14 @@
|
||||
<div class="text-sm font-semibold" :class="totalClass">
|
||||
{{ formatCurrency(locataire.totaux?.total) }}
|
||||
</div>
|
||||
<div v-if="locataire.totaux?.impayes > 0" class="text-xs text-red-500">
|
||||
<div v-if="locataire.totaux?.impayes > 0" class="text-xs text-red-400">
|
||||
Impayes: {{ formatCurrency(locataire.totaux?.impayes) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bouton supprimer -->
|
||||
<button
|
||||
@click.stop="$emit('remove')"
|
||||
class="p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
|
||||
class="p-1 text-gray-500 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors"
|
||||
title="Supprimer ce locataire"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -71,43 +71,43 @@
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-show="expanded" class="border-t border-gray-100 bg-gray-50 p-3">
|
||||
<div v-show="expanded" class="border-t border-gray-700 bg-gray-900/60 p-3">
|
||||
<!-- Totaux editables -->
|
||||
<div class="grid grid-cols-4 gap-2 text-xs mb-3">
|
||||
<div class="text-center p-2 bg-white rounded">
|
||||
<div class="text-center p-2 bg-gray-800 border border-gray-700 rounded">
|
||||
<div class="text-gray-500 mb-1">Loyers</div>
|
||||
<EditableField
|
||||
:modelValue="locataire.totaux?.loyers"
|
||||
@update:modelValue="updateField('totaux.loyers', $event)"
|
||||
type="currency"
|
||||
displayClass="font-semibold text-gray-800"
|
||||
displayClass="font-semibold text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-white rounded">
|
||||
<div class="text-center p-2 bg-gray-800 border border-gray-700 rounded">
|
||||
<div class="text-gray-500 mb-1">Taxes</div>
|
||||
<EditableField
|
||||
:modelValue="locataire.totaux?.taxes"
|
||||
@update:modelValue="updateField('totaux.taxes', $event)"
|
||||
type="currency"
|
||||
displayClass="font-semibold text-gray-800"
|
||||
displayClass="font-semibold text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-white rounded">
|
||||
<div class="text-center p-2 bg-gray-800 border border-gray-700 rounded">
|
||||
<div class="text-gray-500 mb-1">Provisions</div>
|
||||
<EditableField
|
||||
:modelValue="locataire.totaux?.provisions"
|
||||
@update:modelValue="updateField('totaux.provisions', $event)"
|
||||
type="currency"
|
||||
displayClass="font-semibold text-gray-800"
|
||||
displayClass="font-semibold text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-white rounded">
|
||||
<div class="text-center p-2 bg-gray-800 border border-gray-700 rounded">
|
||||
<div class="text-gray-500 mb-1">Regles</div>
|
||||
<EditableField
|
||||
:modelValue="locataire.totaux?.regles"
|
||||
@update:modelValue="updateField('totaux.regles', $event)"
|
||||
type="currency"
|
||||
displayClass="font-semibold text-green-600"
|
||||
displayClass="font-semibold text-green-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
<span class="text-xs text-gray-500 font-medium">Detail des lignes</span>
|
||||
<button
|
||||
@click="addLigne"
|
||||
class="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
class="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:bg-blue-500/10 rounded transition-colors"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
@@ -129,7 +129,7 @@
|
||||
<div
|
||||
v-for="(ligne, idx) in locataire.lignes"
|
||||
:key="idx"
|
||||
class="text-xs bg-white rounded p-2"
|
||||
class="text-xs bg-gray-800 border border-gray-700 rounded p-2"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center gap-2 flex-1">
|
||||
@@ -137,7 +137,7 @@
|
||||
<select
|
||||
:value="ligne.type"
|
||||
@change="updateLigneField(idx, 'type', $event.target.value)"
|
||||
class="px-1.5 py-0.5 rounded text-xs border border-gray-200 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
class="px-1.5 py-0.5 rounded text-xs border border-gray-700 bg-gray-950 text-gray-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option value="loyer">loyer</option>
|
||||
<option value="solde_anterieur">solde_anterieur</option>
|
||||
@@ -171,7 +171,7 @@
|
||||
@update:modelValue="updateLigneField(idx, 'divers.libelle', $event)"
|
||||
type="text"
|
||||
placeholder="libellé"
|
||||
displayClass="text-xs text-gray-600 italic truncate"
|
||||
displayClass="text-xs text-gray-400 italic truncate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,20 +182,20 @@
|
||||
:modelValue="ligne.divers?.montant"
|
||||
@update:modelValue="updateLigneField(idx, 'divers.montant', $event)"
|
||||
type="currency"
|
||||
displayClass="font-medium text-gray-800"
|
||||
displayClass="font-medium text-gray-200"
|
||||
/>
|
||||
<EditableField
|
||||
v-else
|
||||
:modelValue="ligne.total || ligne.loyers"
|
||||
@update:modelValue="updateLigneField(idx, 'total', $event)"
|
||||
type="currency"
|
||||
displayClass="font-medium text-gray-800"
|
||||
displayClass="font-medium text-gray-200"
|
||||
/>
|
||||
|
||||
<!-- Bouton supprimer ligne -->
|
||||
<button
|
||||
@click="removeLigne(idx)"
|
||||
class="p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
|
||||
class="p-1 text-gray-500 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors"
|
||||
title="Supprimer cette ligne"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -211,7 +211,7 @@
|
||||
<div v-else class="text-center py-2">
|
||||
<button
|
||||
@click="addLigne"
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-xs text-blue-600 hover:bg-blue-50 rounded transition-colors mx-auto"
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-xs text-blue-400 hover:bg-blue-500/10 rounded transition-colors mx-auto"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
@@ -267,9 +267,9 @@ watch(() => props.highlighted, (val) => {
|
||||
|
||||
const totalClass = computed(() => {
|
||||
const total = props.locataire.totaux?.total || 0
|
||||
if (total > 0) return 'text-green-600'
|
||||
if (total < 0) return 'text-red-600'
|
||||
return 'text-gray-600'
|
||||
if (total > 0) return 'text-green-400'
|
||||
if (total < 0) return 'text-red-400'
|
||||
return 'text-gray-400'
|
||||
})
|
||||
|
||||
function formatCurrency(value) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div class="card">
|
||||
<!-- Header -->
|
||||
<div class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 transition-colors">
|
||||
<div class="w-full flex items-center justify-between px-3 py-2 bg-gray-900 hover:bg-gray-800 transition-colors">
|
||||
<button
|
||||
@click="expanded = !expanded"
|
||||
class="flex items-center gap-2 flex-1 text-left"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-gray-400 transition-transform flex-shrink-0"
|
||||
class="w-3 h-3 text-gray-500 transition-transform flex-shrink-0"
|
||||
:class="{ 'rotate-90': expanded }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -15,25 +15,25 @@
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span class="font-medium text-gray-800 text-sm">{{ formatCategorie(categorie) }}</span>
|
||||
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
|
||||
<span class="font-medium text-white text-sm">{{ formatCategorie(categorie) }}</span>
|
||||
<span class="badge badge-neutral rounded-full">
|
||||
{{ operations.length }} operation{{ operations.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="changedIndices.length"
|
||||
class="text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 border border-amber-300"
|
||||
class="badge badge-warning text-[10px] uppercase tracking-wide"
|
||||
>
|
||||
{{ changedIndices.length }} modifiée{{ changedIndices.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
<div class="text-sm font-semibold text-gray-700">
|
||||
<div class="text-sm font-semibold text-gray-200">
|
||||
{{ formatCurrency(totalDebit) }}
|
||||
</div>
|
||||
<!-- Bouton ajouter operation -->
|
||||
<button
|
||||
@click.stop="addOperation"
|
||||
class="p-1 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded transition-colors"
|
||||
class="p-1 text-gray-500 hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors"
|
||||
title="Ajouter une operation"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -44,20 +44,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-show="expanded" class="border-t border-gray-100 bg-gray-50">
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div v-show="expanded" class="border-t border-gray-700 bg-gray-900/60">
|
||||
<div class="divide-y divide-gray-800">
|
||||
<div
|
||||
v-for="(op, idx) in operations"
|
||||
:key="idx"
|
||||
:ref="el => { if (el) opRefs[idx] = el }"
|
||||
class="px-3 py-2 bg-white text-xs transition-all duration-300"
|
||||
:class="changedIndices.includes(idx) ? 'ring-2 ring-amber-400 bg-amber-50' : (highlightedIdx === idx ? 'ring-2 ring-blue-400 bg-blue-50' : '')"
|
||||
class="px-3 py-2 text-xs transition-all duration-300"
|
||||
:class="changedIndices.includes(idx) ? 'ring-2 ring-amber-500/50 bg-amber-500/5' : (highlightedIdx === idx ? 'ring-2 ring-blue-500/50 bg-blue-500/5' : '')"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex-1 min-w-0 space-y-1">
|
||||
<!-- Sous-categorie -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-400 text-xs w-20">Sous-cat:</span>
|
||||
<span class="text-gray-500 text-xs w-20">Sous-cat:</span>
|
||||
<EditableField
|
||||
:modelValue="op.sous_categorie"
|
||||
@update:modelValue="updateOperationField(idx, 'sous_categorie', $event)"
|
||||
@@ -69,19 +69,19 @@
|
||||
|
||||
<!-- Fournisseur -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-400 text-xs w-20">Fournisseur:</span>
|
||||
<span class="text-gray-500 text-xs w-20">Fournisseur:</span>
|
||||
<EditableField
|
||||
:modelValue="op.fournisseur"
|
||||
@update:modelValue="updateOperationField(idx, 'fournisseur', $event)"
|
||||
type="text"
|
||||
placeholder="-"
|
||||
displayClass="font-medium text-gray-800"
|
||||
displayClass="font-medium text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-400 text-xs w-20">Description:</span>
|
||||
<span class="text-gray-500 text-xs w-20">Description:</span>
|
||||
<EditableField
|
||||
:modelValue="op.description"
|
||||
@update:modelValue="updateOperationField(idx, 'description', $event)"
|
||||
@@ -94,20 +94,20 @@
|
||||
|
||||
<!-- Lot -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-400 text-xs w-20">Lot:</span>
|
||||
<span class="text-gray-500 text-xs w-20">Lot:</span>
|
||||
<EditableField
|
||||
:modelValue="op.lot_numero"
|
||||
@update:modelValue="updateOperationField(idx, 'lot_numero', $event)"
|
||||
type="text"
|
||||
placeholder="N°"
|
||||
displayClass="text-blue-500 font-medium text-xs"
|
||||
displayClass="text-blue-400 font-medium text-xs"
|
||||
/>
|
||||
<EditableField
|
||||
:modelValue="op.lot_concerne"
|
||||
@update:modelValue="updateOperationField(idx, 'lot_concerne', $event)"
|
||||
type="text"
|
||||
placeholder="Concerne"
|
||||
displayClass="text-blue-500 text-xs"
|
||||
displayClass="text-blue-400 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,48 +116,48 @@
|
||||
<!-- Montants -->
|
||||
<div class="text-right space-y-0.5">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-gray-400 text-xs">Debit:</span>
|
||||
<span class="text-gray-500 text-xs">Debit:</span>
|
||||
<EditableField
|
||||
:modelValue="op.montants?.debit"
|
||||
@update:modelValue="updateOperationField(idx, 'montants.debit', $event)"
|
||||
type="currency"
|
||||
displayClass="font-semibold text-gray-800"
|
||||
displayClass="font-semibold text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-gray-400 text-xs">Credit:</span>
|
||||
<span class="text-gray-500 text-xs">Credit:</span>
|
||||
<EditableField
|
||||
:modelValue="op.montants?.credit"
|
||||
@update:modelValue="updateOperationField(idx, 'montants.credit', $event)"
|
||||
type="currency"
|
||||
displayClass="text-green-600"
|
||||
displayClass="text-green-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-gray-400 text-xs">TVA:</span>
|
||||
<span class="text-gray-500 text-xs">TVA:</span>
|
||||
<EditableField
|
||||
:modelValue="op.montants?.tva"
|
||||
@update:modelValue="updateOperationField(idx, 'montants.tva', $event)"
|
||||
type="currency"
|
||||
displayClass="text-gray-400"
|
||||
displayClass="text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-gray-400 text-xs">Locatif:</span>
|
||||
<span class="text-gray-500 text-xs">Locatif:</span>
|
||||
<EditableField
|
||||
:modelValue="op.montants?.locatif"
|
||||
@update:modelValue="updateOperationField(idx, 'montants.locatif', $event)"
|
||||
type="currency"
|
||||
displayClass="text-green-600 text-xs"
|
||||
displayClass="text-green-400 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-gray-400 text-xs">Deductible:</span>
|
||||
<span class="text-gray-500 text-xs">Deductible:</span>
|
||||
<EditableField
|
||||
:modelValue="op.montants?.deductible"
|
||||
@update:modelValue="updateOperationField(idx, 'montants.deductible', $event)"
|
||||
type="currency"
|
||||
displayClass="text-orange-600 text-xs"
|
||||
displayClass="text-orange-400 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@
|
||||
<!-- Bouton supprimer -->
|
||||
<button
|
||||
@click="removeOperation(idx)"
|
||||
class="p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
|
||||
class="p-1 text-gray-500 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors"
|
||||
title="Supprimer cette operation"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -178,11 +178,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Totaux de la categorie -->
|
||||
<div class="px-3 py-2 bg-gray-100 border-t border-gray-200 text-xs">
|
||||
<div class="px-3 py-2 bg-gray-800 border-t border-gray-700 text-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium text-gray-600">Total {{ formatCategorie(categorie) }}</span>
|
||||
<span class="font-medium text-gray-400">Total {{ formatCategorie(categorie) }}</span>
|
||||
<div class="text-right">
|
||||
<span class="font-semibold text-gray-800">{{ formatCurrency(totalDebit) }}</span>
|
||||
<span class="font-semibold text-white">{{ formatCurrency(totalDebit) }}</span>
|
||||
<span v-if="totalTva > 0" class="text-gray-500 ml-2">(TVA: {{ formatCurrency(totalTva) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-gray-800 overflow-hidden relative">
|
||||
<div class="flex flex-col h-full bg-gray-950 overflow-hidden relative">
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 flex items-center px-3 py-2 bg-gray-900 text-white">
|
||||
<div class="flex-shrink-0 flex items-center px-3 py-2 bg-gray-900 border-b border-gray-700 text-white">
|
||||
<span class="text-sm font-medium truncate">{{ fileName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- États -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex-1 flex items-center justify-center text-gray-400 text-sm"
|
||||
class="flex-1 flex items-center justify-center text-gray-500 text-sm"
|
||||
>
|
||||
Chargement du PDF…
|
||||
</div>
|
||||
@@ -24,12 +24,12 @@
|
||||
<div
|
||||
v-else-if="hasSource"
|
||||
ref="container"
|
||||
class="flex-1 overflow-auto p-2 bg-gray-800"
|
||||
class="flex-1 overflow-auto custom-scrollbar p-2 bg-gray-950"
|
||||
/>
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-else
|
||||
class="flex-1 flex items-center justify-center text-gray-400 text-sm"
|
||||
class="flex-1 flex items-center justify-center text-gray-500 text-sm"
|
||||
>
|
||||
Aucun PDF sélectionné
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div
|
||||
class="border-2 border-dashed rounded-lg p-8 text-center transition-colors"
|
||||
:class="{
|
||||
'border-blue-500 bg-blue-50': isDragging,
|
||||
'border-gray-300 hover:border-gray-400': !isDragging && !file,
|
||||
'border-green-500 bg-green-50': file && !isLoading
|
||||
'border-blue-500 bg-blue-500/10': isDragging,
|
||||
'border-gray-700 hover:border-gray-600': !isDragging && !file,
|
||||
'border-green-500 bg-green-500/10': file && !isLoading
|
||||
}"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@@ -13,11 +13,11 @@
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center gap-4">
|
||||
<svg class="animate-spin h-12 w-12 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<svg class="animate-spin h-12 w-12 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p class="text-gray-600">Extraction en cours...</p>
|
||||
<p class="text-gray-400">Extraction en cours...</p>
|
||||
</div>
|
||||
|
||||
<!-- File selected state -->
|
||||
@@ -25,18 +25,18 @@
|
||||
<svg class="h-12 w-12 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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 class="text-gray-700 font-medium">{{ file.name }}</p>
|
||||
<p class="text-gray-200 font-medium">{{ file.name }}</p>
|
||||
<p class="text-sm text-gray-500">{{ formatFileSize(file.size) }}</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="extractPdf"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Extraire les donnees
|
||||
</button>
|
||||
<button
|
||||
@click="clearFile"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
@@ -45,12 +45,12 @@
|
||||
|
||||
<!-- Default upload state -->
|
||||
<div v-else class="flex flex-col items-center gap-4">
|
||||
<svg class="h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="h-12 w-12 text-gray-500" 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>
|
||||
<div>
|
||||
<p class="text-gray-600 mb-1">Glissez un PDF ici ou</p>
|
||||
<label class="cursor-pointer text-blue-600 hover:text-blue-700 font-medium">
|
||||
<p class="text-gray-400 mb-1">Glissez un PDF ici ou</p>
|
||||
<label class="cursor-pointer text-blue-400 hover:text-blue-300 font-medium">
|
||||
cliquez pour selectionner
|
||||
<input
|
||||
type="file"
|
||||
@@ -60,12 +60,12 @@
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400">Fichiers PDF uniquement</p>
|
||||
<p class="text-sm text-gray-500">Fichiers PDF uniquement</p>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div v-if="error" class="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p class="text-red-600 text-sm">{{ error }}</p>
|
||||
<div v-if="error" class="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p class="text-red-400 text-sm">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="flex-shrink-0 border-b border-amber-300 bg-amber-50">
|
||||
<div class="flex-shrink-0 border-b border-amber-500/40 bg-amber-500/10">
|
||||
<!-- En-tête -->
|
||||
<div class="flex items-center justify-between px-4 py-2">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span v-if="diff.summary.total === 0" class="font-medium text-amber-800">
|
||||
<span v-if="diff.summary.total === 0" class="font-medium text-amber-300">
|
||||
Nouvelle extraction — aucune différence avec la version précédente
|
||||
</span>
|
||||
<span v-else class="font-medium text-amber-800">
|
||||
<span v-else class="font-medium text-amber-300">
|
||||
Nouvelle extraction — {{ diff.summary.total }} différence{{ diff.summary.total > 1 ? 's' : '' }}
|
||||
<span class="text-amber-600 font-normal">
|
||||
<span class="text-amber-400/80 font-normal">
|
||||
({{ diff.summary.metadata }} métadonnée{{ diff.summary.metadata > 1 ? 's' : '' }},
|
||||
{{ diff.summary.locataires }} locataire{{ diff.summary.locataires > 1 ? 's' : '' }},
|
||||
{{ diff.summary.operations }} opération{{ diff.summary.operations > 1 ? 's' : '' }})
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="$emit('revert')"
|
||||
class="px-2 py-1 text-xs text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded transition-colors"
|
||||
class="px-2 py-1 text-xs text-amber-300 hover:text-amber-100 hover:bg-amber-500/20 rounded transition-colors"
|
||||
title="Restaurer les données d'avant la re-extraction"
|
||||
>
|
||||
Revenir à la version précédente
|
||||
@@ -30,13 +30,13 @@
|
||||
<button
|
||||
v-if="diff.summary.total > 0"
|
||||
@click="open = !open"
|
||||
class="px-2 py-1 text-xs text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded transition-colors"
|
||||
class="px-2 py-1 text-xs text-amber-300 hover:text-amber-100 hover:bg-amber-500/20 rounded transition-colors"
|
||||
>
|
||||
{{ open ? 'Masquer le détail' : 'Voir le détail' }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="p-1 text-amber-500 hover:text-amber-700 hover:bg-amber-100 rounded transition-colors"
|
||||
class="p-1 text-amber-400 hover:text-amber-200 hover:bg-amber-500/20 rounded transition-colors"
|
||||
title="Fermer"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -50,33 +50,33 @@
|
||||
<div v-if="open && diff.summary.total > 0" class="max-h-56 overflow-auto px-4 pb-3 space-y-3 text-xs">
|
||||
<!-- Métadonnées -->
|
||||
<div v-if="diff.metadata.length">
|
||||
<div class="font-semibold text-amber-800 mb-1">Métadonnées</div>
|
||||
<div class="font-semibold text-amber-300 mb-1">Métadonnées</div>
|
||||
<div class="space-y-0.5">
|
||||
<div v-for="(c, i) in diff.metadata" :key="'m' + i" class="flex items-baseline gap-2">
|
||||
<span class="text-gray-500 w-40 flex-shrink-0">{{ c.label }}</span>
|
||||
<span class="text-red-600 line-through">{{ c.before }}</span>
|
||||
<span class="text-gray-400">→</span>
|
||||
<span class="text-green-700 font-medium">{{ c.after }}</span>
|
||||
<span class="text-red-400 line-through">{{ c.before }}</span>
|
||||
<span class="text-gray-500">→</span>
|
||||
<span class="text-green-400 font-medium">{{ c.after }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Locataires -->
|
||||
<div v-if="diff.locataires.length">
|
||||
<div class="font-semibold text-amber-800 mb-1">Locataires</div>
|
||||
<div class="font-semibold text-amber-300 mb-1">Locataires</div>
|
||||
<div class="space-y-1.5">
|
||||
<div v-for="(loc, i) in diff.locataires" :key="'l' + i" class="bg-white/60 rounded p-1.5">
|
||||
<div v-for="(loc, i) in diff.locataires" :key="'l' + i" class="bg-gray-900/70 border border-gray-700 rounded p-1.5">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<span :class="kindClass(loc.kind)" class="text-[10px] uppercase font-semibold px-1 rounded">
|
||||
{{ kindLabel(loc.kind) }}
|
||||
</span>
|
||||
<span class="font-medium text-gray-700">{{ loc.title }}</span>
|
||||
<span class="font-medium text-gray-200">{{ loc.title }}</span>
|
||||
</div>
|
||||
<div v-for="(f, j) in loc.fields" :key="j" class="flex items-baseline gap-2 pl-2">
|
||||
<span class="text-gray-500 w-24 flex-shrink-0">{{ f.label }}</span>
|
||||
<span class="text-red-600 line-through break-all">{{ f.before }}</span>
|
||||
<span class="text-gray-400">→</span>
|
||||
<span class="text-green-700 font-medium break-all">{{ f.after }}</span>
|
||||
<span class="text-red-400 line-through break-all">{{ f.before }}</span>
|
||||
<span class="text-gray-500">→</span>
|
||||
<span class="text-green-400 font-medium break-all">{{ f.after }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,9 +84,9 @@
|
||||
|
||||
<!-- Opérations -->
|
||||
<div v-if="diff.operations.length">
|
||||
<div class="font-semibold text-amber-800 mb-1">Opérations</div>
|
||||
<div class="font-semibold text-amber-300 mb-1">Opérations</div>
|
||||
<div class="space-y-1.5">
|
||||
<div v-for="(op, i) in diff.operations" :key="'o' + i" class="bg-white/60 rounded p-1.5">
|
||||
<div v-for="(op, i) in diff.operations" :key="'o' + i" class="bg-gray-900/70 border border-gray-700 rounded p-1.5">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<span :class="kindClass(op.kind)" class="text-[10px] uppercase font-semibold px-1 rounded">
|
||||
{{ kindLabel(op.kind) }}
|
||||
@@ -94,8 +94,8 @@
|
||||
<span v-if="op.index >= 0" class="text-gray-500">opération #{{ op.index + 1 }}</span>
|
||||
</div>
|
||||
<div class="pl-2">
|
||||
<div class="text-red-600 line-through break-all">{{ op.before }}</div>
|
||||
<div class="text-green-700 font-medium break-all">{{ op.after }}</div>
|
||||
<div class="text-red-400 line-through break-all">{{ op.before }}</div>
|
||||
<div class="text-green-400 font-medium break-all">{{ op.after }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,8 +125,8 @@ function kindLabel(kind) {
|
||||
}
|
||||
|
||||
function kindClass(kind) {
|
||||
if (kind === 'added') return 'bg-green-100 text-green-700'
|
||||
if (kind === 'removed') return 'bg-red-100 text-red-700'
|
||||
return 'bg-amber-100 text-amber-700'
|
||||
if (kind === 'added') return 'bg-green-500/20 text-green-300'
|
||||
if (kind === 'removed') return 'bg-red-500/20 text-red-300'
|
||||
return 'bg-amber-500/20 text-amber-300'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
@keydown.escape="closeDropdown"
|
||||
@keydown.tab="closeDropdown"
|
||||
:placeholder="placeholder"
|
||||
class="w-full px-3 py-2 pr-8 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
class="w-full px-3 py-2 pr-8 border rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
:class="inputClasses"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<button
|
||||
v-if="modelValue"
|
||||
@mousedown.prevent="clearSelection"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 rounded"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-500 hover:text-white rounded"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -34,7 +34,7 @@
|
||||
<button
|
||||
v-else
|
||||
@mousedown.prevent="toggleDropdown"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 rounded"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-500 hover:text-white rounded"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': isOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -46,7 +46,7 @@
|
||||
<!-- Dropdown -->
|
||||
<div
|
||||
v-show="isOpen && filteredOptions.length > 0"
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto"
|
||||
class="absolute z-50 w-full mt-1 bg-gray-900 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto custom-scrollbar"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in filteredOptions"
|
||||
@@ -55,8 +55,8 @@
|
||||
@mouseenter="highlightedIndex = index"
|
||||
class="px-3 py-2 cursor-pointer text-sm transition-colors"
|
||||
:class="{
|
||||
'bg-blue-50 text-blue-700': index === highlightedIndex,
|
||||
'hover:bg-gray-50': index !== highlightedIndex
|
||||
'bg-blue-500/20 text-blue-300': index === highlightedIndex,
|
||||
'text-gray-300 hover:bg-gray-800': index !== highlightedIndex
|
||||
}"
|
||||
>
|
||||
<span v-html="highlightMatch(option.nom)"></span>
|
||||
@@ -66,7 +66,7 @@
|
||||
<!-- No results -->
|
||||
<div
|
||||
v-show="isOpen && searchQuery && filteredOptions.length === 0"
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg p-3 text-sm text-gray-500"
|
||||
class="absolute z-50 w-full mt-1 bg-gray-900 border border-gray-700 rounded-lg shadow-lg p-3 text-sm text-gray-500"
|
||||
>
|
||||
Aucun tag trouvé pour "{{ searchQuery }}"
|
||||
</div>
|
||||
@@ -112,12 +112,12 @@ const isUserInput = ref(true)
|
||||
// Compute input classes based on state
|
||||
const inputClasses = computed(() => {
|
||||
if (props.modelValue && props.hasPrediction) {
|
||||
return 'bg-green-50 border-green-300'
|
||||
return 'bg-gray-950 border-green-500/60'
|
||||
}
|
||||
if (!props.modelValue) {
|
||||
return 'bg-yellow-50 border-yellow-300'
|
||||
return 'bg-gray-950 border-amber-500/60'
|
||||
}
|
||||
return 'border-gray-300'
|
||||
return 'bg-gray-950 border-gray-700'
|
||||
})
|
||||
|
||||
// Get selected option name for display
|
||||
@@ -249,6 +249,6 @@ function highlightMatch(text) {
|
||||
|
||||
const query = searchQuery.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
return text.replace(regex, '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>')
|
||||
return text.replace(regex, '<mark class="bg-amber-500/30 text-amber-200 rounded px-0.5">$1</mark>')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-gray-50">
|
||||
<div class="flex flex-col h-full bg-gray-950">
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 p-4 bg-white border-b border-gray-200">
|
||||
<div class="flex-shrink-0 p-4 bg-gray-900 border-b border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Tagging des dépenses</h2>
|
||||
<span v-if="isDuplicate" class="px-2 py-1 text-xs font-medium bg-orange-100 text-orange-800 rounded">
|
||||
<h2 class="page-title">Tagging des dépenses</h2>
|
||||
<span v-if="isDuplicate" class="badge badge-warning">
|
||||
⚠️ Écrasement
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
<p class="page-subtitle">
|
||||
{{ taggedCount }} / {{ depenses.length }} dépenses taggées
|
||||
<span v-if="predictions.length > 0" class="text-green-600 ml-2">
|
||||
<span v-if="predictions.length > 0" class="text-green-400 ml-2">
|
||||
({{ predictedCount }} pré-remplies automatiquement)
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="isDuplicate" class="text-xs text-orange-600 mt-1">
|
||||
<p v-if="isDuplicate" class="text-xs text-amber-400 mt-1">
|
||||
Les données existantes seront remplacées lors de la validation
|
||||
</p>
|
||||
</div>
|
||||
@@ -22,8 +22,8 @@
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingPredictions" class="flex-1 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p class="text-gray-600">Analyse en cours...</p>
|
||||
<div class="spinner h-12 w-12 mx-auto mb-4"></div>
|
||||
<p class="text-gray-400">Analyse en cours...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,15 +32,15 @@
|
||||
<div
|
||||
v-for="(depense, index) in depenses"
|
||||
:key="index"
|
||||
class="bg-white rounded-lg shadow-sm border border-gray-200 p-4"
|
||||
class="card card-body"
|
||||
>
|
||||
<!-- Expense info -->
|
||||
<div class="flex items-start justify-between gap-4 mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-gray-900 truncate">
|
||||
<div class="font-medium text-white truncate">
|
||||
{{ depense.fournisseur || 'Fournisseur inconnu' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 truncate">
|
||||
<div class="text-sm text-gray-400 truncate">
|
||||
{{ depense.sous_categorie || depense.description || 'Aucune description' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mt-1">
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Tag selector -->
|
||||
<div class="flex-shrink-0 w-56">
|
||||
<div class="flex-shrink-0 w-64">
|
||||
<TagAutocomplete
|
||||
v-model="selectedTags[index]"
|
||||
:options="availableTags"
|
||||
@@ -64,9 +64,9 @@
|
||||
<!-- Prediction info -->
|
||||
<div
|
||||
v-if="predictions[index] && predictions[index].tag_id"
|
||||
class="flex items-center gap-2 text-xs text-gray-600 bg-blue-50 border border-blue-200 rounded px-3 py-2"
|
||||
class="flex items-center gap-2 text-xs text-gray-300 bg-blue-500/10 border border-blue-500/30 rounded px-3 py-2"
|
||||
>
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>
|
||||
@@ -77,7 +77,7 @@
|
||||
<!-- No prediction warning -->
|
||||
<div
|
||||
v-else-if="predictions[index] && !predictions[index].tag_id && !selectedTags[index]"
|
||||
class="flex items-center gap-2 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded px-3 py-2"
|
||||
class="flex items-center gap-2 text-xs text-gray-400 bg-gray-800 border border-gray-700 rounded px-3 py-2"
|
||||
>
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
@@ -88,23 +88,23 @@
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex-shrink-0 p-4 bg-white border-t border-gray-200">
|
||||
<div class="flex-shrink-0 p-4 bg-gray-900 border-t border-gray-700">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<button
|
||||
@click="$emit('cancel')"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Annuler
|
||||
Retour à l'édition
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<span v-if="taggedCount < depenses.length" class="text-sm text-yellow-600">
|
||||
<span v-if="taggedCount < depenses.length" class="text-sm text-amber-400">
|
||||
⚠️ {{ depenses.length - taggedCount }} dépense(s) sans tag
|
||||
</span>
|
||||
<button
|
||||
@click="validateAndSave"
|
||||
:disabled="taggedCount === 0"
|
||||
class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-primary px-6"
|
||||
>
|
||||
Valider et enregistrer
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-300 mb-4">Repartition par categorie</h3>
|
||||
<div class="card card-body">
|
||||
<h3 class="card-title block mb-4">Repartition par categorie</h3>
|
||||
<div class="h-64">
|
||||
<Doughnut
|
||||
v-if="chartData.labels.length > 0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-700 flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-300">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Detail des depenses
|
||||
<span class="text-gray-500 font-normal ml-2">({{ depenses.length }} resultats)</span>
|
||||
</h3>
|
||||
@@ -25,65 +25,64 @@
|
||||
Aucune depense trouvee avec les filtres actuels.
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-800/50">
|
||||
<div v-else class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Date</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Immeuble</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Lot</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Categorie</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Fournisseur</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Description</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Tag</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-400 uppercase">Debit</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-400 uppercase">Credit</th>
|
||||
<th class="px-3 py-2 w-10"></th>
|
||||
<th>Date</th>
|
||||
<th>Immeuble</th>
|
||||
<th>Lot</th>
|
||||
<th>Categorie</th>
|
||||
<th>Fournisseur</th>
|
||||
<th>Description</th>
|
||||
<th>Tag</th>
|
||||
<th class="text-right">Debit</th>
|
||||
<th class="text-right">Credit</th>
|
||||
<th class="w-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="dep in paginatedDepenses"
|
||||
:key="dep.id"
|
||||
class="hover:bg-gray-800/50"
|
||||
>
|
||||
<td class="px-3 py-2 text-gray-300 whitespace-nowrap">
|
||||
<td class="text-gray-300 whitespace-nowrap">
|
||||
{{ formatDate(dep.document_date) }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-400 whitespace-nowrap">
|
||||
<td class="text-gray-400 whitespace-nowrap">
|
||||
{{ dep.immeuble_code }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-400">
|
||||
<td class="text-gray-400">
|
||||
{{ dep.lot_numero || '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-300">
|
||||
<td class="text-gray-300">
|
||||
<div class="text-xs text-gray-500">{{ formatCategorie(dep.categorie) }}</div>
|
||||
<div v-if="dep.sous_categorie" class="text-xs text-gray-400 truncate max-w-[150px]">
|
||||
<div v-if="dep.sous_categorie" class="text-xs text-gray-400 truncate max-w-[220px]">
|
||||
{{ dep.sous_categorie }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-white truncate max-w-[150px]">
|
||||
<td class="text-white truncate max-w-[240px]">
|
||||
{{ dep.fournisseur || '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-400 truncate max-w-[200px]">
|
||||
<td class="text-gray-400 truncate max-w-[360px]">
|
||||
{{ dep.description || '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<td>
|
||||
<span
|
||||
v-if="dep.tag_nom"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-500/20 text-blue-400"
|
||||
class="badge badge-info"
|
||||
>
|
||||
{{ dep.tag_nom }}
|
||||
</span>
|
||||
<span v-else class="text-gray-600">-</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-right text-red-400 font-mono whitespace-nowrap">
|
||||
<td class="text-right text-red-400 font-mono whitespace-nowrap">
|
||||
{{ dep.debit > 0 ? formatCurrency(dep.debit) : '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-right text-green-400 font-mono whitespace-nowrap">
|
||||
<td class="text-right text-green-400 font-mono whitespace-nowrap">
|
||||
{{ dep.credit > 0 ? formatCurrency(dep.credit) : '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<td class="text-center">
|
||||
<router-link
|
||||
:to="{ path: '/documents/' + dep.document_id + '/edit', query: { highlight: 'operation', fournisseur: dep.fournisseur, description: dep.description } }"
|
||||
class="text-gray-500 hover:text-blue-400 transition-colors"
|
||||
@@ -108,14 +107,14 @@
|
||||
<button
|
||||
@click="currentPage = 1"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
<<
|
||||
</button>
|
||||
<button
|
||||
@click="currentPage--"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
@@ -125,14 +124,14 @@
|
||||
<button
|
||||
@click="currentPage++"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
@click="currentPage = totalPages"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
>>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4 space-y-4">
|
||||
<div class="card card-body space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-300">Filtres</h3>
|
||||
<h3 class="card-title">Filtres</h3>
|
||||
<button
|
||||
v-if="hasActiveFilters"
|
||||
@click="resetFilters"
|
||||
@@ -11,14 +11,14 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-7 gap-3">
|
||||
<!-- Immeuble -->
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Immeuble</label>
|
||||
<select
|
||||
v-model="filters.immeuble_id"
|
||||
@change="onImmeubleChange"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
>
|
||||
<option :value="null">Tous</option>
|
||||
<option v-for="imm in immeubles" :key="imm.id" :value="imm.id">
|
||||
@@ -33,7 +33,7 @@
|
||||
<select
|
||||
v-model="filters.lot_id"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
:disabled="!filters.immeuble_id"
|
||||
>
|
||||
<option :value="null">Tous</option>
|
||||
@@ -49,7 +49,7 @@
|
||||
<select
|
||||
v-model="filters.categorie"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
>
|
||||
<option :value="null">Toutes</option>
|
||||
<option v-for="cat in categories" :key="cat" :value="cat">
|
||||
@@ -64,7 +64,7 @@
|
||||
<select
|
||||
v-model="filters.tag_id"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
>
|
||||
<option :value="null">Tous</option>
|
||||
<option v-for="tag in tags" :key="tag.id" :value="tag.id">
|
||||
@@ -80,7 +80,7 @@
|
||||
type="date"
|
||||
v-model="filters.date_debut"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -91,21 +91,21 @@
|
||||
type="date"
|
||||
v-model="filters.date_fin"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fournisseur (recherche) -->
|
||||
<div class="max-w-xs">
|
||||
<label class="block text-xs text-gray-400 mb-1">Fournisseur</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="filters.fournisseur"
|
||||
@input="emitFilters"
|
||||
placeholder="Rechercher..."
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<!-- Fournisseur (recherche) -->
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Fournisseur</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="filters.fournisseur"
|
||||
@input="emitFilters"
|
||||
placeholder="Rechercher..."
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div
|
||||
v-for="kpi in kpis"
|
||||
:key="kpi.key"
|
||||
class="bg-gray-900 rounded-lg p-4"
|
||||
class="card card-body"
|
||||
>
|
||||
<div class="text-xs text-gray-400 mb-1">{{ kpi.label }}</div>
|
||||
<div class="text-xl font-bold" :class="kpi.colorClass || 'text-white'">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-300 mb-4">Evolution mensuelle</h3>
|
||||
<div class="card card-body">
|
||||
<h3 class="card-title block mb-4">Evolution mensuelle</h3>
|
||||
<div class="h-64">
|
||||
<Bar
|
||||
v-if="chartData.labels.length > 0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-300 mb-4">Top fournisseurs</h3>
|
||||
<div class="card card-body">
|
||||
<h3 class="card-title block mb-4">Top fournisseurs</h3>
|
||||
<div class="h-64">
|
||||
<Bar
|
||||
v-if="chartData.labels.length > 0"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<!-- Revenus -->
|
||||
<div class="relative bg-gray-900 rounded-lg p-4 border border-gray-700 overflow-hidden">
|
||||
<div class="card card-body relative">
|
||||
<!-- Sparkline background -->
|
||||
<svg
|
||||
v-if="data.revenus_history?.length"
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Depenses -->
|
||||
<div class="relative bg-gray-900 rounded-lg p-4 border border-gray-700 overflow-hidden">
|
||||
<div class="card card-body relative">
|
||||
<!-- Sparkline background -->
|
||||
<svg
|
||||
v-if="data.depenses_history?.length"
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Impayes -->
|
||||
<div class="relative bg-gray-900 rounded-lg p-4 border border-gray-700 overflow-hidden">
|
||||
<div class="card card-body relative">
|
||||
<!-- Sparkline background -->
|
||||
<svg
|
||||
v-if="data.impayes_history?.length"
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Solde -->
|
||||
<div class="relative bg-gray-900 rounded-lg p-4 border border-gray-700 overflow-hidden">
|
||||
<div class="card card-body relative">
|
||||
<!-- Sparkline background -->
|
||||
<svg
|
||||
v-if="data.solde_history?.length"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div 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">Vos immeubles</h2>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Vos immeubles</h2>
|
||||
<button
|
||||
@click="$emit('view-all')"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="loading" 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 class="spinner h-6 w-6"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="immeubles.length === 0" class="p-8 text-center text-gray-500">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div 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">Evolution mensuelle</h2>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Evolution mensuelle</h2>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-green-400"></span>
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="loading" 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 class="spinner h-6 w-6"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="trends.length === 0" class="p-8 text-center text-gray-500">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div 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 loyers</h2>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Derniers loyers</h2>
|
||||
<span class="text-xs text-gray-500">{{ revenus.length }} entrees</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" 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 class="spinner h-6 w-6"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="revenus.length === 0" class="p-8 text-center text-gray-500">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-white">Filtres</h3>
|
||||
<button
|
||||
@@ -17,7 +17,7 @@
|
||||
<select
|
||||
v-model="filters.immeuble_id"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
>
|
||||
<option :value="null">Tous</option>
|
||||
<option
|
||||
@@ -36,7 +36,7 @@
|
||||
<select
|
||||
v-model="filters.type_ligne"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
>
|
||||
<option :value="null">Tous</option>
|
||||
<option value="loyer">Loyer</option>
|
||||
@@ -53,7 +53,7 @@
|
||||
type="date"
|
||||
v-model="filters.date_debut"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
type="date"
|
||||
v-model="filters.date_fin"
|
||||
@change="emitFilters"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
type="checkbox"
|
||||
v-model="filters.impayes_only"
|
||||
@change="emitFilters"
|
||||
class="w-4 h-4 rounded bg-gray-800 border-gray-700 text-amber-500 focus:ring-amber-500 focus:ring-offset-gray-900"
|
||||
class="w-4 h-4 rounded border-gray-700 accent-blue-500 [color-scheme:dark]"
|
||||
/>
|
||||
<span class="text-sm text-gray-300">Impayes uniquement</span>
|
||||
</label>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-white">Revenus par immeuble</h3>
|
||||
<span class="text-xs text-gray-500">{{ data.length }} immeubles</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<!-- Total Revenus -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400 uppercase tracking-wide">Total Revenus</span>
|
||||
<svg class="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Regles -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400 uppercase tracking-wide">Regles</span>
|
||||
<svg class="w-4 h-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Impayes -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400 uppercase tracking-wide">Impayes</span>
|
||||
<div
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Taux Recouvrement -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400 uppercase tracking-wide">Recouvrement</span>
|
||||
<svg class="w-4 h-4 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Locataires Actifs -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400 uppercase tracking-wide">Locataires</span>
|
||||
<svg class="w-4 h-4 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Lots Occupes -->
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400 uppercase tracking-wide">Lots</span>
|
||||
<svg class="w-4 h-4 text-pink-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-white">Evolution mensuelle des revenus</h3>
|
||||
<div class="flex items-center gap-4 text-xs">
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-sm font-medium text-white">Detail des revenus</h3>
|
||||
<span class="text-xs text-gray-500">({{ revenus.length }} lignes)</span>
|
||||
</div>
|
||||
<button
|
||||
@click="exportCsv"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
Exporter CSV
|
||||
</button>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="card-title">Detail des revenus</h3>
|
||||
<span class="text-xs text-gray-500">({{ revenus.length }} lignes)</span>
|
||||
</div>
|
||||
<button
|
||||
@click="exportCsv"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
Exporter CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="p-8 text-center text-gray-500">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2" fill="none" viewBox="0 0 24 24">
|
||||
<div v-if="isLoading" class="empty-state">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2 text-blue-400" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
@@ -25,60 +23,59 @@
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div v-else-if="revenus.length > 0" class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-800/50 text-gray-400 text-xs uppercase">
|
||||
<div v-else-if="revenus.length > 0" class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">Date</th>
|
||||
<th class="px-4 py-3 text-left">Locataire</th>
|
||||
<th class="px-4 py-3 text-left">Lot</th>
|
||||
<th class="px-4 py-3 text-left">Type</th>
|
||||
<th class="px-4 py-3 text-right">Loyers</th>
|
||||
<th class="px-4 py-3 text-right">Taxes</th>
|
||||
<th class="px-4 py-3 text-right">Total</th>
|
||||
<th class="px-4 py-3 text-right">Regle</th>
|
||||
<th class="px-4 py-3 text-right">Impaye</th>
|
||||
<th class="px-4 py-3 w-10"></th>
|
||||
<th>Date</th>
|
||||
<th>Locataire</th>
|
||||
<th>Lot</th>
|
||||
<th>Type</th>
|
||||
<th class="text-right">Loyers</th>
|
||||
<th class="text-right">Taxes</th>
|
||||
<th class="text-right">Total</th>
|
||||
<th class="text-right">Regle</th>
|
||||
<th class="text-right">Impaye</th>
|
||||
<th class="w-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="revenu in paginatedRevenus"
|
||||
:key="revenu.id"
|
||||
class="hover:bg-gray-800/30 transition-colors"
|
||||
>
|
||||
<td class="px-4 py-3 text-gray-300 whitespace-nowrap">
|
||||
<td class="text-gray-300 whitespace-nowrap">
|
||||
{{ formatDate(revenu.document_date) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-white font-medium max-w-[200px] truncate" :title="revenu.locataire_nom">
|
||||
<td class="text-white font-medium max-w-[280px] truncate" :title="revenu.locataire_nom">
|
||||
{{ revenu.locataire_nom }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-400 whitespace-nowrap" :title="getImmeubleName(revenu)">
|
||||
<td class="text-gray-400 whitespace-nowrap" :title="getImmeubleName(revenu)">
|
||||
{{ getImmeubleName(revenu) }} / {{ revenu.lot_numero }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<td>
|
||||
<span :class="getTypeBadgeClass(revenu.type_ligne)">
|
||||
{{ revenu.type_ligne }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-gray-300 whitespace-nowrap">
|
||||
<td class="text-right text-gray-300 whitespace-nowrap">
|
||||
{{ formatAmount(revenu.loyers) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400 whitespace-nowrap">
|
||||
<td class="text-right text-gray-400 whitespace-nowrap">
|
||||
{{ formatAmount(revenu.taxes) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-green-400 font-medium whitespace-nowrap">
|
||||
<td class="text-right text-green-400 font-medium whitespace-nowrap">
|
||||
{{ formatAmount(revenu.total) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-blue-400 whitespace-nowrap">
|
||||
<td class="text-right text-blue-400 whitespace-nowrap">
|
||||
{{ formatAmount(revenu.regles) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<td class="text-right whitespace-nowrap">
|
||||
<span :class="revenu.impayes > 0 ? 'text-amber-400 font-medium' : 'text-gray-500'">
|
||||
{{ formatAmount(revenu.impayes) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<td class="text-center">
|
||||
<router-link
|
||||
:to="{ path: '/documents/' + revenu.document_id + '/edit', query: { highlight: 'locataire', lot_numero: revenu.lot_numero, locataire_nom: revenu.locataire_nom } }"
|
||||
class="text-gray-500 hover:text-blue-400 transition-colors"
|
||||
@@ -95,12 +92,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="p-8 text-center text-gray-500">
|
||||
<div v-else class="empty-state">
|
||||
Aucun revenu trouve
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="revenus.length > pageSize" class="p-4 border-t border-gray-700 flex items-center justify-between">
|
||||
<div v-if="revenus.length > pageSize" class="card-header border-b-0 border-t border-gray-700">
|
||||
<div class="text-xs text-gray-500">
|
||||
Affichage {{ startIndex + 1 }} - {{ endIndex }} sur {{ revenus.length }}
|
||||
</div>
|
||||
@@ -108,14 +105,14 @@
|
||||
<button
|
||||
@click="currentPage = 1"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
@click="currentPage--"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
@@ -125,14 +122,14 @@
|
||||
<button
|
||||
@click="currentPage++"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
@click="currentPage = totalPages"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div class="card card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-sm font-medium text-white">Locataires avec impayes</h3>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white">Analyse des depenses</h1>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
<h1 class="page-title">Analyse des depenses</h1>
|
||||
<p class="page-subtitle">
|
||||
Visualisez et filtrez les depenses de vos immeubles
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,114 +1,128 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Configuration</h1>
|
||||
<p class="text-gray-400 text-sm mt-1">Paramètres LLM et gestion des tags</p>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Configuration</h1>
|
||||
<p class="page-subtitle">Paramètres LLM et gestion des tags</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section LLM -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Configuration LLM (Ollama)</h2>
|
||||
<!-- Les deux sections cote a cote des que la largeur le permet -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 items-start">
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="field in settingsFields" :key="field.key" class="flex items-end gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<label class="text-sm font-medium text-gray-300">{{ field.label }}</label>
|
||||
<span
|
||||
class="text-xs px-1.5 py-0.5 rounded"
|
||||
:class="sourceBadgeClass(settingsSources[field.key])"
|
||||
>{{ settingsSources[field.key] || 'default' }}</span>
|
||||
<!-- Section LLM -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Configuration LLM (Ollama)</h2>
|
||||
</div>
|
||||
<div class="card-body space-y-4">
|
||||
<div v-for="field in settingsFields" :key="field.key" class="flex items-end gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<label class="form-label mb-0">{{ field.label }}</label>
|
||||
<span class="badge" :class="sourceBadgeClass(settingsSources[field.key])">
|
||||
{{ settingsSources[field.key] || 'default' }}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
v-model="settingsForm[field.key]"
|
||||
:type="field.type || 'text'"
|
||||
:placeholder="field.placeholder"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model="settingsForm[field.key]"
|
||||
:type="field.type || 'text'"
|
||||
:placeholder="field.placeholder"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
|
||||
/>
|
||||
<button
|
||||
@click="resetSetting(field.key)"
|
||||
class="btn btn-secondary"
|
||||
title="Reset au défaut"
|
||||
>Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
@click="saveSettings"
|
||||
:disabled="savingSettings"
|
||||
class="btn btn-primary"
|
||||
>{{ savingSettings ? 'Enregistrement...' : 'Enregistrer' }}</button>
|
||||
<span v-if="settingsMessage" class="text-sm" :class="settingsMessageError ? 'text-red-400' : 'text-green-400'">{{ settingsMessage }}</span>
|
||||
</div>
|
||||
<button
|
||||
@click="resetSetting(field.key)"
|
||||
class="px-3 py-2 text-sm text-gray-400 border border-gray-600 rounded-lg hover:text-white hover:border-gray-500 transition-colors"
|
||||
title="Reset au défaut"
|
||||
>Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<button
|
||||
@click="saveSettings"
|
||||
:disabled="savingSettings"
|
||||
class="px-5 py-2 bg-blue-600 text-white rounded-lg font-medium transition-colors disabled:opacity-50 hover:bg-blue-700"
|
||||
>{{ savingSettings ? 'Enregistrement...' : 'Enregistrer' }}</button>
|
||||
<span v-if="settingsMessage" class="text-sm" :class="settingsMessageError ? 'text-red-400' : 'text-green-400'">{{ settingsMessage }}</span>
|
||||
<!-- Section Tags -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Gestion des tags</h2>
|
||||
<span class="text-xs text-gray-500">{{ tags.length }} tags</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16">ID</th>
|
||||
<th>Nom</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tag in tags" :key="tag.id">
|
||||
<td class="text-gray-500">{{ tag.id }}</td>
|
||||
<td>
|
||||
<input
|
||||
v-if="editingTagId === tag.id"
|
||||
v-model="editingTagNom"
|
||||
class="input py-1"
|
||||
@keydown.enter="confirmRenameTag(tag.id)"
|
||||
@keydown.escape="cancelEditTag"
|
||||
ref="editInput"
|
||||
/>
|
||||
<span v-else class="text-white">{{ tag.nom }}</span>
|
||||
</td>
|
||||
<td class="text-right whitespace-nowrap">
|
||||
<template v-if="editingTagId === tag.id">
|
||||
<button
|
||||
@click="confirmRenameTag(tag.id)"
|
||||
class="text-green-400 hover:text-green-300 text-sm mr-2"
|
||||
>Valider</button>
|
||||
<button
|
||||
@click="cancelEditTag"
|
||||
class="text-gray-400 hover:text-white text-sm"
|
||||
>Annuler</button>
|
||||
</template>
|
||||
<button
|
||||
v-else
|
||||
@click="startEditTag(tag)"
|
||||
class="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>Renommer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Ajout d'un nouveau tag -->
|
||||
<div class="card-body border-t border-gray-700">
|
||||
<div class="flex gap-3">
|
||||
<input
|
||||
v-model="newTagNom"
|
||||
type="text"
|
||||
placeholder="Nouveau tag..."
|
||||
class="input"
|
||||
@keydown.enter="createTag"
|
||||
/>
|
||||
<button
|
||||
@click="createTag"
|
||||
:disabled="!newTagNom.trim()"
|
||||
class="btn btn-primary whitespace-nowrap"
|
||||
>Ajouter</button>
|
||||
</div>
|
||||
<p v-if="tagMessage" class="mt-2 text-sm" :class="tagMessageError ? 'text-red-400' : 'text-green-400'">{{ tagMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Tags -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Gestion des tags</h2>
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700">
|
||||
<th class="text-left py-2 text-gray-400 font-medium">ID</th>
|
||||
<th class="text-left py-2 text-gray-400 font-medium">Nom</th>
|
||||
<th class="text-right py-2 text-gray-400 font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tag in tags" :key="tag.id" class="border-b border-gray-700/50">
|
||||
<td class="py-2 text-gray-500">{{ tag.id }}</td>
|
||||
<td class="py-2">
|
||||
<input
|
||||
v-if="editingTagId === tag.id"
|
||||
v-model="editingTagNom"
|
||||
class="bg-gray-900 border border-blue-500 rounded px-2 py-1 text-white text-sm focus:outline-none"
|
||||
@keydown.enter="confirmRenameTag(tag.id)"
|
||||
@keydown.escape="cancelEditTag"
|
||||
ref="editInput"
|
||||
/>
|
||||
<span v-else class="text-white">{{ tag.nom }}</span>
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<template v-if="editingTagId === tag.id">
|
||||
<button
|
||||
@click="confirmRenameTag(tag.id)"
|
||||
class="text-green-400 hover:text-green-300 text-sm mr-2"
|
||||
>Valider</button>
|
||||
<button
|
||||
@click="cancelEditTag"
|
||||
class="text-gray-400 hover:text-gray-300 text-sm"
|
||||
>Annuler</button>
|
||||
</template>
|
||||
<button
|
||||
v-else
|
||||
@click="startEditTag(tag)"
|
||||
class="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>Renommer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Ajout d'un nouveau tag -->
|
||||
<div class="mt-4 flex gap-3">
|
||||
<input
|
||||
v-model="newTagNom"
|
||||
type="text"
|
||||
placeholder="Nouveau tag..."
|
||||
class="flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors text-sm"
|
||||
@keydown.enter="createTag"
|
||||
/>
|
||||
<button
|
||||
@click="createTag"
|
||||
:disabled="!newTagNom.trim()"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 hover:bg-green-700"
|
||||
>Ajouter</button>
|
||||
</div>
|
||||
<p v-if="tagMessage" class="mt-2 text-sm" :class="tagMessageError ? 'text-red-400' : 'text-green-400'">{{ tagMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,9 +201,9 @@ async function resetSetting(key) {
|
||||
}
|
||||
|
||||
function sourceBadgeClass(source) {
|
||||
if (source === 'database') return 'bg-blue-600/30 text-blue-400'
|
||||
if (source === 'env') return 'bg-yellow-600/30 text-yellow-400'
|
||||
return 'bg-gray-600/30 text-gray-400'
|
||||
if (source === 'database') return 'badge-info'
|
||||
if (source === 'env') return 'badge-warning'
|
||||
return 'badge-neutral'
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-800">
|
||||
<div class="max-w-6xl mx-auto space-y-6">
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white">Documents importes</h1>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
<h1 class="page-title">Documents importes</h1>
|
||||
<p class="page-subtitle">
|
||||
{{ documents.length }} document{{ documents.length > 1 ? 's' : '' }} en base
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Import button -->
|
||||
<label class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<label class="btn btn-primary cursor-pointer">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
Importer un PDF
|
||||
@@ -27,13 +27,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="bg-gray-900 rounded-lg p-12 text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-2 border-gray-600 border-t-blue-400"></div>
|
||||
<div v-if="isLoading" class="card empty-state">
|
||||
<div class="spinner h-8 w-8"></div>
|
||||
<p class="text-gray-400 mt-4">Chargement des documents...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="documents.length === 0" class="bg-gray-900 rounded-lg p-12 text-center">
|
||||
<div v-else-if="documents.length === 0" class="card empty-state">
|
||||
<svg class="mx-auto h-16 w-16 text-gray-600 mb-4" 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>
|
||||
@@ -42,26 +42,26 @@
|
||||
</div>
|
||||
|
||||
<!-- Documents table -->
|
||||
<div v-else class="bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-800 border-b border-gray-700">
|
||||
<div v-else class="card">
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Document</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Immeuble</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Date</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">Solde</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Fichiers</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
<th>Document</th>
|
||||
<th>Immeuble</th>
|
||||
<th>Date</th>
|
||||
<th class="text-right">Solde</th>
|
||||
<th class="text-center">Fichiers</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
class="hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<!-- Document info -->
|
||||
<td class="px-4 py-4">
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-red-500/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -77,19 +77,19 @@
|
||||
</td>
|
||||
|
||||
<!-- Immeuble -->
|
||||
<td class="px-4 py-4">
|
||||
<td>
|
||||
<div class="text-sm text-white">{{ doc.immeuble_adresse || '-' }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono">{{ doc.immeuble_code }}</div>
|
||||
</td>
|
||||
|
||||
<!-- Date -->
|
||||
<td class="px-4 py-4">
|
||||
<td>
|
||||
<div class="text-sm text-white">{{ formatDate(doc.date) }}</div>
|
||||
<div class="text-xs text-gray-500">Importe le {{ formatDateTime(doc.created_at) }}</div>
|
||||
</td>
|
||||
|
||||
<!-- Solde -->
|
||||
<td class="px-4 py-4 text-right">
|
||||
<td class="text-right">
|
||||
<span
|
||||
:class="[
|
||||
'text-sm font-medium',
|
||||
@@ -102,7 +102,7 @@
|
||||
</td>
|
||||
|
||||
<!-- Files status -->
|
||||
<td class="px-4 py-4">
|
||||
<td>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<!-- PDF download -->
|
||||
<a
|
||||
@@ -143,12 +143,12 @@
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-4 text-right">
|
||||
<td class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<!-- Edit & reimport button -->
|
||||
<button
|
||||
@click="router.push(`/documents/${doc.id}/edit`)"
|
||||
class="px-3 py-1.5 text-xs rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white transition-colors flex items-center gap-1"
|
||||
class="btn btn-sm btn-secondary"
|
||||
title="Editer et reimporter"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -161,6 +161,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<div class="page-fixed">
|
||||
<!-- Header avec breadcrumb et actions -->
|
||||
<div class="flex-shrink-0 px-6 py-4 bg-gray-800 border-b border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-shrink-0 px-6 py-4 bg-gray-900 border-b border-gray-700">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400 mb-1">
|
||||
<router-link to="/documents" class="hover:text-white transition-colors">Documents</router-link>
|
||||
<span>›</span>
|
||||
<span class="text-white">{{ documentData?.reference || 'Chargement...' }}</span>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold text-white">
|
||||
<h1 class="page-title">
|
||||
Édition du document {{ documentData?.reference }}
|
||||
</h1>
|
||||
<p v-if="documentData" class="text-sm text-gray-400 mt-1">
|
||||
<p v-if="documentData" class="page-subtitle">
|
||||
{{ documentData.immeuble?.adresse || '-' }} - {{ formatDate(documentData.date) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Masque pendant le tagging : celui du bandeau de tagging fait foi. -->
|
||||
<button
|
||||
v-if="!showTagging"
|
||||
@click="cancel"
|
||||
:disabled="isSaving"
|
||||
class="px-4 py-2 text-sm text-gray-400 hover:text-white transition-colors disabled:opacity-50"
|
||||
class="btn btn-ghost"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
@@ -30,7 +32,7 @@
|
||||
@click="reExtract"
|
||||
:disabled="isSaving || isReExtracting || !documentData?.has_pdf"
|
||||
:title="documentData?.has_pdf ? 'Relancer l\'extraction depuis le PDF stocké' : 'PDF non disponible pour ce document'"
|
||||
class="px-4 py-2 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
class="btn btn-warning"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
@@ -46,7 +48,7 @@
|
||||
v-if="!showTagging && extractedData"
|
||||
@click="goToTagging"
|
||||
:disabled="isSaving"
|
||||
class="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
@@ -58,9 +60,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="flex-1 flex items-center justify-center bg-gray-800">
|
||||
<div v-if="isLoading" class="flex-1 flex items-center justify-center bg-gray-950">
|
||||
<div class="text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-gray-600 border-t-blue-400 mb-4"></div>
|
||||
<div class="spinner h-12 w-12 mb-4"></div>
|
||||
<p class="text-gray-400">Chargement du document...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,9 +70,9 @@
|
||||
<!-- Corps : Split view PDF + Données -->
|
||||
<div v-else class="flex-1 flex overflow-hidden">
|
||||
<!-- Gauche : PDF Preview -->
|
||||
<div class="w-1/2 border-r border-gray-700 flex flex-col">
|
||||
<div class="flex-shrink-0 px-4 py-2 bg-gray-700/50 border-b border-gray-700">
|
||||
<span class="text-sm text-gray-300">Aperçu PDF</span>
|
||||
<div class="w-2/5 border-r border-gray-700 flex flex-col">
|
||||
<div class="flex-shrink-0 px-4 py-2 bg-gray-900 border-b border-gray-700">
|
||||
<span class="card-title">Aperçu PDF</span>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<PdfPreview
|
||||
@@ -79,7 +81,7 @@
|
||||
:file-name="documentData?.source_file || 'document.pdf'"
|
||||
class="h-full"
|
||||
/>
|
||||
<div v-else class="h-full flex items-center justify-center bg-gray-800 text-gray-500">
|
||||
<div v-else class="h-full flex items-center justify-center bg-gray-950 text-gray-500">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-16 w-16 text-gray-600 mb-4" 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" />
|
||||
@@ -92,9 +94,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Droite : Édition ou Tagging -->
|
||||
<div class="w-1/2 flex flex-col bg-gray-50">
|
||||
<div class="w-3/5 flex flex-col bg-gray-950">
|
||||
<!-- Bandeau erreur de re-extraction -->
|
||||
<div v-if="reExtractError" class="flex-shrink-0 px-4 py-2 bg-red-500/10 border-b border-red-500/30 text-sm text-red-600">
|
||||
<div v-if="reExtractError" class="flex-shrink-0 px-4 py-2 bg-red-500/10 border-b border-red-500/30 text-sm text-red-400">
|
||||
{{ reExtractError }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left panel: Upload or PDF Preview -->
|
||||
<div class="w-1/2 flex flex-col border-r border-gray-700">
|
||||
<div class="w-2/5 flex flex-col border-r border-gray-700">
|
||||
<!-- Upload zone when no file -->
|
||||
<div v-if="!pdfFile" class="flex-1 flex items-center justify-center p-8 bg-gray-800">
|
||||
<div v-if="!pdfFile" class="flex-1 flex items-center justify-center p-8 bg-gray-950">
|
||||
<div class="max-w-md w-full">
|
||||
<div
|
||||
class="border-2 border-dashed border-gray-600 rounded-lg p-8 text-center hover:border-blue-500 transition-colors cursor-pointer bg-gray-900/50"
|
||||
class="border-2 border-dashed border-gray-700 rounded-lg p-8 text-center hover:border-blue-500 transition-colors cursor-pointer bg-gray-900/50"
|
||||
@click="triggerFileInput"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@@ -39,22 +39,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Right panel: JSON Viewer or Tagging Step -->
|
||||
<div class="w-1/2 flex flex-col bg-gray-50">
|
||||
<div class="w-3/5 flex flex-col bg-gray-950">
|
||||
<!-- Extract button bar -->
|
||||
<div v-if="pdfFile && !extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-white border-b border-gray-200">
|
||||
<div v-if="pdfFile && !extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-b border-gray-700">
|
||||
<button
|
||||
@click="extractCurrentFile"
|
||||
class="w-full px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
class="btn btn-primary w-full py-2.5"
|
||||
>
|
||||
Extraire les donnees du PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Continue to tagging button (when data is extracted) -->
|
||||
<div v-if="extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-white border-b border-gray-200">
|
||||
<div v-if="extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-b border-gray-700">
|
||||
<!-- Duplicate warning -->
|
||||
<div v-if="saveMessage && saveMessage.type === 'duplicate'" class="mb-3">
|
||||
<div class="px-4 py-3 rounded-lg text-sm bg-yellow-100 text-yellow-800 border border-yellow-200">
|
||||
<div class="px-4 py-3 rounded-lg text-sm bg-amber-500/10 text-amber-400 border border-amber-500/30">
|
||||
<div class="font-medium flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<button
|
||||
@click="goToTagging"
|
||||
class="w-full px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
class="btn btn-primary w-full py-2.5"
|
||||
>
|
||||
Continuer vers le tagging
|
||||
</button>
|
||||
@@ -84,13 +84,13 @@
|
||||
/>
|
||||
|
||||
<!-- Save feedback message -->
|
||||
<div v-if="saveMessage && showTagging" class="flex-shrink-0 p-4 bg-white border-t border-gray-200">
|
||||
<div v-if="saveMessage && showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-t border-gray-700">
|
||||
<div
|
||||
:class="[
|
||||
'px-4 py-3 rounded-lg text-sm',
|
||||
saveMessage.type === 'success' ? 'bg-green-100 text-green-800 border border-green-200' : '',
|
||||
saveMessage.type === 'duplicate' ? 'bg-yellow-100 text-yellow-800 border border-yellow-200' : '',
|
||||
saveMessage.type === 'error' ? 'bg-red-100 text-red-800 border border-red-200' : ''
|
||||
'px-4 py-3 rounded-lg text-sm border',
|
||||
saveMessage.type === 'success' ? 'bg-green-500/10 text-green-400 border-green-500/30' : '',
|
||||
saveMessage.type === 'duplicate' ? 'bg-amber-500/10 text-amber-400 border-amber-500/30' : '',
|
||||
saveMessage.type === 'error' ? 'bg-red-500/10 text-red-400 border-red-500/30' : ''
|
||||
]"
|
||||
>
|
||||
<div class="font-medium">{{ saveMessage.title }}</div>
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Placeholder when no file -->
|
||||
<div v-if="!pdfFile && !showTagging" class="flex-1 flex items-center justify-center text-gray-400">
|
||||
<div v-if="!pdfFile && !showTagging" class="flex-1 flex items-center justify-center text-gray-500">
|
||||
<p>Selectionnez un PDF pour commencer</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-800">
|
||||
<div class="max-w-6xl mx-auto space-y-6">
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- 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"
|
||||
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"
|
||||
@@ -54,14 +54,14 @@
|
||||
/>
|
||||
|
||||
<!-- 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>
|
||||
<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="inline-block animate-spin rounded-full h-6 w-6 border-2 border-gray-600 border-t-blue-400"></div>
|
||||
<div class="spinner h-6 w-6"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="documents.length === 0" class="p-8 text-center text-gray-500">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="max-w-5xl mx-auto space-y-6">
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Assistant IA</h1>
|
||||
<p class="text-gray-400 text-sm mt-1">Posez des questions sur vos données en langage naturel</p>
|
||||
<h1 class="page-title">Assistant IA</h1>
|
||||
<p class="page-subtitle">Posez des questions sur vos données en langage naturel</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
@@ -19,19 +19,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Formulaire -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
|
||||
<div class="card card-body">
|
||||
<div class="flex gap-3">
|
||||
<input
|
||||
v-model="question"
|
||||
type="text"
|
||||
placeholder="Posez votre question..."
|
||||
class="flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
|
||||
class="input"
|
||||
@keydown.enter="askQuestion"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<select
|
||||
v-model="chartType"
|
||||
class="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors"
|
||||
class="input w-auto"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option value="auto">Auto</option>
|
||||
@@ -43,7 +43,7 @@
|
||||
<button
|
||||
@click="askQuestion"
|
||||
:disabled="!question.trim() || loading"
|
||||
class="px-5 py-2.5 bg-blue-600 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700"
|
||||
class="btn btn-primary whitespace-nowrap"
|
||||
>
|
||||
{{ loading ? 'Analyse...' : 'Demander' }}
|
||||
</button>
|
||||
@@ -51,14 +51,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Exemples de questions -->
|
||||
<div v-if="!result && !loading && !error" class="bg-gray-800 rounded-lg border border-gray-700 p-4">
|
||||
<div v-if="!result && !loading && !error" class="card card-body">
|
||||
<p class="text-gray-400 text-sm mb-3">Exemples de questions :</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="example in examples"
|
||||
:key="example"
|
||||
@click="question = example; askQuestion()"
|
||||
class="px-3 py-1.5 text-sm bg-gray-900 border border-gray-700 rounded-lg text-gray-300 hover:border-blue-500 hover:text-white transition-colors"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
{{ example }}
|
||||
</button>
|
||||
@@ -66,8 +66,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Spinner -->
|
||||
<div v-if="loading" class="bg-gray-800 rounded-lg border border-gray-700 p-8 text-center">
|
||||
<div class="inline-block w-8 h-8 border-2 border-gray-600 border-t-blue-500 rounded-full animate-spin mb-3"></div>
|
||||
<div v-if="loading" class="card empty-state">
|
||||
<div class="spinner w-8 h-8 mb-3"></div>
|
||||
<p class="text-gray-400">{{ loadingMessage }}</p>
|
||||
</div>
|
||||
|
||||
@@ -77,23 +77,33 @@
|
||||
</div>
|
||||
|
||||
<!-- Résultat -->
|
||||
<div v-if="result" class="space-y-4">
|
||||
<div v-if="result" class="space-y-6">
|
||||
<!-- Réponse texte -->
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
|
||||
<h2 class="text-sm font-medium text-gray-400 mb-2">Réponse</h2>
|
||||
<p class="text-white whitespace-pre-wrap">{{ result.answer }}</p>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Réponse</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-white whitespace-pre-wrap">{{ result.answer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requête SQL -->
|
||||
<div v-if="result.sql_query" class="bg-gray-800 rounded-lg border border-gray-700 p-4">
|
||||
<h2 class="text-sm font-medium text-gray-400 mb-2">Requête SQL exécutée</h2>
|
||||
<pre class="text-xs text-gray-300 font-mono bg-gray-900 rounded p-3 overflow-x-auto">{{ result.sql_query }}</pre>
|
||||
<div v-if="result.sql_query" class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Requête SQL exécutée</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="text-xs text-gray-300 font-mono bg-gray-950 border border-gray-700 rounded p-3 overflow-x-auto">{{ result.sql_query }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Graphique -->
|
||||
<div v-if="result.chart_config && chartComponent" class="bg-gray-800 rounded-lg border border-gray-700 p-4">
|
||||
<h2 class="text-sm font-medium text-gray-400 mb-3">Graphique</h2>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<div v-if="result.chart_config && chartComponent" class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Graphique</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<component
|
||||
:is="chartComponent"
|
||||
:data="result.chart_config.data"
|
||||
@@ -103,34 +113,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Tableau de données -->
|
||||
<div v-if="result.data && result.data.columns && result.data.rows && result.data.rows.length > 0" class="bg-gray-800 rounded-lg border border-gray-700 p-4">
|
||||
<h2 class="text-sm font-medium text-gray-400 mb-3">
|
||||
Données ({{ result.data.row_count }} ligne{{ result.data.row_count > 1 ? 's' : '' }})
|
||||
</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<div v-if="result.data && result.data.columns && result.data.rows && result.data.rows.length > 0" class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">
|
||||
Données ({{ result.data.row_count }} ligne{{ result.data.row_count > 1 ? 's' : '' }})
|
||||
</h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700">
|
||||
<th
|
||||
v-for="col in result.data.columns"
|
||||
:key="col"
|
||||
class="text-left px-3 py-2 text-gray-400 font-medium"
|
||||
>
|
||||
{{ col }}
|
||||
</th>
|
||||
<tr>
|
||||
<th v-for="col in result.data.columns" :key="col">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, idx) in result.data.rows"
|
||||
:key="idx"
|
||||
class="border-b border-gray-700/50 hover:bg-gray-700/30"
|
||||
>
|
||||
<td
|
||||
v-for="(cell, cidx) in row"
|
||||
:key="cidx"
|
||||
class="px-3 py-2 text-gray-300"
|
||||
>
|
||||
<tr v-for="(row, idx) in result.data.rows" :key="idx">
|
||||
<td v-for="(cell, cidx) in row" :key="cidx">
|
||||
{{ formatCell(cell) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
<div class="page">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white">Dashboard Revenus Locatifs</h1>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
<h1 class="page-title">Dashboard Revenus Locatifs</h1>
|
||||
<p class="page-subtitle">
|
||||
Suivi et analyse des loyers, reglements et impayes
|
||||
</p>
|
||||
</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
<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"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<span v-if="isLoading">Chargement...</span>
|
||||
<span v-else>Actualiser</span>
|
||||
|
||||
@@ -2,22 +2,204 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Custom scrollbar for JSON viewer */
|
||||
/*
|
||||
* Design system : une seule palette sombre pour toute l'application.
|
||||
*
|
||||
* gray-950 fond de page gray-900 surfaces (cartes, header)
|
||||
* gray-800 sous-surfaces gray-700 bordures
|
||||
* white / gray-400 / gray-500 textes principal / secondaire / tertiaire
|
||||
* blue-600 accent
|
||||
*/
|
||||
@layer components {
|
||||
|
||||
/* --- Structure de page ------------------------------------------------ */
|
||||
|
||||
/* Conteneur racine d'une page : pleine largeur, marge laterale fixe. */
|
||||
.page {
|
||||
@apply flex-1 overflow-auto bg-gray-950 px-6 py-6;
|
||||
}
|
||||
|
||||
/* Page sans defilement propre (split view type edition). */
|
||||
.page-fixed {
|
||||
@apply flex-1 flex flex-col overflow-hidden bg-gray-950;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
@apply space-y-6;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@apply flex items-start justify-between gap-4;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
@apply text-xl font-semibold text-white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
@apply text-sm text-gray-400 mt-1;
|
||||
}
|
||||
|
||||
/* --- Cartes ----------------------------------------------------------- */
|
||||
|
||||
.card {
|
||||
@apply bg-gray-900 border border-gray-700 rounded-lg overflow-hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@apply px-4 py-3 border-b border-gray-700 flex items-center justify-between gap-3;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-sm font-medium text-gray-300;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
/* Bloc secondaire a l'interieur d'une carte. */
|
||||
.subcard {
|
||||
@apply bg-gray-800 border border-gray-700 rounded-lg;
|
||||
}
|
||||
|
||||
/* --- Boutons ---------------------------------------------------------- */
|
||||
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 px-3 py-1.5 text-sm rounded-lg
|
||||
transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-blue-600 text-white hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-gray-800 text-gray-300 border border-gray-700 hover:bg-gray-700 hover:text-white;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply text-gray-400 hover:text-white hover:bg-gray-800;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
@apply bg-amber-600 text-white hover:bg-amber-700;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply bg-red-600 text-white hover:bg-red-700;
|
||||
}
|
||||
|
||||
/* Variante compacte pour les actions dans les listes et tableaux. */
|
||||
.btn-sm {
|
||||
@apply px-2 py-1 text-xs gap-1.5;
|
||||
}
|
||||
|
||||
/* --- Formulaires ------------------------------------------------------ */
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-300 mb-1;
|
||||
}
|
||||
|
||||
/* [color-scheme:dark] : les widgets natifs (calendrier, spinners) suivent le theme. */
|
||||
.input {
|
||||
@apply w-full bg-gray-950 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white
|
||||
placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors
|
||||
[color-scheme:dark];
|
||||
}
|
||||
|
||||
select.input {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
@apply text-xs text-gray-500 mt-1;
|
||||
}
|
||||
|
||||
/* --- Tableaux --------------------------------------------------------- */
|
||||
|
||||
/* Enveloppe a poser autour d'un <table> pour le defilement horizontal. */
|
||||
.table-wrap {
|
||||
@apply overflow-x-auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
@apply w-full text-sm;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
@apply bg-gray-800 border-b border-gray-700;
|
||||
}
|
||||
|
||||
.table th {
|
||||
@apply px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider whitespace-nowrap;
|
||||
}
|
||||
|
||||
.table tbody {
|
||||
@apply divide-y divide-gray-800;
|
||||
}
|
||||
|
||||
.table td {
|
||||
@apply px-4 py-3 text-gray-300;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
@apply hover:bg-gray-800/50 transition-colors;
|
||||
}
|
||||
|
||||
/* --- Badges ----------------------------------------------------------- */
|
||||
|
||||
.badge {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium;
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
@apply bg-gray-700 text-gray-300;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply bg-blue-500/15 text-blue-400;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
@apply bg-green-500/15 text-green-400;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply bg-amber-500/15 text-amber-400;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
@apply bg-red-500/15 text-red-400;
|
||||
}
|
||||
|
||||
/* --- Etats ------------------------------------------------------------ */
|
||||
|
||||
.empty-state {
|
||||
@apply p-12 text-center text-gray-500;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
@apply inline-block animate-spin rounded-full border-2 border-gray-700 border-t-blue-400;
|
||||
}
|
||||
}
|
||||
|
||||
/* Barres de defilement accordees au theme sombre. */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
background: #111827; /* gray-900 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
background: #374151; /* gray-700 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
background: #4b5563; /* gray-600 */
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..utils.amounts import parse_amount
|
||||
from ..utils.lots import (
|
||||
LOT_NUMERO_INCONNU,
|
||||
normalize_extraction_lots,
|
||||
normalize_lot_numero,
|
||||
)
|
||||
from . import storage
|
||||
from .models import Depense, Document, Immeuble, Locataire, Lot, Revenu, Tag
|
||||
|
||||
@@ -55,7 +60,12 @@ class DatabaseService:
|
||||
def get_or_create_lot(
|
||||
self, immeuble_id: int, numero: str, lot_type: str = None
|
||||
) -> Lot:
|
||||
"""Get existing lot or create new one."""
|
||||
"""Get existing lot or create new one.
|
||||
|
||||
Le numero est normalise sur 2 chiffres pour qu'un meme lot saisi "6",
|
||||
"06" ou "0006" ne soit pas duplique.
|
||||
"""
|
||||
numero = normalize_lot_numero(numero) or LOT_NUMERO_INCONNU
|
||||
stmt = select(Lot).where(Lot.immeuble_id == immeuble_id, Lot.numero == numero)
|
||||
lot = self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
@@ -134,6 +144,9 @@ class DatabaseService:
|
||||
Raises:
|
||||
DuplicateDocumentError: If document already exists and overwrite=False
|
||||
"""
|
||||
# Uniformiser les numéros de lot avant toute persistance (JSON + tables)
|
||||
normalize_extraction_lots(data)
|
||||
|
||||
metadata = data.get("metadata", {})
|
||||
doc_info = metadata.get("document", {})
|
||||
immeuble_info = metadata.get("immeuble", {})
|
||||
@@ -247,7 +260,7 @@ class DatabaseService:
|
||||
# Get or create lot
|
||||
lot = self.get_or_create_lot(
|
||||
immeuble_id=immeuble.id,
|
||||
numero=lot_info.get("numero", "0000"),
|
||||
numero=lot_info.get("numero") or LOT_NUMERO_INCONNU,
|
||||
lot_type=lot_info.get("type"),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
|
||||
from ..utils.amounts import extract_amounts_from_line
|
||||
from ..utils.dates import parse_french_date
|
||||
from ..utils.lots import normalize_lot_numero
|
||||
|
||||
|
||||
def _preprocess_locataires_text(text: str) -> str:
|
||||
@@ -84,7 +85,7 @@ def extract_situation_locataires(text: str) -> list[dict]:
|
||||
|
||||
Returns:
|
||||
Liste des situations par lot, chacune contenant:
|
||||
- lot: numéro et type
|
||||
- lot: numéro (2 chiffres) et type
|
||||
- locataire: nom
|
||||
- lignes: détail des loyers, charges, etc.
|
||||
- totaux: sommes par catégorie
|
||||
@@ -112,14 +113,14 @@ def extract_situation_locataires(text: str) -> list[dict]:
|
||||
|
||||
# Nouveau lot (peut avoir les données de loyer sur la même ligne)
|
||||
lot_match = re.match(
|
||||
r"Lot\s+(\d{4})\s+(Loc\.\s*Commercial|Appartement\s+T\d|Studio|Garage|Cave|Parking)",
|
||||
r"Lot\s+(\d{1,4})\s+(Loc\.\s*Commercial|Appartement\s+T\d|Studio|Garage|Cave|Parking)",
|
||||
line_stripped,
|
||||
)
|
||||
if lot_match:
|
||||
if current_lot:
|
||||
situations.append(current_lot)
|
||||
|
||||
lot_num = lot_match.group(1)
|
||||
lot_num = normalize_lot_numero(lot_match.group(1))
|
||||
lot_type = lot_match.group(2)
|
||||
|
||||
current_lot = {
|
||||
|
||||
@@ -26,12 +26,13 @@ import pdfplumber
|
||||
|
||||
from ..utils.amounts import extract_amounts_from_line
|
||||
from ..utils.dates import parse_french_date
|
||||
from ..utils.lots import normalize_lot_numero
|
||||
|
||||
# Tolérance verticale (points PDF) pour regrouper les mots d'une même ligne visuelle.
|
||||
_Y_TOL = 3.0
|
||||
|
||||
_LOT_RE = re.compile(
|
||||
r"^Lot\s+(\d{4})\s+"
|
||||
r"^Lot\s+(\d{1,4})\s+"
|
||||
r"(Loc\.\s*Commercial|Appartement\s+T\d|Studio|Garage|Cave|Parking)"
|
||||
)
|
||||
_PERIODE_RE = re.compile(r"Du\s+\d{2}\.\d{2}\.\d{2}\s+Au\s+(\d{2}\.\d{2}\.\d{2})")
|
||||
@@ -149,7 +150,7 @@ def _rows_from_page(page) -> list[dict]:
|
||||
|
||||
def _new_lot(numero: str, lot_type: str) -> dict:
|
||||
return {
|
||||
"lot": {"numero": numero, "type": lot_type},
|
||||
"lot": {"numero": normalize_lot_numero(numero), "type": lot_type},
|
||||
"locataire": {"nom": ""},
|
||||
"lignes": [],
|
||||
"totaux": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import re
|
||||
|
||||
from ..utils.amounts import parse_amount
|
||||
from ..utils.lots import normalize_lot_numero
|
||||
|
||||
|
||||
def _extract_lot_code_from_description(description: str) -> str | None:
|
||||
@@ -10,18 +11,18 @@ def _extract_lot_code_from_description(description: str) -> str | None:
|
||||
|
||||
Les codes lots suivent le format: {Lettre}{Numéro} où:
|
||||
- La lettre identifie l'immeuble (M=Marietton, S=Servient, B=Bloch, etc.)
|
||||
- Le numéro correspond au lot (ex: 06 -> lot 0006)
|
||||
- Le numéro correspond au lot (ex: 06 -> lot 06)
|
||||
|
||||
Exemples:
|
||||
- "M06 - Commande moteur pompe" -> "0006"
|
||||
- "S05 - Mise en service" -> "0005"
|
||||
- "B01 - Plaques" -> "0001"
|
||||
- "M06 - Commande moteur pompe" -> "06"
|
||||
- "S05 - Mise en service" -> "05"
|
||||
- "B01 - Plaques" -> "01"
|
||||
|
||||
Args:
|
||||
description: Description de l'opération
|
||||
|
||||
Returns:
|
||||
Code lot au format 4 chiffres (ex: "0006") ou None si non trouvé
|
||||
Code lot au format 2 chiffres (ex: "06") ou None si non trouvé
|
||||
"""
|
||||
if not description:
|
||||
return None
|
||||
@@ -31,9 +32,7 @@ def _extract_lot_code_from_description(description: str) -> str | None:
|
||||
# code) et "S01 SOLDE ..." (code lot non suivi d'un tiret).
|
||||
match = re.search(r"\b[A-Z]\s*(\d{1,2})(?=[\s-]|$)", description)
|
||||
if match:
|
||||
lot_num = match.group(1)
|
||||
# Formater sur 4 chiffres (ex: "6" -> "0006", "12" -> "0012")
|
||||
return lot_num.zfill(4)
|
||||
return normalize_lot_numero(match.group(1))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ Tables et colonnes :
|
||||
3. **lots** — Lots (appartements, locaux commerciaux) au sein d'un immeuble
|
||||
- id (INTEGER, PK)
|
||||
- immeuble_id (INTEGER, FK → immeubles.id)
|
||||
- numero (VARCHAR(10)) — numéro du lot dans l'immeuble
|
||||
- numero (VARCHAR(10)) — numéro du lot dans l'immeuble, sur 2 chiffres ("01", "17")
|
||||
- type (VARCHAR(50)) — type de lot ("Appartement", "Loc. Commercial", etc.)
|
||||
- created_at (DATETIME)
|
||||
Contrainte : (immeuble_id, numero) est unique
|
||||
|
||||
79
src/plesna_gerance/utils/lots.py
Normal file
79
src/plesna_gerance/utils/lots.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Utilitaires pour la normalisation des numéros de lot."""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
#: Largeur canonique d'un numéro de lot (ex: "06", "17").
|
||||
LOT_NUMERO_WIDTH = 2
|
||||
|
||||
#: Numéro utilisé quand aucun lot n'est identifiable.
|
||||
LOT_NUMERO_INCONNU = "00"
|
||||
|
||||
|
||||
def normalize_lot_numero(value: str | int | None) -> str | None:
|
||||
"""Normalise un numéro de lot sur exactement 2 chiffres.
|
||||
|
||||
Les PDF et les saisies manuelles utilisent des formats variés ("6", "06",
|
||||
"0006"). Tous doivent aboutir au même numéro canonique, sinon un même lot
|
||||
est dupliqué en base.
|
||||
|
||||
Gère:
|
||||
- "0006" -> "06"
|
||||
- "6" -> "06"
|
||||
- "06" -> "06"
|
||||
- "0020" -> "20"
|
||||
- "0000" -> "00"
|
||||
|
||||
Les numéros comportant plus de 2 chiffres significatifs (ex: "0123") sont
|
||||
conservés sans leurs zéros de tête ("123") : les tronquer perdrait de
|
||||
l'information.
|
||||
|
||||
Args:
|
||||
value: Numéro de lot dans un format quelconque
|
||||
|
||||
Returns:
|
||||
Numéro sur 2 chiffres, ou None si aucun chiffre n'est présent
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
digits = re.sub(r"\D", "", str(value))
|
||||
if not digits:
|
||||
return None
|
||||
|
||||
significant = digits.lstrip("0")
|
||||
if not significant:
|
||||
return LOT_NUMERO_INCONNU
|
||||
|
||||
return significant.zfill(LOT_NUMERO_WIDTH)
|
||||
|
||||
|
||||
def normalize_extraction_lots(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalise, sur place, tous les numéros de lot d'une extraction.
|
||||
|
||||
Couvre les deux emplacements où un numéro de lot apparaît:
|
||||
- ``situation_locataires[].lot.numero``
|
||||
- ``recapitulatif_operations[].lot_numero``
|
||||
|
||||
Utile pour les extractions éditées à la main via l'API, où le numéro saisi
|
||||
peut arriver sous n'importe quelle forme.
|
||||
|
||||
Args:
|
||||
data: Extraction (metadata, situation_locataires, recapitulatif_operations)
|
||||
|
||||
Returns:
|
||||
Le même dict, numéros de lot normalisés
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
for situation in data.get("situation_locataires") or []:
|
||||
lot = situation.get("lot")
|
||||
if isinstance(lot, dict) and lot.get("numero") is not None:
|
||||
lot["numero"] = normalize_lot_numero(lot["numero"])
|
||||
|
||||
for operation in data.get("recapitulatif_operations") or []:
|
||||
if isinstance(operation, dict) and operation.get("lot_numero") is not None:
|
||||
operation["lot_numero"] = normalize_lot_numero(operation["lot_numero"])
|
||||
|
||||
return data
|
||||
@@ -53,7 +53,7 @@ def sample_data():
|
||||
},
|
||||
"situation_locataires": [
|
||||
{
|
||||
"lot": {"numero": "001", "type": "Appartement"},
|
||||
"lot": {"numero": "01", "type": "Appartement"},
|
||||
"locataire": {"nom": "DUPONT"},
|
||||
"lignes": [
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_save_document_creates_full_graph(db_session, sample_data):
|
||||
|
||||
# Immeuble / lot / locataire créés
|
||||
assert db_session.query(Immeuble).filter_by(code="IMM1").count() == 1
|
||||
assert db_session.query(Lot).filter_by(numero="001").count() == 1
|
||||
assert db_session.query(Lot).filter_by(numero="01").count() == 1
|
||||
assert db_session.query(Locataire).filter_by(nom="DUPONT").count() == 1
|
||||
|
||||
# Revenu et dépense rattachés
|
||||
|
||||
121
tests/test_lots.py
Normal file
121
tests/test_lots.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Tests de la normalisation des numéros de lot."""
|
||||
|
||||
import pytest
|
||||
|
||||
from plesna_gerance.parsers.operations import _extract_lot_code_from_description
|
||||
from plesna_gerance.utils.lots import normalize_extraction_lots, normalize_lot_numero
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
("0006", "06"),
|
||||
("06", "06"),
|
||||
("6", "06"),
|
||||
(6, "06"),
|
||||
("0020", "20"),
|
||||
("20", "20"),
|
||||
("Lot 6", "06"),
|
||||
(" 7 ", "07"),
|
||||
("0000", "00"),
|
||||
("0", "00"),
|
||||
# Plus de 2 chiffres significatifs : on ne tronque pas
|
||||
("0123", "123"),
|
||||
# Rien d'exploitable
|
||||
(None, None),
|
||||
("", None),
|
||||
("abc", None),
|
||||
],
|
||||
)
|
||||
def test_normalize_lot_numero(raw, expected):
|
||||
assert normalize_lot_numero(raw) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"description,expected",
|
||||
[
|
||||
("M06 - Commande moteur pompe", "06"),
|
||||
("S05 - Mise en service", "05"),
|
||||
("B01 - Plaques", "01"),
|
||||
("S10 - Reparation", "10"),
|
||||
("S 17 - Espace dans le code", "17"),
|
||||
("S01 SOLDE LOCATAIRE", "01"),
|
||||
("Nettoyage immeuble", None),
|
||||
("", None),
|
||||
],
|
||||
)
|
||||
def test_extract_lot_code_from_description(description, expected):
|
||||
assert _extract_lot_code_from_description(description) == expected
|
||||
|
||||
|
||||
def test_normalize_extraction_lots():
|
||||
data = {
|
||||
"situation_locataires": [
|
||||
{"lot": {"numero": "0006", "type": "Appartement T3"}},
|
||||
{"lot": {"numero": "7", "type": "Studio"}},
|
||||
{"lot": {"numero": None, "type": "Garage"}},
|
||||
],
|
||||
"recapitulatif_operations": [
|
||||
{"lot_numero": "0013"},
|
||||
{"lot_numero": "1"},
|
||||
{"lot_numero": None},
|
||||
],
|
||||
}
|
||||
|
||||
normalize_extraction_lots(data)
|
||||
|
||||
assert [s["lot"]["numero"] for s in data["situation_locataires"]] == [
|
||||
"06",
|
||||
"07",
|
||||
None,
|
||||
]
|
||||
assert [o["lot_numero"] for o in data["recapitulatif_operations"]] == [
|
||||
"13",
|
||||
"01",
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
def test_normalize_extraction_lots_tolerates_missing_sections():
|
||||
# Une extraction partielle ne doit pas lever
|
||||
assert normalize_extraction_lots({}) == {}
|
||||
assert normalize_extraction_lots({"situation_locataires": None}) == {
|
||||
"situation_locataires": None
|
||||
}
|
||||
|
||||
|
||||
def test_get_or_create_lot_deduplicates_formats(db_session):
|
||||
""""0006", "06" et "6" désignent le même lot : un seul enregistrement."""
|
||||
from plesna_gerance.database.models import Immeuble, Lot
|
||||
from plesna_gerance.database.service import DatabaseService
|
||||
|
||||
service = DatabaseService(db_session)
|
||||
immeuble = service.get_or_create_immeuble(code="IMM1")
|
||||
|
||||
lots = [
|
||||
service.get_or_create_lot(immeuble_id=immeuble.id, numero=numero)
|
||||
for numero in ("0006", "06", "6")
|
||||
]
|
||||
|
||||
assert {lot.id for lot in lots} == {lots[0].id}
|
||||
assert lots[0].numero == "06"
|
||||
assert db_session.query(Lot).filter_by(immeuble_id=immeuble.id).count() == 1
|
||||
assert db_session.query(Immeuble).count() == 1
|
||||
|
||||
|
||||
def test_save_document_normalizes_lot_numero(db_session, sample_data):
|
||||
from plesna_gerance.database.models import Lot
|
||||
from plesna_gerance.database.service import DatabaseService
|
||||
|
||||
sample_data["situation_locataires"][0]["lot"]["numero"] = "0001"
|
||||
sample_data["recapitulatif_operations"][0]["lot_numero"] = "1"
|
||||
|
||||
service = DatabaseService(db_session)
|
||||
doc = service.save_document(data=sample_data)
|
||||
|
||||
# Le lot du locataire et celui de l'opération sont le même
|
||||
assert db_session.query(Lot).count() == 1
|
||||
assert db_session.query(Lot).first().numero == "01"
|
||||
# Le JSON persisté est normalisé lui aussi
|
||||
assert '"numero": "01"' in doc.json_data
|
||||
assert '"lot_numero": "01"' in doc.json_data
|
||||
Reference in New Issue
Block a user