Files
pdf_oralia_vibe/frontend/src/components/EditableField.vue
Bertrand Benjamin cae8d5b963 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
2026-01-19 22:02:28 +01:00

185 lines
4.6 KiB
Vue

<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>