fix(constructeurs): improve search filtering and duplicate prevention

Switch ConstructeurSelect to client-side filtering instead of debounced
API calls. Add duplicate name check before creating a new constructeur
in both ConstructeurSelect and the constructeurs page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-02 14:05:54 +01:00
parent 256039264e
commit e22463874c
2 changed files with 40 additions and 63 deletions

View File

@@ -20,16 +20,16 @@
</button>
<div
v-if="openDropdown"
class="absolute z-20 mt-1 w-full max-h-48 overflow-y-auto bg-base-100 border border-base-200 rounded-box shadow-lg flex flex-col"
class="absolute z-20 mt-1 w-full max-h-60 overflow-y-auto bg-base-100 border border-base-200 rounded-box shadow-lg flex flex-col"
>
<div
v-if="options.length === 0"
v-if="filteredOptions.length === 0"
class="px-3 py-2 text-xs text-gray-500"
>
Aucun fournisseur trouvé
</div>
<button
v-for="option in options"
v-for="option in filteredOptions"
:key="option.id"
type="button"
class="w-full text-left px-3 py-2 hover:bg-base-200 focus:bg-base-200 focus:outline-none"
@@ -164,8 +164,7 @@ const openCreateModal = ref(false)
const creating = ref(false)
const options = ref<ConstructeurSummary[]>([])
const selectedIds = ref<string[]>([])
let searchTimeout: ReturnType<typeof setTimeout> | null = null
let lastSearchTerm = ''
const uniqueOptions = (items: ConstructeurSummary[] = []) => {
const seen = new Map<string, ConstructeurSummary>()
@@ -182,32 +181,22 @@ const normalizedInitialOptions = computed(() =>
)
const applyOptions = (items: ConstructeurSummary[] = []) => {
const normalized = uniqueOptions([
options.value = uniqueOptions([
...normalizedInitialOptions.value,
...items,
])
const limited = normalized.slice(0, 10)
selectedIds.value.forEach((id) => {
if (!limited.some((item) => item.id === id)) {
const match =
normalized.find((item) => item.id === id) ||
constructeurs.value.find((item) => item.id === id)
if (match) {
if (limited.length >= 10) {
limited.pop()
}
limited.unshift(match)
}
}
})
options.value = uniqueOptions([
...normalizedInitialOptions.value,
...limited,
])
}
const filteredOptions = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
if (!term) return options.value
return options.value.filter((option) =>
(option.name ?? '').toLowerCase().includes(term)
|| (option.email && option.email.toLowerCase().includes(term))
|| (option.phone && option.phone.toLowerCase().includes(term))
)
})
const createForm = ref({
name: '',
email: '',
@@ -257,46 +246,20 @@ const extractDataArray = (data: unknown): ConstructeurSummary[] => {
}
const ensureOptionsLoaded = async (force = false) => {
if (!force && !searchTerm.value && constructeurs.value.length) {
if (!force && constructeurs.value.length) {
applyOptions(constructeurs.value as ConstructeurSummary[])
return
}
if (!force && searchTerm.value === lastSearchTerm && options.value.length) {
return
}
if (options.value.length && !force) {
return
}
const result = await searchConstructeurs(searchTerm.value)
const result = await searchConstructeurs('')
if (result.success) {
applyOptions(extractDataArray(result.data))
lastSearchTerm = searchTerm.value
}
}
const onSearch = () => {
openDropdown.value = true
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(async () => {
if (!searchTerm.value && constructeurs.value.length) {
applyOptions(constructeurs.value as ConstructeurSummary[])
lastSearchTerm = ''
return
}
if (searchTerm.value === lastSearchTerm) {
return
}
const result = await searchConstructeurs(searchTerm.value)
if (result.success) {
applyOptions(extractDataArray(result.data))
lastSearchTerm = searchTerm.value
}
}, 250)
ensureOptionsLoaded()
}
const toggleOption = (option: ConstructeurSummary) => {
@@ -319,9 +282,19 @@ const closeCreateModal = () => {
}
const handleCreate = async () => {
const trimmedName = createForm.value.name.trim()
const duplicate = options.value.find(
(o) => (o.name ?? '').toLowerCase() === trimmedName.toLowerCase(),
)
if (duplicate) {
emitSelection([...selectedIds.value, duplicate.id])
closeCreateModal()
return
}
creating.value = true
const payload: { name: string; email?: string; phone?: string } = {
name: createForm.value.name,
name: trimmedName,
}
if (createForm.value.email) {
payload.email = createForm.value.email
@@ -383,9 +356,6 @@ watch(
constructeurs,
(list) => {
applyOptions((list as ConstructeurSummary[]) || [])
if (!searchTerm.value) {
lastSearchTerm = ''
}
},
{ immediate: true },
)
@@ -405,9 +375,6 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('click', clickHandler)
if (searchTimeout) {
clearTimeout(searchTimeout)
}
})
watch(

View File

@@ -195,8 +195,18 @@ const closeModal = () => {
}
const saveConstructeur = async () => {
const trimmedName = form.value.name.trim()
const duplicate = constructeurs.value.find(
(c) => c.name.toLowerCase() === trimmedName.toLowerCase()
&& c.id !== editingConstructeur.value?.id,
)
if (duplicate) {
showError(`Un fournisseur "${duplicate.name}" existe déjà.`)
return
}
saving.value = true
const payload = { ...form.value }
const payload = { ...form.value, name: trimmedName }
if (!payload.email) { delete payload.email }
if (!payload.phone) { delete payload.phone }
let result