- Cataloguer en rafale — nouvel écran /ajout/rafale : on scanne une pile de livres à la suite dans une zone de texte, chaque code est traité (BnF puis OpenLibrary), les doublons connus sont passés automatiquement. La collecte marche même hors-ligne. Le compte rendu liste maintenant les livres créés, en lien vers leur fiche, et reste consultable en revenant sur l'écran même après une rafale entièrement réussie. Le catalogue reconnaît un ISBN dans sa barre de recherche (13 ou 10 chiffres, avec ou sans tirets) : scanner un livre en main ouvre directement sa fiche s'il n'y en a qu'un. Un bouton « Scanner » l'alimente, actif hors-ligne. - Ajouter un tome à une série accepte aussi un ISBN dans le champ manuel : le catalogue est cherché d'abord (rattachement direct si un seul exemplaire), sinon la BnF prend le relais. Séries et sagas - Numéro de tome distinct de la position de lecture : on peut indiquer « c'est le tome 7 » même si on ne possède pas les six premiers ; l'ordre de lecture reste un réglage séparé (utile pour les préquelles). - Tri par numéro en plus du tri par ordre de lecture quand des tomes en portent un. - Panneau « Ajouter » regroupé et repliable sur la fiche d'une série (manuellement / en rafale / depuis le catalogue / depuis les envies), au lieu de quatre formulaires ouverts en permanence. - Filtre catalogue « sans couverture » pour repérer les livres à illustrer. Le catalogue groupe les tomes d'une même série sous un bloc repliable, avec un décompte plus clair (affichés / possédés / total). - Corrections directes sur la fiche - Effacer un prêt saisi par erreur (bouton ✕ sur chaque ligne, avec confirmation), sans passer par « rendre ». - Corriger une couverture manquante ou cassée en cliquant dessus : le champ d'adresse s'ouvre focalisé, Entrée enregistre. Étendu aux numéros de revue dans la dernière modification. - Les thèmes déjà utilisés dans la bibliothèque sont proposés à la frappe. - La recherche d'un livre à rattacher montre des suggestions dès le focus, sans attendre de taper. - Visuel : un rendu manquant après une écriture asynchrone dans le formulaire de livre, une bascule de rôle cassée, des débordements à 320 px, et le style d'un bouton-lien qui restait souligné.
123 lines
5.3 KiB
C#
123 lines
5.3 KiB
C#
using MaBibli.Shared.Entites;
|
|
using MaBibli.Shared.Isbn;
|
|
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));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <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);
|
|
}
|