Auteurs cherchables et triables, filtre de prêt, rang dans les exports, couvertures d'envies
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>
This commit is contained in:
@@ -31,6 +31,7 @@ public static class LivresEndpoints
|
|||||||
TypeDocument? typeDocument,
|
TypeDocument? typeDocument,
|
||||||
Statut? statut,
|
Statut? statut,
|
||||||
int? auteurId,
|
int? auteurId,
|
||||||
|
bool? prete,
|
||||||
IServiceCatalogue service,
|
IServiceCatalogue service,
|
||||||
IFournisseurUtilisateur utilisateurs,
|
IFournisseurUtilisateur utilisateurs,
|
||||||
CancellationToken ct) =>
|
CancellationToken ct) =>
|
||||||
@@ -42,13 +43,14 @@ public static class LivresEndpoints
|
|||||||
TypeDocument = typeDocument,
|
TypeDocument = typeDocument,
|
||||||
Statut = statut,
|
Statut = statut,
|
||||||
AuteurId = auteurId,
|
AuteurId = auteurId,
|
||||||
|
Prete = prete,
|
||||||
};
|
};
|
||||||
|
|
||||||
var utilisateur = utilisateurs.Obtenir().Identifiant;
|
var utilisateur = utilisateurs.Obtenir().Identifiant;
|
||||||
return Results.Ok(await service.ListerAsync(criteres, utilisateur, ct));
|
return Results.Ok(await service.ListerAsync(criteres, utilisateur, ct));
|
||||||
})
|
})
|
||||||
.WithName("ListerLivres")
|
.WithName("ListerLivres")
|
||||||
.WithSummary("Liste le catalogue, filtrable par format, type de document, statut et auteur ; cherchable sur titre et auteurs.")
|
.WithSummary("Liste le catalogue, filtrable par format, type de document, statut, auteur et prêt en cours ; cherchable sur titre et auteurs.")
|
||||||
.WithDescription(
|
.WithDescription(
|
||||||
"Catalogue unique : physiques et numériques cohabitent, distingués par leur format. "
|
"Catalogue unique : physiques et numériques cohabitent, distingués par leur format. "
|
||||||
+ "La bibliothèque est commune — la liste n'est jamais restreinte à l'utilisateur courant. "
|
+ "La bibliothèque est commune — la liste n'est jamais restreinte à l'utilisateur courant. "
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ public static class ExportSouhaits
|
|||||||
|
|
||||||
sortie.Append('\n').Append(souhaits.Count).Append(souhaits.Count > 1 ? " livres" : " livre").Append('\n');
|
sortie.Append('\n').Append(souhaits.Count).Append(souhaits.Count > 1 ? " livres" : " livre").Append('\n');
|
||||||
|
|
||||||
|
// ⚠️ Le groupement par auteur DÉTRUIT l'ordre d'envie : c'est le prix, assumé, d'une
|
||||||
|
// liste rangée comme une librairie. Le rang est donc réinscrit devant chaque titre,
|
||||||
|
// faute de quoi le fichier emporté ne dit plus par quoi commencer — précisément
|
||||||
|
// l'information que l'écran de réordonnancement sert à établir.
|
||||||
|
var rangs = Rangs(souhaits);
|
||||||
|
sortie.Append("(le chiffre entre crochets est votre ordre d'envie, 1 = le plus désiré)\n");
|
||||||
|
|
||||||
// « Auteur inconnu » est rejeté en fin de liste : ce sont les entrées les moins
|
// « Auteur inconnu » est rejeté en fin de liste : ce sont les entrées les moins
|
||||||
// exploitables en rayon, elles ne doivent pas ouvrir le fichier.
|
// exploitables en rayon, elles ne doivent pas ouvrir le fichier.
|
||||||
var groupes = souhaits
|
var groupes = souhaits
|
||||||
@@ -97,7 +104,7 @@ public static class ExportSouhaits
|
|||||||
|
|
||||||
foreach (var souhait in groupe.OrderBy(s => s.Titre, StringComparer.CurrentCultureIgnoreCase))
|
foreach (var souhait in groupe.OrderBy(s => s.Titre, StringComparer.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
sortie.Append(" - ").Append(souhait.Titre);
|
sortie.Append(" - [").Append(rangs[souhait.Id]).Append("] ").Append(souhait.Titre);
|
||||||
|
|
||||||
// Éditeur, année et ISBN sur la même ligne, entre parenthèses : c'est ce qu'on
|
// Éditeur, année et ISBN sur la même ligne, entre parenthèses : c'est ce qu'on
|
||||||
// épelle à un libraire, et ça reste lisible sur un écran de téléphone.
|
// épelle à un libraire, et ça reste lisible sur un écran de téléphone.
|
||||||
@@ -127,12 +134,18 @@ public static class ExportSouhaits
|
|||||||
{
|
{
|
||||||
var sortie = new StringBuilder();
|
var sortie = new StringBuilder();
|
||||||
|
|
||||||
Ligne(sortie, "Titre", "Auteur", "Éditeur", "Année", "ISBN", "Note", "Ajouté le");
|
// Le rang ouvre le tableau : c'est la colonne sur laquelle on trie dans un tableur, et
|
||||||
|
// celle qui répond à « par quoi commence-t-on ? » quand on se répartit des achats.
|
||||||
|
// Le CSV suit déjà cet ordre, mais un ordre implicite se perd au premier tri par titre.
|
||||||
|
Ligne(sortie, "Rang", "Titre", "Auteur", "Éditeur", "Année", "ISBN", "Note", "Ajouté le");
|
||||||
|
|
||||||
|
var rangs = Rangs(souhaits);
|
||||||
|
|
||||||
foreach (var souhait in souhaits)
|
foreach (var souhait in souhaits)
|
||||||
{
|
{
|
||||||
Ligne(
|
Ligne(
|
||||||
sortie,
|
sortie,
|
||||||
|
rangs[souhait.Id].ToString(CultureInfo.InvariantCulture),
|
||||||
souhait.Titre,
|
souhait.Titre,
|
||||||
souhait.Auteur,
|
souhait.Auteur,
|
||||||
souhait.Editeur,
|
souhait.Editeur,
|
||||||
@@ -145,6 +158,18 @@ public static class ExportSouhaits
|
|||||||
return sortie.ToString();
|
return sortie.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rang affiché de chaque envie, à partir de 1.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Déduit de la <b>position dans la liste reçue</b>, et non lu sur une propriété du DTO :
|
||||||
|
/// <c>ListerAsync</c> trie déjà par <c>Rang</c>, et la colonne en base peut comporter des
|
||||||
|
/// trous (une suppression ne renumérote pas). Ce qu'on veut imprimer est « troisième de ma
|
||||||
|
/// liste », pas la valeur brute stockée.
|
||||||
|
/// </remarks>
|
||||||
|
private static Dictionary<int, int> Rangs(IReadOnlyList<SouhaitDto> souhaits) =>
|
||||||
|
souhaits.Select((s, i) => (s.Id, Rang: i + 1)).ToDictionary(x => x.Id, x => x.Rang);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ISBN découpé pour la ligne texte : c'est un numéro qu'on <b>épelle à un libraire</b>,
|
/// ISBN découpé pour la ligne texte : c'est un numéro qu'on <b>épelle à un libraire</b>,
|
||||||
/// et les tranches sont ce qui permet de ne pas se perdre au milieu de treize chiffres.
|
/// et les tranches sont ce qui permet de ne pas se perdre au milieu de treize chiffres.
|
||||||
|
|||||||
@@ -330,7 +330,21 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
|||||||
Editeur = souhait.Editeur,
|
Editeur = souhait.Editeur,
|
||||||
Annee = souhait.Annee,
|
Annee = souhait.Annee,
|
||||||
Isbn = souhait.Isbn,
|
Isbn = souhait.Isbn,
|
||||||
CoverUrl = souhait.CoverUrl,
|
|
||||||
|
// ⚠️ La couverture se déduit de l'ISBN quand elle n'a pas été fournie.
|
||||||
|
//
|
||||||
|
// Constaté en usage : aucune vignette dans la liste d'envies. La cause n'était pas le
|
||||||
|
// composant Couverture mais le chemin d'ajout — seul l'écran « ajouter une envie »
|
||||||
|
// remplissait CoverUrl, alors que la plupart des envies arrivent par la bibliographie
|
||||||
|
// d'un auteur ou par un tome manquant d'une série, qui transmettent bien un ISBN mais
|
||||||
|
// aucune image.
|
||||||
|
//
|
||||||
|
// Le repli est posé ICI, à la lecture, et non à l'écriture : il vaut alors aussi pour
|
||||||
|
// les envies DÉJÀ enregistrées, sans migration ni rattrapage. La règle « sans ISBN,
|
||||||
|
// pas de couverture, et on n'en invente pas » est intacte — on n'invente rien, on
|
||||||
|
// applique la formule OpenLibrary habituelle à un ISBN qu'on possède déjà.
|
||||||
|
CoverUrl = souhait.CoverUrl
|
||||||
|
?? (souhait.Isbn is null ? null : IsbnUtils.UrlCouverture(souhait.Isbn)),
|
||||||
Note = souhait.Note,
|
Note = souhait.Note,
|
||||||
DateAjout = souhait.DateAjout,
|
DateAjout = souhait.DateAjout,
|
||||||
Possede = livreId is not null,
|
Possede = livreId is not null,
|
||||||
|
|||||||
@@ -67,21 +67,71 @@ else if (_auteurs.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p class="compteur">@_auteurs.Count auteur@(_auteurs.Count > 1 ? "s" : "")</p>
|
@*
|
||||||
|
Recherche et tri d'une seule pièce : la question qu'on se pose devant une liste
|
||||||
|
d'auteurs est soit « où est untel ? » (recherche), soit « qui ai-je le plus ? » (tri).
|
||||||
|
|
||||||
|
⚠️ Tout se fait en mémoire, sans nouvel appel : la liste des auteurs est déjà
|
||||||
|
entièrement chargée — c'est elle qui sert l'instantané hors-ligne — et la filtrer
|
||||||
|
côté serveur aurait rendu la page inutilisable sans réseau. La normalisation passe
|
||||||
|
par NormalisationTexte, comme partout : « emile » retrouve « Émile ».
|
||||||
|
*@
|
||||||
|
<div class="barre-recherche">
|
||||||
|
<input class="champ-saisie" type="search" placeholder="Nom d'auteur…"
|
||||||
|
value="@_recherche" @oninput="SurRecherche" aria-label="Rechercher un auteur" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="segments" role="group" aria-label="Tri">
|
||||||
|
<button type="button" class="segment @(_tri == TriAuteurs.Nom ? "segment-actif" : null)"
|
||||||
|
@onclick="() => _tri = TriAuteurs.Nom">Par nom</button>
|
||||||
|
<button type="button" class="segment @(_tri == TriAuteurs.NombreDeLivres ? "segment-actif" : null)"
|
||||||
|
@onclick="() => _tri = TriAuteurs.NombreDeLivres">Les plus présents</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Le compteur dit ce qu'on regarde : sur une liste filtrée, afficher le total ferait
|
||||||
|
croire que la recherche n'a rien fait. *@
|
||||||
|
<p class="compteur">
|
||||||
|
@Affiches.Count auteur@(Affiches.Count > 1 ? "s" : "")
|
||||||
|
@if (Affiches.Count != _auteurs.Count)
|
||||||
|
{
|
||||||
|
<text> sur @_auteurs.Count</text>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (Affiches.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="message-discret">Aucun auteur ne correspond à « @_recherche ».</p>
|
||||||
|
}
|
||||||
|
|
||||||
<ul class="liste-auteurs">
|
<ul class="liste-auteurs">
|
||||||
@foreach (var auteur in _auteurs)
|
@foreach (var auteur in Affiches)
|
||||||
{
|
{
|
||||||
|
@*
|
||||||
|
Trois colonnes, dont une seule est élastique.
|
||||||
|
|
||||||
|
Le nombre de livres passe à GAUCHE du nom, sur une largeur figée de trois
|
||||||
|
chiffres : les compteurs s'alignent alors les uns sous les autres et se
|
||||||
|
comparent d'un coup d'œil. Trois chiffres suffisent — personne ne possède
|
||||||
|
mille livres d'un même auteur. Les actions se rangent tout à droite, elles
|
||||||
|
aussi sur une largeur constante, pour que seul le nom absorbe la place
|
||||||
|
restante au lieu de décaler les boutons ligne après ligne.
|
||||||
|
*@
|
||||||
<li class="carte-auteur-ligne" @key="auteur.Id">
|
<li class="carte-auteur-ligne" @key="auteur.Id">
|
||||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
<span class="auteur-compte" title="@Livres(auteur.NombreDeLivres)">
|
||||||
<span class="etiquette">@Livres(auteur.NombreDeLivres)</span>
|
@auteur.NombreDeLivres
|
||||||
|
<span class="lecture-ecran">@Livres(auteur.NombreDeLivres)</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a class="lien-auteur auteur-nom" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
||||||
|
|
||||||
|
<span class="auteur-actions">
|
||||||
@* Le declencheur decrit dans IDEES.md : depuis un auteur qu'on a deja, voir
|
@* Le declencheur decrit dans IDEES.md : depuis un auteur qu'on a deja, voir
|
||||||
tout ce qu'il a ecrit et marquer ce qui manque. *@
|
tout ce qu'il a ecrit et marquer ce qui manque. *@
|
||||||
<a class="bouton bouton-compact bouton-discret"
|
<a class="bouton bouton-compact bouton-discret"
|
||||||
href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
|
href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
|
||||||
@if (_auteurRenomme == auteur.Id)
|
@if (_auteurRenomme == auteur.Id)
|
||||||
{
|
{
|
||||||
<input class="champ-texte champ-auteur-renomme" @bind="_nomRenomme" />
|
<input class="champ-saisie champ-auteur-renomme" @bind="_nomRenomme" />
|
||||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||||
disabled="@_renommageEnCours" @onclick="RenommerAsync">
|
disabled="@_renommageEnCours" @onclick="RenommerAsync">
|
||||||
Enregistrer
|
Enregistrer
|
||||||
@@ -100,6 +150,7 @@ else
|
|||||||
Renommer
|
Renommer
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -127,7 +178,19 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
/// <summary>Les deux façons de trier une liste d'auteurs, et il n'y en a pas de troisième.</summary>
|
||||||
|
private enum TriAuteurs
|
||||||
|
{
|
||||||
|
/// <summary>Ordre alphabétique : « où est untel ? ».</summary>
|
||||||
|
Nom = 0,
|
||||||
|
|
||||||
|
/// <summary>Du plus présent au moins présent : « qui ai-je le plus ? ».</summary>
|
||||||
|
NombreDeLivres = 1,
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<AuteurDto>? _auteurs;
|
private IReadOnlyList<AuteurDto>? _auteurs;
|
||||||
|
private string _recherche = string.Empty;
|
||||||
|
private TriAuteurs _tri = TriAuteurs.Nom;
|
||||||
private IReadOnlyList<SuggestionRapprochement>? _suggestions;
|
private IReadOnlyList<SuggestionRapprochement>? _suggestions;
|
||||||
private bool _enCours;
|
private bool _enCours;
|
||||||
private bool _renommageEnCours;
|
private bool _renommageEnCours;
|
||||||
@@ -153,6 +216,47 @@ else
|
|||||||
|
|
||||||
private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";
|
private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";
|
||||||
|
|
||||||
|
private void SurRecherche(ChangeEventArgs e) => _recherche = e.Value?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La liste telle qu'elle s'affiche : filtrée par la recherche, puis triée.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// La recherche est une <b>sous-chaîne du nom normalisé</b>, comme celle du catalogue :
|
||||||
|
/// « zola » retrouve « Émile Zola », et « emile » aussi. Elle ne cherche pas mot à mot,
|
||||||
|
/// contrairement au rapprochement d'auteurs, qui répond à une autre question — « ces deux
|
||||||
|
/// noms désignent-ils la même personne ? » — et n'a rien à faire ici.
|
||||||
|
/// <para>
|
||||||
|
/// Le tri par nom est <b>ordinal sur la forme normalisée</b>, pour ne pas dépendre de la
|
||||||
|
/// culture du navigateur : c'est la même règle que <c>FiltreLivresLocal</c>, et elle évite
|
||||||
|
/// qu'un appareil classe « Éluard » avant « Duras » et un autre après.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private IReadOnlyList<AuteurDto> Affiches
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_auteurs is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var terme = NormalisationTexte.Normaliser(_recherche);
|
||||||
|
var retenus = string.IsNullOrEmpty(terme)
|
||||||
|
? _auteurs
|
||||||
|
: _auteurs.Where(a => NormalisationTexte.Normaliser(a.Nom).Contains(terme)).ToList();
|
||||||
|
|
||||||
|
return _tri == TriAuteurs.NombreDeLivres
|
||||||
|
? retenus
|
||||||
|
.OrderByDescending(a => a.NombreDeLivres)
|
||||||
|
.ThenBy(a => NormalisationTexte.Normaliser(a.Nom), StringComparer.Ordinal)
|
||||||
|
.ToList()
|
||||||
|
: retenus
|
||||||
|
.OrderBy(a => NormalisationTexte.Normaliser(a.Nom), StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ChargerAsync()
|
private async Task ChargerAsync()
|
||||||
{
|
{
|
||||||
_erreur = null;
|
_erreur = null;
|
||||||
|
|||||||
@@ -86,9 +86,33 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@*
|
||||||
|
Le prêt, lui, est COMMUN au foyer : ce filtre ne dépend de personne. L'écran
|
||||||
|
« Prêts » répond déjà à « qu'est-ce qui est dehors ? » ; celui-ci sert l'autre
|
||||||
|
moitié de la question — « qu'est-ce que j'ai réellement sous la main ? » —, celle
|
||||||
|
qu'on se pose avant de promettre un livre à quelqu'un.
|
||||||
|
|
||||||
|
⚠️ Même règle que pour les formats et les types : la ligne disparaît tant que rien
|
||||||
|
n'est prêté, où elle n'offrirait qu'un choix utile. Et comme pour eux, elle ne se
|
||||||
|
déduit QUE d'un chargement sans critère, sinon une recherche qui ne remonte que des
|
||||||
|
livres à la maison escamoterait le filtre sans qu'on puisse l'élargir.
|
||||||
|
*@
|
||||||
|
@if (QuelqueChoseEstPrete)
|
||||||
|
{
|
||||||
|
<div class="segments" role="group" aria-label="Filtrer par prêt en cours">
|
||||||
|
<button type="button" class="segment @(_prete is null ? "segment-actif" : null)"
|
||||||
|
@onclick="() => FiltrerPret(null)">Tous</button>
|
||||||
|
<button type="button" class="segment @(_prete == false ? "segment-actif" : null)"
|
||||||
|
@onclick="() => FiltrerPret(false)">À la maison</button>
|
||||||
|
<button type="button"
|
||||||
|
class="segment segment-prete @(_prete == true ? "segment-actif" : null)"
|
||||||
|
@onclick="() => FiltrerPret(true)">Prêtés</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<p class="champ-aide">
|
<p class="champ-aide">
|
||||||
Le statut de lecture est <strong>le vôtre</strong> : filtrer dessus montre votre
|
Le statut de lecture est <strong>le vôtre</strong> : filtrer dessus montre votre
|
||||||
lecture, pas celle du foyer.
|
lecture, pas celle du foyer. Le prêt, lui, est commun.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -157,6 +181,7 @@ else if (_livres is not null)
|
|||||||
private Format? _format;
|
private Format? _format;
|
||||||
private TypeDocument? _type;
|
private TypeDocument? _type;
|
||||||
private Statut? _statut;
|
private Statut? _statut;
|
||||||
|
private bool? _prete;
|
||||||
private bool _chargement;
|
private bool _chargement;
|
||||||
private bool _filtresOuverts;
|
private bool _filtresOuverts;
|
||||||
private string? _erreur;
|
private string? _erreur;
|
||||||
@@ -180,6 +205,14 @@ else if (_livres is not null)
|
|||||||
|
|
||||||
private bool PlusieursTypes => _typesDuFonds is null || _typesDuFonds.Count > 1;
|
private bool PlusieursTypes => _typesDuFonds is null || _typesDuFonds.Count > 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vrai si au moins un livre du fonds est actuellement dehors — ou qu'on ne le sait pas.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Mêmes précautions que les formats : dans le doute, la ligne reste affichée.</remarks>
|
||||||
|
private bool? _fondsAvecPret;
|
||||||
|
|
||||||
|
private bool QuelqueChoseEstPrete => _fondsAvecPret is not false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nombre de filtres repliés actuellement actifs, affiché sur le bouton « Filtrer ».
|
/// Nombre de filtres repliés actuellement actifs, affiché sur le bouton « Filtrer ».
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -189,14 +222,15 @@ else if (_livres is not null)
|
|||||||
/// qui serait invisible une fois le panneau refermé.
|
/// qui serait invisible une fois le panneau refermé.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private int FiltresActifs =>
|
private int FiltresActifs =>
|
||||||
(_format is null ? 0 : 1) + (_type is null ? 0 : 1) + (_statut is null ? 0 : 1);
|
(_format is null ? 0 : 1) + (_type is null ? 0 : 1) + (_statut is null ? 0 : 1)
|
||||||
|
+ (_prete is null ? 0 : 1);
|
||||||
|
|
||||||
// Une frappe au clavier annule la recherche précédente encore en vol.
|
// Une frappe au clavier annule la recherche précédente encore en vol.
|
||||||
private CancellationTokenSource _cts = new();
|
private CancellationTokenSource _cts = new();
|
||||||
|
|
||||||
private bool EstFiltre =>
|
private bool EstFiltre =>
|
||||||
!string.IsNullOrWhiteSpace(_recherche) || _format is not null || _type is not null
|
!string.IsNullOrWhiteSpace(_recherche) || _format is not null || _type is not null
|
||||||
|| _statut is not null || AuteurId is not null;
|
|| _statut is not null || _prete is not null || AuteurId is not null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// La liste se relit à chaque bascule du réseau : au retour, pour reprendre les données du
|
/// La liste se relit à chaque bascule du réseau : au retour, pour reprendre les données du
|
||||||
@@ -245,12 +279,19 @@ else if (_livres is not null)
|
|||||||
return ChargerAsync();
|
return ChargerAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task FiltrerPret(bool? prete)
|
||||||
|
{
|
||||||
|
_prete = prete;
|
||||||
|
return ChargerAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Vrai quand la liste demandée est le fonds entier, sans restriction d'aucune sorte.</summary>
|
/// <summary>Vrai quand la liste demandée est le fonds entier, sans restriction d'aucune sorte.</summary>
|
||||||
private static bool EstSansCritere(CritereLivres criteres) =>
|
private static bool EstSansCritere(CritereLivres criteres) =>
|
||||||
string.IsNullOrWhiteSpace(criteres.Recherche)
|
string.IsNullOrWhiteSpace(criteres.Recherche)
|
||||||
&& criteres.Format is null
|
&& criteres.Format is null
|
||||||
&& criteres.TypeDocument is null
|
&& criteres.TypeDocument is null
|
||||||
&& criteres.Statut is null
|
&& criteres.Statut is null
|
||||||
|
&& criteres.Prete is null
|
||||||
&& criteres.AuteurId is null;
|
&& criteres.AuteurId is null;
|
||||||
|
|
||||||
private async Task ChargerAsync()
|
private async Task ChargerAsync()
|
||||||
@@ -271,6 +312,7 @@ else if (_livres is not null)
|
|||||||
Format = _format,
|
Format = _format,
|
||||||
TypeDocument = _type,
|
TypeDocument = _type,
|
||||||
Statut = _statut,
|
Statut = _statut,
|
||||||
|
Prete = _prete,
|
||||||
AuteurId = AuteurId,
|
AuteurId = AuteurId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -280,6 +322,7 @@ else if (_livres is not null)
|
|||||||
{
|
{
|
||||||
_formatsDuFonds = _livres.Select(l => l.Format).ToHashSet();
|
_formatsDuFonds = _livres.Select(l => l.Format).ToHashSet();
|
||||||
_typesDuFonds = _livres.Select(l => l.TypeDocument).ToHashSet();
|
_typesDuFonds = _livres.Select(l => l.TypeDocument).ToHashSet();
|
||||||
|
_fondsAvecPret = _livres.Any(l => l.PreteA is not null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ public static class FiltreLivresLocal
|
|||||||
livres = livres.Where(l => l.Auteurs.Any(a => a.Id == auteurId));
|
livres = livres.Where(l => l.Auteurs.Any(a => a.Id == auteurId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (criteres.Prete is { } prete)
|
||||||
|
{
|
||||||
|
// LivreDto.PreteA porte l'emprunteur du prêt EN COURS, ou null : le serveur a déjà
|
||||||
|
// écarté les prêts clos. C'est exactement ce que le filtre serveur exprime par
|
||||||
|
// « une ligne sans date de retour », d'où l'équivalence des deux implémentations.
|
||||||
|
livres = livres.Where(l => (l.PreteA is not null) == prete);
|
||||||
|
}
|
||||||
|
|
||||||
var terme = NormalisationTexte.Normaliser(criteres.Recherche);
|
var terme = NormalisationTexte.Normaliser(criteres.Recherche);
|
||||||
if (terme.Length > 0)
|
if (terme.Length > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
|||||||
parametres.Add($"auteurId={auteurId}");
|
parametres.Add($"auteurId={auteurId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (criteres.Prete is { } prete)
|
||||||
|
{
|
||||||
|
// Minuscules : c'est ce que le liage de ASP.NET Core attend d'un bool?.
|
||||||
|
parametres.Add($"prete={(prete ? "true" : "false")}");
|
||||||
|
}
|
||||||
|
|
||||||
var url = "api/livres" + (parametres.Count > 0 ? "?" + string.Join('&', parametres) : string.Empty);
|
var url = "api/livres" + (parametres.Count > 0 ? "?" + string.Join('&', parametres) : string.Empty);
|
||||||
|
|
||||||
var (ok, liste) = await EssayerAsync(() => http.GetFromJsonAsync<List<LivreDto>>(url, Json, ct));
|
var (ok, liste) = await EssayerAsync(() => http.GetFromJsonAsync<List<LivreDto>>(url, Json, ct));
|
||||||
|
|||||||
@@ -14,3 +14,4 @@
|
|||||||
@using MaBibli.Shared.Dtos
|
@using MaBibli.Shared.Dtos
|
||||||
@using MaBibli.Shared.Entites
|
@using MaBibli.Shared.Entites
|
||||||
@using MaBibli.Shared.Isbn
|
@using MaBibli.Shared.Isbn
|
||||||
|
@using MaBibli.Shared.Textes
|
||||||
|
|||||||
@@ -248,6 +248,10 @@ body {
|
|||||||
|
|
||||||
/* Type de document : même vocabulaire que les pastilles de statut, deux couleurs distinctes
|
/* Type de document : même vocabulaire que les pastilles de statut, deux couleurs distinctes
|
||||||
des trois du statut et de celle du prêt, pour qu'on reconnaisse ce qu'on filtre (A2, IDEES.md). */
|
des trois du statut et de celle du prêt, pour qu'on reconnaisse ce qu'on filtre (A2, IDEES.md). */
|
||||||
|
/* Le segment « Prêtés » reprend la couleur de l'étiquette de prêt, comme les segments de
|
||||||
|
statut reprennent celle de leur pastille : le filtre et la carte doivent se répondre. */
|
||||||
|
.segment.segment-prete.segment-actif { background: #8a5a00; border-color: #8a5a00; }
|
||||||
|
|
||||||
.segment.type-roman.segment-actif { background: #2a6f97; border-color: #2a6f97; }
|
.segment.type-roman.segment-actif { background: #2a6f97; border-color: #2a6f97; }
|
||||||
.segment.type-bd.segment-actif { background: #a13d63; border-color: #a13d63; }
|
.segment.type-bd.segment-actif { background: #a13d63; border-color: #a13d63; }
|
||||||
|
|
||||||
@@ -739,12 +743,50 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
padding: 0.4rem 0;
|
padding: 0.4rem 0;
|
||||||
border-bottom: 1px solid var(--mb-bord);
|
border-bottom: 1px solid var(--mb-bord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Trois colonnes, dont une seule est élastique.
|
||||||
|
|
||||||
|
⚠️ C'est la largeur FIGÉE des deux colonnes extrêmes qui fait tout l'intérêt : sans elle,
|
||||||
|
chaque ligne plaçait son compteur et ses boutons à un endroit différent, au gré de la
|
||||||
|
longueur du nom, et l'œil devait relire chaque ligne entière. Trois chiffres suffisent au
|
||||||
|
compteur — personne ne possède mille livres d'un même auteur.
|
||||||
|
*/
|
||||||
|
.auteur-compte {
|
||||||
|
flex: 0 0 2.5rem;
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mb-texte-doux);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auteur-nom {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auteur-actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Le champ de renommage s'insère dans la colonne d'actions : il doit pouvoir rétrécir, sinon
|
||||||
|
il pousse « Enregistrer » hors de l'écran sur un téléphone. */
|
||||||
|
.champ-auteur-renomme {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rapprochements {
|
.rapprochements {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
|||||||
@@ -46,4 +46,20 @@ public record CritereLivres
|
|||||||
|
|
||||||
/// <summary>Ne garder que les livres d'un auteur — le « cliquer sur un auteur » d'IDEES.md.</summary>
|
/// <summary>Ne garder que les livres d'un auteur — le « cliquer sur un auteur » d'IDEES.md.</summary>
|
||||||
public int? AuteurId { get; init; }
|
public int? AuteurId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>true</c> = ce qui est dehors, <c>false</c> = ce qui est à la maison, <c>null</c> = tout.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// ⚠️ Contrairement à <see cref="Statut"/>, ce critère est <b>commun au foyer</b> : un prêt
|
||||||
|
/// n'appartient à personne en particulier, et aucune identité n'entre dans son évaluation.
|
||||||
|
/// C'est la même dissymétrie que partout ailleurs — le livre absent l'est pour tout le monde,
|
||||||
|
/// sa lecture ne l'est que pour soi.
|
||||||
|
/// <para>
|
||||||
|
/// L'écran « Prêts » répond déjà à « qu'est-ce qui est dehors ? ». Ce critère-ci répond à
|
||||||
|
/// l'autre moitié de la question, celle qu'aucun écran ne servait : « qu'est-ce que j'ai
|
||||||
|
/// réellement sous la main ? », par exemple avant de promettre un livre à quelqu'un.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public bool? Prete { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ public static class FiltreLivres
|
|||||||
source = source.Where(l => l.Auteurs.Any(la => la.AuteurId == 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);
|
var terme = NormalisationTexte.Normaliser(criteres.Recherche);
|
||||||
if (terme.Length > 0)
|
if (terme.Length > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ public class ExportSouhaitsTests
|
|||||||
Assert.Single(lignes, l => l == "Bernard Werber");
|
Assert.Single(lignes, l => l == "Bernard Werber");
|
||||||
|
|
||||||
var debutWerber = Array.IndexOf(lignes, "Bernard Werber");
|
var debutWerber = Array.IndexOf(lignes, "Bernard Werber");
|
||||||
Assert.Equal(" - La prophétie des abeilles", lignes[debutWerber + 1]);
|
// Le crochet porte le rang d'envie, que le groupement par auteur ferait sinon
|
||||||
Assert.Equal(" - Les fourmis", lignes[debutWerber + 2]);
|
// disparaître : la liste s'emporte pour savoir par quoi commencer.
|
||||||
|
Assert.Equal(" - [1] La prophétie des abeilles", lignes[debutWerber + 1]);
|
||||||
|
Assert.Equal(" - [3] Les fourmis", lignes[debutWerber + 2]);
|
||||||
|
|
||||||
// Werber avant Zola : les auteurs sont classés alphabétiquement.
|
// Werber avant Zola : les auteurs sont classés alphabétiquement.
|
||||||
Assert.True(debutWerber < Array.IndexOf(lignes, "Émile Zola"));
|
Assert.True(debutWerber < Array.IndexOf(lignes, "Émile Zola"));
|
||||||
@@ -75,7 +77,7 @@ public class ExportSouhaitsTests
|
|||||||
|
|
||||||
// ISBN découpé : c'est un numéro qu'on épelle à un libraire. La valeur passée est nue
|
// ISBN découpé : c'est un numéro qu'on épelle à un libraire. La valeur passée est nue
|
||||||
// (« 2253004227 »), les tirets n'existent qu'à la sortie.
|
// (« 2253004227 »), les tirets n'existent qu'à la sortie.
|
||||||
Assert.Contains(" - Germinal (Le Livre de poche, 1885, ISBN 2-253-00422-7)", texte);
|
Assert.Contains(" - [1] Germinal (Le Livre de poche, 1885, ISBN 2-253-00422-7)", texte);
|
||||||
Assert.Contains(" en poche seulement", texte);
|
Assert.Contains(" en poche seulement", texte);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +111,8 @@ public class ExportSouhaitsTests
|
|||||||
|
|
||||||
var lignes = csv.Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
|
var lignes = csv.Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
Assert.Equal("Titre;Auteur;Éditeur;Année;ISBN;Note;Ajouté le", lignes[0]);
|
Assert.Equal("Rang;Titre;Auteur;Éditeur;Année;ISBN;Note;Ajouté le", lignes[0]);
|
||||||
Assert.Equal("Germinal;Émile Zola;Le Livre de poche;1885;;;2026-08-18", lignes[1]);
|
Assert.Equal("1;Germinal;Émile Zola;Le Livre de poche;1885;;;2026-08-18", lignes[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -125,11 +127,35 @@ public class ExportSouhaitsTests
|
|||||||
|
|
||||||
var ligne = csv.Split("\r\n")[1];
|
var ligne = csv.Split("\r\n")[1];
|
||||||
|
|
||||||
Assert.StartsWith("\"Titre; piégé\";", ligne);
|
Assert.StartsWith("1;\"Titre; piégé\";", ligne);
|
||||||
Assert.Contains("\"il a dit \"\"oui\"\"\"", ligne);
|
Assert.Contains("\"il a dit \"\"oui\"\"\"", ligne);
|
||||||
|
|
||||||
// Sept colonnes une fois l'échappement respecté, pas huit.
|
// Huit colonnes une fois l'échappement respecté, pas neuf.
|
||||||
Assert.Equal(7, ColonnesCsv(ligne).Count);
|
Assert.Equal(8, ColonnesCsv(ligne).Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Le rang imprimé est la <b>position dans la liste reçue</b>, qui vient déjà triée par
|
||||||
|
/// <c>Rang</c>. Le texte la regroupe par auteur, ce qui casse l'ordre à l'affichage : le
|
||||||
|
/// crochet est alors la seule trace de « par quoi commencer ».
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Le_rang_suit_lordre_recu_et_non_lalphabet()
|
||||||
|
{
|
||||||
|
IReadOnlyList<SouhaitDto> liste =
|
||||||
|
[
|
||||||
|
Souhait(7, "Zorro", "Émile Zola"),
|
||||||
|
Souhait(3, "Abécédaire", "Émile Zola"),
|
||||||
|
];
|
||||||
|
|
||||||
|
var texte = ExportSouhaits.Texte(liste, null, Jour);
|
||||||
|
Assert.Contains(" - [1] Zorro", texte);
|
||||||
|
Assert.Contains(" - [2] Abécédaire", texte);
|
||||||
|
|
||||||
|
// Le CSV, lui, garde l'ordre : le rang y est croissant ligne après ligne.
|
||||||
|
var lignes = ExportSouhaits.Csv(liste).Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
Assert.StartsWith("1;Zorro;", lignes[1]);
|
||||||
|
Assert.StartsWith("2;Abécédaire;", lignes[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ public class FiltreLivresLocalTests
|
|||||||
|
|
||||||
private static Livre Livre(
|
private static Livre Livre(
|
||||||
int id, string titre, Auteur? auteur, Format format, Statut? statut,
|
int id, string titre, Auteur? auteur, Format format, Statut? statut,
|
||||||
TypeDocument type = TypeDocument.NonPrecise)
|
TypeDocument type = TypeDocument.NonPrecise,
|
||||||
|
string? preteA = null, bool pretClos = false)
|
||||||
{
|
{
|
||||||
var livre = new Livre
|
var livre = new Livre
|
||||||
{
|
{
|
||||||
@@ -50,6 +51,20 @@ public class FiltreLivresLocalTests
|
|||||||
livre.Statuts.Add(new StatutLecture { LivreId = id, Utilisateur = Lecteur, Statut = valeur });
|
livre.Statuts.Add(new StatutLecture { LivreId = id, Utilisateur = Lecteur, Statut = valeur });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preteA is not null)
|
||||||
|
{
|
||||||
|
// ⚠️ Un prêt CLOS ne doit pas compter comme « dehors ». C'est le point où les deux
|
||||||
|
// implémentations pourraient diverger sans qu'on s'en aperçoive : le serveur regarde
|
||||||
|
// DateRetour, le client un PreteA que le serveur a déjà résolu.
|
||||||
|
livre.Prets.Add(new Pret
|
||||||
|
{
|
||||||
|
LivreId = id,
|
||||||
|
Emprunteur = preteA,
|
||||||
|
DatePret = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||||
|
DateRetour = pretClos ? new DateTime(2026, 2, 1, 0, 0, 0, DateTimeKind.Utc) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return livre;
|
return livre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +72,9 @@ public class FiltreLivresLocalTests
|
|||||||
[
|
[
|
||||||
Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, TypeDocument.Roman),
|
Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, TypeDocument.Roman),
|
||||||
Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire, TypeDocument.BandeDessinee),
|
Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire, TypeDocument.BandeDessinee),
|
||||||
Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours, TypeDocument.Roman),
|
Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours, TypeDocument.Roman,
|
||||||
Livre(4, "Bel-Ami", Maupassant, Format.Numerique, null),
|
preteA: "Marie"),
|
||||||
|
Livre(4, "Bel-Ami", Maupassant, Format.Numerique, null, preteA: "Paul", pretClos: true),
|
||||||
Livre(5, "Œuvres complètes", Zola, Format.Physique, Statut.ALire),
|
Livre(5, "Œuvres complètes", Zola, Format.Physique, Statut.ALire),
|
||||||
Livre(6, "L'Éducation sentimentale", null, Format.Physique, null),
|
Livre(6, "L'Éducation sentimentale", null, Format.Physique, null),
|
||||||
];
|
];
|
||||||
@@ -76,6 +92,9 @@ public class FiltreLivresLocalTests
|
|||||||
TypeDocument = l.TypeDocument,
|
TypeDocument = l.TypeDocument,
|
||||||
DateAjout = DateTime.UtcNow,
|
DateAjout = DateTime.UtcNow,
|
||||||
Statut = l.Statuts.FirstOrDefault(s => s.Utilisateur == Lecteur)?.Statut,
|
Statut = l.Statuts.FirstOrDefault(s => s.Utilisateur == Lecteur)?.Statut,
|
||||||
|
|
||||||
|
// Ce que l'API expose : l'emprunteur du prêt EN COURS, rien de l'historique clos.
|
||||||
|
PreteA = l.Prets.FirstOrDefault(p => p.DateRetour is null)?.Emprunteur,
|
||||||
Auteurs = l.Auteurs
|
Auteurs = l.Auteurs
|
||||||
.Select(la => new AuteurDto { Id = la.AuteurId, Nom = la.Auteur!.Nom })
|
.Select(la => new AuteurDto { Id = la.AuteurId, Nom = la.Auteur!.Nom })
|
||||||
.ToList(),
|
.ToList(),
|
||||||
@@ -104,6 +123,10 @@ public class FiltreLivresLocalTests
|
|||||||
new CritereLivres { AuteurId = 1 },
|
new CritereLivres { AuteurId = 1 },
|
||||||
new CritereLivres { AuteurId = 2 },
|
new CritereLivres { AuteurId = 2 },
|
||||||
new CritereLivres { AuteurId = 99 },
|
new CritereLivres { AuteurId = 99 },
|
||||||
|
new CritereLivres { Prete = true },
|
||||||
|
new CritereLivres { Prete = false },
|
||||||
|
new CritereLivres { Prete = false, Format = Format.Physique },
|
||||||
|
new CritereLivres { Prete = true, Recherche = "horla" },
|
||||||
new CritereLivres { Recherche = "zola", Format = Format.Physique, Statut = Statut.ALire },
|
new CritereLivres { Recherche = "zola", Format = Format.Physique, Statut = Statut.ALire },
|
||||||
new CritereLivres
|
new CritereLivres
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user