Auteurs - Recherche et tri (par nom, ou du plus présent au moins présent), en mémoire : la liste est déjà chargée en entier — c'est elle qui sert l'instantané hors-ligne — et la filtrer côté serveur aurait rendu la page inutilisable sans réseau. - Trois colonnes dont une seule est élastique : compteur à gauche sur une largeur figée, nom au milieu, actions à droite. Les compteurs et les boutons s'alignent alors d'une ligne à l'autre au lieu de suivre la longueur du nom. Catalogue - Filtre « À la maison / Prêtés », posé dans les deux implémentations (serveur et hors-ligne) et dans le test qui les confronte, avec un cas de prêt CLOS — c'est exactement là que les deux auraient pu diverger en silence. - La ligne disparaît tant que rien n'est prêté, comme celles des formats et des types. Envies - ⚠️ Les couvertures manquaient parce que seul l'écran « ajouter une envie » remplissait CoverUrl : la bibliographie et les tomes manquants transmettent un ISBN mais aucune image. Le repli est posé à la LECTURE, donc il vaut aussi pour les envies déjà enregistrées, sans migration. - Les exports .txt et .csv portent le rang d'envie. Le .txt en a le plus besoin : son groupement par auteur détruit précisément cet ordre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
4.1 KiB
C#
99 lines
4.1 KiB
C#
using MaBibli.Shared.Entites;
|
|
using MaBibli.Shared.Textes;
|
|
|
|
namespace MaBibli.Shared.Catalogue;
|
|
|
|
/// <summary>
|
|
/// Filtrage, recherche et tri du catalogue.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Écrit sur <see cref="IQueryable{T}"/> pour être exécuté par EF Core côté base <b>et</b>
|
|
/// testable en mémoire sur une simple liste, sans base ni réseau.
|
|
/// <para>
|
|
/// La recherche porte sur les colonnes <b>normalisées</b> (<see cref="Livre.TitreNormalise"/>,
|
|
/// <see cref="Auteur.NomNormalise"/>), pas sur les colonnes d'affichage : SQLite ne sait pas
|
|
/// ignorer les accents, et sa collation <c>NOCASE</c> 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 ».
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class FiltreLivres
|
|
{
|
|
/// <param name="utilisateur">
|
|
/// Personne dont on regarde le statut. Sert <b>uniquement</b> à
|
|
/// <see cref="CritereLivres.Statut"/> : il ne restreint jamais la liste elle-même.
|
|
/// </param>
|
|
public static IQueryable<Livre> Appliquer(
|
|
IQueryable<Livre> 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));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tri par titre, puis par identifiant pour rendre l'ordre déterministe.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// <para>
|
|
/// Le tri porte sur le titre normalisé : sinon « Œuvres » et « Éducation » atterrissent
|
|
/// après « Zola » dans l'ordre des points de code.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IQueryable<Livre> Trier(IQueryable<Livre> source) =>
|
|
source.OrderBy(l => l.TitreNormalise).ThenBy(l => l.Id);
|
|
}
|