fix: recherche insensible à la casse et aux accents dans tous les filtres

- utils/search.ts : normalize() (NFD + minuscules) + matchesSearch()
- ShoppingPage filteredProducts : matchesSearch sur nom ET marque
- Backend searchProducts : ilike sur nom ET marque (or_)
- Notes FTS : déjà insensible nativement (plainto_tsquery)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 07:00:27 +02:00
co-authored by Claude Sonnet 4.6
parent a86c14b0b9
commit 377531d08e
3 changed files with 17 additions and 5 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response from fastapi.responses import Response
from sqlalchemy import select, text from sqlalchemy import select, text, or_
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -104,7 +104,7 @@ async def search_products(
): ):
stmt = select(Product).order_by(Product.frequency_score.desc(), Product.name) stmt = select(Product).order_by(Product.frequency_score.desc(), Product.name)
if q: if q:
stmt = stmt.where(Product.name.ilike(f"%{q}%")) stmt = stmt.where(or_(Product.name.ilike(f"%{q}%"), Product.brand.ilike(f"%{q}%")))
stmt = stmt.limit(limit) stmt = stmt.limit(limit)
result = await session.execute(stmt) result = await session.execute(stmt)
return result.scalars().all() return result.scalars().all()
+5 -3
View File
@@ -1,5 +1,6 @@
// frontend/src/pages/ShoppingPage.tsx // frontend/src/pages/ShoppingPage.tsx
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { matchesSearch } from '../utils/search'
import type { ShoppingListDetail, ShoppingList, Store, Product } from '../api/shopping' import type { ShoppingListDetail, ShoppingList, Store, Product } from '../api/shopping'
import { import {
fetchLists, createList, fetchListDetail, deleteList, fetchLists, createList, fetchListDetail, deleteList,
@@ -213,9 +214,10 @@ export default function ShoppingPage() {
const hasCurrentList = currentList !== null const hasCurrentList = currentList !== null
const pastLists = allLists.filter(l => l.status === 'done') const pastLists = allLists.filter(l => l.status === 'done')
const filteredProducts = products.filter( const filteredProducts = products.filter(p => {
p => !itemSearch.trim() || p.name.toLowerCase().includes(itemSearch.trim().toLowerCase()) const term = itemSearch.trim()
) return !term || matchesSearch(p.name, term) || matchesSearch(p.brand ?? '', term)
})
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', minHeight: '100%' }}>
+10
View File
@@ -0,0 +1,10 @@
/** Normalise une chaîne : minuscules + suppression des accents. */
export function normalize(s: string): string {
return s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
}
/** Vérifie si `haystack` contient `needle` (insensible casse/accents). */
export function matchesSearch(haystack: string, needle: string): boolean {
if (!needle) return true
return normalize(haystack).includes(normalize(needle))
}