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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user