- 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
185 lines
4.6 KiB
Vue
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>
|