feat: add inline editing for extracted data and autocomplete for tagging

- Add EditableField component for inline editing of any field
- Update DataRow, LocataireCard, OperationCard to support inline editing
- Add CRUD operations for locataires and operations (add/remove)
- Update JsonViewer to propagate changes via v-model
- Add TagAutocomplete component with search/filter functionality
- Replace select dropdown with autocomplete in TaggingStep
This commit is contained in:
2026-01-19 22:02:28 +01:00
parent aac4d194e3
commit cae8d5b963
8 changed files with 1041 additions and 129 deletions

View File

@@ -1,11 +1,11 @@
<template>
<div class="border border-gray-200 rounded-lg overflow-hidden">
<!-- Header -->
<button
@click="expanded = !expanded"
class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 transition-colors text-left"
>
<div class="flex items-center gap-2">
<div class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 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="{ 'rotate-90': expanded }"
@@ -19,11 +19,23 @@
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{{ operations.length }} operation{{ operations.length > 1 ? 's' : '' }}
</span>
</button>
<div class="flex items-center gap-2">
<div class="text-sm font-semibold text-gray-700">
{{ 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"
title="Ajouter une operation"
>
<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" />
</svg>
</button>
</div>
<div class="text-sm font-semibold text-gray-700">
{{ formatCurrency(totalDebit) }}
</div>
</button>
</div>
<!-- Content -->
<div v-show="expanded" class="border-t border-gray-100 bg-gray-50">
@@ -34,31 +46,124 @@
class="px-3 py-2 bg-white text-xs"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<div v-if="op.sous_categorie" class="text-gray-400 text-xs mb-0.5">
{{ op.sous_categorie }}
<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>
<EditableField
:modelValue="op.sous_categorie"
@update:modelValue="updateOperationField(idx, 'sous_categorie', $event)"
type="text"
placeholder="-"
displayClass="text-xs text-gray-500"
/>
</div>
<div v-if="op.fournisseur" class="font-medium text-gray-800 truncate">
{{ op.fournisseur }}
<!-- Fournisseur -->
<div class="flex items-center gap-2">
<span class="text-gray-400 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"
/>
</div>
<div class="text-gray-500 truncate">{{ op.description || '-' }}</div>
<div v-if="op.lot_numero || op.lot_concerne" class="text-blue-500 text-xs mt-0.5">
<span v-if="op.lot_numero" class="font-medium">Lot {{ op.lot_numero }}</span>
<span v-if="op.lot_numero && op.lot_concerne"> - </span>
<span v-if="op.lot_concerne">{{ op.lot_concerne }}</span>
<!-- Description -->
<div class="flex items-center gap-2">
<span class="text-gray-400 text-xs w-20">Description:</span>
<EditableField
:modelValue="op.description"
@update:modelValue="updateOperationField(idx, 'description', $event)"
type="text"
placeholder="-"
displayClass="text-gray-500"
:fullWidth="true"
/>
</div>
<!-- Lot -->
<div class="flex items-center gap-2">
<span class="text-gray-400 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"
/>
<EditableField
:modelValue="op.lot_concerne"
@update:modelValue="updateOperationField(idx, 'lot_concerne', $event)"
type="text"
placeholder="Concerne"
displayClass="text-blue-500 text-xs"
/>
</div>
</div>
<div class="text-right flex-shrink-0">
<div class="font-semibold text-gray-800">{{ formatCurrency(op.montants?.debit) }}</div>
<div v-if="op.montants?.tva" class="text-gray-400">
TVA: {{ formatCurrency(op.montants.tva) }}
</div>
<div v-if="op.montants?.locatif" class="text-green-600 text-xs">
Locatif: {{ formatCurrency(op.montants.locatif) }}
</div>
<div v-if="op.montants?.deductible" class="text-orange-600 text-xs">
Deductible: {{ formatCurrency(op.montants.deductible) }}
<div class="flex items-start gap-2">
<!-- 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>
<EditableField
:modelValue="op.montants?.debit"
@update:modelValue="updateOperationField(idx, 'montants.debit', $event)"
type="currency"
displayClass="font-semibold text-gray-800"
/>
</div>
<div class="flex items-center justify-end gap-1">
<span class="text-gray-400 text-xs">Credit:</span>
<EditableField
:modelValue="op.montants?.credit"
@update:modelValue="updateOperationField(idx, 'montants.credit', $event)"
type="currency"
displayClass="text-green-600"
/>
</div>
<div class="flex items-center justify-end gap-1">
<span class="text-gray-400 text-xs">TVA:</span>
<EditableField
:modelValue="op.montants?.tva"
@update:modelValue="updateOperationField(idx, 'montants.tva', $event)"
type="currency"
displayClass="text-gray-400"
/>
</div>
<div class="flex items-center justify-end gap-1">
<span class="text-gray-400 text-xs">Locatif:</span>
<EditableField
:modelValue="op.montants?.locatif"
@update:modelValue="updateOperationField(idx, 'montants.locatif', $event)"
type="currency"
displayClass="text-green-600 text-xs"
/>
</div>
<div class="flex items-center justify-end gap-1">
<span class="text-gray-400 text-xs">Deductible:</span>
<EditableField
:modelValue="op.montants?.deductible"
@update:modelValue="updateOperationField(idx, 'montants.deductible', $event)"
type="currency"
displayClass="text-orange-600 text-xs"
/>
</div>
</div>
<!-- Bouton supprimer -->
<button
@click="removeOperation(idx)"
class="p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
title="Supprimer cette operation"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
@@ -80,6 +185,7 @@
<script setup>
import { ref, computed } from 'vue'
import EditableField from './EditableField.vue'
const props = defineProps({
categorie: {
@@ -92,6 +198,8 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:operations'])
const expanded = ref(false)
const totalDebit = computed(() => {
@@ -104,7 +212,6 @@ const totalTva = computed(() => {
function formatCategorie(cat) {
if (!cat) return '-'
// Convertir DEPENSES_LOCATIVES -> Depenses locatives
return cat
.replace(/_/g, ' ')
.toLowerCase()
@@ -115,4 +222,60 @@ function formatCurrency(value) {
if (value === null || value === undefined) return '-'
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value)
}
// Helper pour setter une valeur nested
function setNestedValue(obj, path, value) {
const parts = path.split('.')
let current = obj
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {}
}
current = current[parts[i]]
}
current[parts[parts.length - 1]] = value
}
// Mettre a jour un champ d'une operation
function updateOperationField(opIndex, path, value) {
const updatedOperations = JSON.parse(JSON.stringify(props.operations))
if (path.includes('.')) {
setNestedValue(updatedOperations[opIndex], path, value)
} else {
updatedOperations[opIndex][path] = value
}
emit('update:operations', updatedOperations)
}
// Ajouter une nouvelle operation dans cette categorie
function addOperation() {
const updatedOperations = JSON.parse(JSON.stringify(props.operations))
updatedOperations.push({
categorie: props.categorie,
sous_categorie: null,
fournisseur: null,
description: null,
lot_concerne: null,
lot_numero: null,
montants: {
debit: 0,
credit: 0,
tva: 0,
locatif: 0,
deductible: 0
}
})
emit('update:operations', updatedOperations)
}
// Supprimer une operation
function removeOperation(opIndex) {
const updatedOperations = JSON.parse(JSON.stringify(props.operations))
updatedOperations.splice(opIndex, 1)
emit('update:operations', updatedOperations)
}
</script>