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:
184
frontend/src/components/EditableField.vue
Normal file
184
frontend/src/components/EditableField.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div
|
||||
class="editable-field inline-flex items-center gap-1 group"
|
||||
:class="{ 'w-full': fullWidth }"
|
||||
>
|
||||
<!-- Mode lecture -->
|
||||
<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="[
|
||||
displayClass,
|
||||
{ 'text-gray-400 italic': isEmpty }
|
||||
]"
|
||||
:title="'Cliquer pour modifier'"
|
||||
>
|
||||
{{ displayValue }}
|
||||
</span>
|
||||
|
||||
<!-- Mode edition -->
|
||||
<input
|
||||
v-else
|
||||
ref="inputRef"
|
||||
v-model="localValue"
|
||||
:type="inputType"
|
||||
:step="type === 'currency' || type === 'number' ? '0.01' : undefined"
|
||||
:placeholder="placeholder"
|
||||
@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="[
|
||||
inputClass,
|
||||
{ 'w-full': fullWidth, 'w-24': !fullWidth && (type === 'currency' || type === 'number'), 'w-32': !fullWidth && type === 'date' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Indicateur d'erreur -->
|
||||
<span
|
||||
v-if="error"
|
||||
class="text-red-500 text-xs"
|
||||
:title="error"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
validator: (v) => ['text', 'number', 'date', 'currency'].includes(v)
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '-'
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
displayClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const isEditing = ref(false)
|
||||
const localValue = ref('')
|
||||
const inputRef = ref(null)
|
||||
const error = ref(null)
|
||||
|
||||
// Computed pour l'affichage
|
||||
const isEmpty = computed(() => {
|
||||
return props.modelValue === null || props.modelValue === undefined || props.modelValue === ''
|
||||
})
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (isEmpty.value) return props.placeholder
|
||||
|
||||
if (props.type === 'currency') {
|
||||
const num = parseFloat(props.modelValue)
|
||||
if (isNaN(num)) return props.modelValue
|
||||
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(num)
|
||||
}
|
||||
|
||||
return props.modelValue
|
||||
})
|
||||
|
||||
const inputType = computed(() => {
|
||||
if (props.type === 'currency' || props.type === 'number') return 'number'
|
||||
if (props.type === 'date') return 'date'
|
||||
return 'text'
|
||||
})
|
||||
|
||||
// Demarrer l'edition
|
||||
function startEditing() {
|
||||
// Convertir la valeur pour l'input
|
||||
if (props.type === 'currency' || props.type === 'number') {
|
||||
localValue.value = props.modelValue !== null && props.modelValue !== undefined ? props.modelValue : ''
|
||||
} else {
|
||||
localValue.value = props.modelValue || ''
|
||||
}
|
||||
|
||||
isEditing.value = true
|
||||
error.value = null
|
||||
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus()
|
||||
inputRef.value?.select()
|
||||
})
|
||||
}
|
||||
|
||||
// Valider la valeur
|
||||
function validate(value) {
|
||||
if (props.type === 'number' || props.type === 'currency') {
|
||||
if (value === '' || value === null) return { valid: true, value: null }
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return { valid: false, error: 'Nombre invalide' }
|
||||
return { valid: true, value: num }
|
||||
}
|
||||
|
||||
if (props.type === 'date') {
|
||||
if (value === '' || value === null) return { valid: true, value: null }
|
||||
// Format YYYY-MM-DD
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
|
||||
if (!dateRegex.test(value)) return { valid: false, error: 'Format: YYYY-MM-DD' }
|
||||
return { valid: true, value }
|
||||
}
|
||||
|
||||
return { valid: true, value: value || null }
|
||||
}
|
||||
|
||||
// Sauvegarder et fermer
|
||||
function saveAndClose() {
|
||||
const validation = validate(localValue.value)
|
||||
|
||||
if (!validation.valid) {
|
||||
error.value = validation.error
|
||||
return
|
||||
}
|
||||
|
||||
isEditing.value = false
|
||||
error.value = null
|
||||
|
||||
// Emettre seulement si la valeur a change
|
||||
if (validation.value !== props.modelValue) {
|
||||
emit('update:modelValue', validation.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Annuler l'edition
|
||||
function cancelEditing() {
|
||||
isEditing.value = false
|
||||
error.value = null
|
||||
localValue.value = props.modelValue || ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editable-field input[type="number"]::-webkit-inner-spin-button,
|
||||
.editable-field input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.editable-field input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user