using MaBibli.Shared.Entites; using MaBibli.Shared.Isbn; using MaBibli.Shared.Textes; namespace MaBibli.Shared.Catalogue; /// /// Filtrage, recherche et tri du catalogue. /// /// /// Écrit sur pour être exécuté par EF Core côté base et /// testable en mémoire sur une simple liste, sans base ni réseau. /// /// La recherche porte sur les colonnes normalisées (, /// ), pas sur les colonnes d'affichage : SQLite ne sait pas /// ignorer les accents, et sa collation NOCASE s'arrête à l'ASCII. Le terme cherché passe /// par la même normalisation que la valeur stockée — c'est ce qui fait que « emile » trouve /// « Émile ». /// /// public static class FiltreLivres { /// /// Personne dont on regarde le statut. Sert uniquement à /// : il ne restreint jamais la liste elle-même. /// public static IQueryable Appliquer( IQueryable source, CritereLivres criteres, string? utilisateur) { // AUCUN filtre sur AjoutePar : la bibliothèque est commune (CLAUDE.md). // Ce commentaire est là pour qu'une régression future saute aux yeux en relecture. if (criteres.Format is { } format) { source = source.Where(l => l.Format == format); } if (criteres.TypeDocument is { } type) { source = source.Where(l => l.TypeDocument == type); } if (criteres.Statut is { } statut) { // Le statut vit dans une table par personne : on interroge la ligne de l'appelant, // et lui seulement. Sans utilisateur identifié, personne n'a de statut : la sélection // est vide, ce qui vaut mieux que de montrer celui d'un autre. source = utilisateur is null ? source.Where(_ => false) : source.Where(l => l.Statuts.Any(s => s.Utilisateur == utilisateur && s.Statut == statut)); } if (criteres.AuteurId is { } auteurId) { source = source.Where(l => l.Auteurs.Any(la => la.AuteurId == auteurId)); } if (criteres.Prete is { } prete) { // Un prêt EN COURS est une ligne sans date de retour : l'historique clos ne compte // pas. Aucune identité n'intervient — le prêt est commun au foyer, contrairement au // statut de lecture juste au-dessus. source = prete ? source.Where(l => l.Prets.Any(p => p.DateRetour == null)) : source.Where(l => !l.Prets.Any(p => p.DateRetour == null)); } if (criteres.SansCouverture is { } sansCouverture) { // ⚠️ `IsNullOrEmpty` et non `IsNullOrWhiteSpace` : le second ne se traduit PAS en SQL, // et EF Core basculerait l'évaluation en mémoire — donc sur tout le catalogue chargé. // Une URL vide compte comme absente, comme côté client. source = sansCouverture ? source.Where(l => string.IsNullOrEmpty(l.CoverUrl)) : source.Where(l => !string.IsNullOrEmpty(l.CoverUrl)); } // ⚠️ Un terme qui est un CODE ne se cherche pas dans les mots : il désigne un objet, pas // une œuvre. Les deux branches s'excluent — chercher aussi dans les titres ne rendrait // rien de plus, et laisser la branche des mots seule ne rendrait rien du tout. if (RechercheIsbn.Formes(criteres.Recherche) is { } formes) { var longue = formes[0]; var courte = formes.Count > 1 ? formes[1] : null; return Trier(source.Where(l => l.Isbn != null && (l.Isbn.Contains(longue) || (courte != null && l.Isbn.Contains(courte))))); } var terme = NormalisationTexte.Normaliser(criteres.Recherche); if (terme.Length > 0) { // Le terme est confronté aux deux formes stockées de chaque auteur : le nom dans // l'ordre de saisie, et la clé aux mots triés. La seconde est ce qui fait que // « zola émile » trouve « Émile Zola » — le terme est trié de la même façon. var cle = RapprochementAuteurs.Cle(criteres.Recherche); source = source.Where(l => l.TitreNormalise.Contains(terme) || l.Auteurs.Any(la => la.Auteur!.NomNormalise.Contains(terme) || la.Auteur.CleRegroupement.Contains(cle))); } return Trier(source); } /// /// Tri par titre, puis par identifiant pour rendre l'ordre déterministe. /// /// /// CLAUDE.md ne tranche pas le tri. L'ordre alphabétique a été retenu parce qu'une /// bibliothèque se parcourt par titre ; l'ordre d'ajout n'a d'intérêt que le jour de la saisie. /// /// Le tri porte sur le titre normalisé : sinon « Œuvres » et « Éducation » atterrissent /// après « Zola » dans l'ordre des points de code. /// /// public static IQueryable Trier(IQueryable source) => source.OrderBy(l => l.TitreNormalise).ThenBy(l => l.Id); }