/// La liste se relit à chaque bascule du réseau : au retour, pour reprendre les données du
@@ -255,6 +286,12 @@ else if (_livres is not null)
return ChargerAsync();
}
+ private Task FiltrerType(TypeDocument? type)
+ {
+ _type = type;
+ return ChargerAsync();
+ }
+
private Task FiltrerStatut(Statut? statut)
{
_statut = statut;
@@ -265,6 +302,7 @@ else if (_livres is not null)
private static bool EstSansCritere(CritereLivres criteres) =>
string.IsNullOrWhiteSpace(criteres.Recherche)
&& criteres.Format is null
+ && criteres.TypeDocument is null
&& criteres.Statut is null
&& criteres.AuteurId is null;
@@ -284,6 +322,7 @@ else if (_livres is not null)
{
Recherche = _recherche,
Format = _format,
+ TypeDocument = _type,
Statut = _statut,
AuteurId = AuteurId,
};
@@ -293,6 +332,7 @@ else if (_livres is not null)
if (EstSansCritere(criteres))
{
_formatsDuFonds = _livres.Select(l => l.Format).ToHashSet();
+ _typesDuFonds = _livres.Select(l => l.TypeDocument).ToHashSet();
}
}
catch (OperationCanceledException)
diff --git a/MaBibli.Client/Pages/FicheLivre.razor b/MaBibli.Client/Pages/FicheLivre.razor
index f80680d..433892a 100644
--- a/MaBibli.Client/Pages/FicheLivre.razor
+++ b/MaBibli.Client/Pages/FicheLivre.razor
@@ -127,6 +127,12 @@ else
{
Format@Libelles.Format(_livre.Format)
}
+
+ @* Idem : « non précisé » est l'absence de réponse, pas une réponse. *@
+ @if (_livre.TypeDocument != TypeDocument.NonPrecise)
+ {
+ Type@Libelles.TypeDocument(_livre.TypeDocument)
+ }
@@ -274,6 +280,7 @@ else
Auteurs = livre.Auteurs.Select(a => a.Nom).ToList(),
Editeur = livre.Editeur,
Format = livre.Format,
+ TypeDocument = livre.TypeDocument,
Statut = livre.Statut,
CoverUrl = livre.CoverUrl,
};
diff --git a/MaBibli.Client/Services/FiltreLivresLocal.cs b/MaBibli.Client/Services/FiltreLivresLocal.cs
index 7598575..46a78ce 100644
--- a/MaBibli.Client/Services/FiltreLivresLocal.cs
+++ b/MaBibli.Client/Services/FiltreLivresLocal.cs
@@ -36,6 +36,11 @@ public static class FiltreLivresLocal
livres = livres.Where(l => l.Format == format);
}
+ if (criteres.TypeDocument is { } type)
+ {
+ livres = livres.Where(l => l.TypeDocument == type);
+ }
+
if (criteres.Statut is { } statut)
{
// LivreDto.Statut est déjà celui de l'utilisateur courant : le serveur l'a résolu
diff --git a/MaBibli.Client/Services/ServiceLivresApi.cs b/MaBibli.Client/Services/ServiceLivresApi.cs
index ac12094..8491bce 100644
--- a/MaBibli.Client/Services/ServiceLivresApi.cs
+++ b/MaBibli.Client/Services/ServiceLivresApi.cs
@@ -76,6 +76,11 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
parametres.Add($"format={format}");
}
+ if (criteres.TypeDocument is { } type)
+ {
+ parametres.Add($"typeDocument={type}");
+ }
+
if (criteres.Statut is { } statut)
{
parametres.Add($"statut={statut}");
diff --git a/MaBibli.Shared/Catalogue/CritereLivres.cs b/MaBibli.Shared/Catalogue/CritereLivres.cs
index 526be08..8c62199 100644
--- a/MaBibli.Shared/Catalogue/CritereLivres.cs
+++ b/MaBibli.Shared/Catalogue/CritereLivres.cs
@@ -25,6 +25,16 @@ public record CritereLivres
/// Physique ou numérique. null = les deux : le catalogue est unique.
public Format? Format { get; init; }
+ ///
+ /// Roman, bande dessinée… null = tous.
+ ///
+ ///
+ /// TypeDocument.NonPrecise est une valeur filtrable : « ce que je n'ai pas
+ /// encore rangé » est justement la question qu'on se pose après avoir introduit ce champ.
+ /// C'est ce qui la distingue du statut de lecture, dont l'absence n'est pas une valeur.
+ ///
+ public TypeDocument? TypeDocument { get; init; }
+
///
/// Statut de lecture de l'utilisateur courant. null = tous.
///
diff --git a/MaBibli.Shared/Catalogue/FiltreLivres.cs b/MaBibli.Shared/Catalogue/FiltreLivres.cs
index 8d13a59..8befa00 100644
--- a/MaBibli.Shared/Catalogue/FiltreLivres.cs
+++ b/MaBibli.Shared/Catalogue/FiltreLivres.cs
@@ -34,6 +34,11 @@ public static class FiltreLivres
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,
diff --git a/MaBibli.Shared/Dtos/EnregistrementLivre.cs b/MaBibli.Shared/Dtos/EnregistrementLivre.cs
index 15114d3..d56e7d3 100644
--- a/MaBibli.Shared/Dtos/EnregistrementLivre.cs
+++ b/MaBibli.Shared/Dtos/EnregistrementLivre.cs
@@ -34,6 +34,9 @@ public record EnregistrementLivre
public Format Format { get; set; }
+ /// Roman, bande dessinée… Facultatif : « non précisé » est une réponse valable.
+ public TypeDocument TypeDocument { get; set; }
+
///
/// Statut de lecture de l'appelant. null = non commencé, et aucune ligne n'est
/// alors écrite en base.
diff --git a/MaBibli.Shared/Dtos/LivreDto.cs b/MaBibli.Shared/Dtos/LivreDto.cs
index 2b2c415..ce84b57 100644
--- a/MaBibli.Shared/Dtos/LivreDto.cs
+++ b/MaBibli.Shared/Dtos/LivreDto.cs
@@ -34,6 +34,11 @@ public record LivreDto
public required Format Format { get; init; }
+ ///
+ /// Roman, bande dessinée… NonPrecise par défaut, qui ne veut pas dire « roman ».
+ ///
+ public TypeDocument TypeDocument { get; init; }
+
///
/// Statut de lecture de l'utilisateur courant, ou null s'il n'en a jamais posé
/// (« non commencé »).
diff --git a/MaBibli.Shared/Entites/Livre.cs b/MaBibli.Shared/Entites/Livre.cs
index e0fce68..9bae6ef 100644
--- a/MaBibli.Shared/Entites/Livre.cs
+++ b/MaBibli.Shared/Entites/Livre.cs
@@ -27,6 +27,16 @@ public class Livre
public Format Format { get; set; }
+ ///
+ /// Roman, bande dessinée… Indépendant du format : une BD numérique existe.
+ ///
+ ///
+ /// Par défaut , qui ne veut pas dire « roman » : voir
+ /// l'énumération. L'interface n'affiche donc une étiquette que lorsque le type a été
+ /// choisi, comme elle n'affiche le format que pour les ebooks.
+ ///
+ public TypeDocument TypeDocument { get; set; }
+
/// URL de la couverture (OpenLibrary), pas de fichier stocké localement.
public string? CoverUrl { get; set; }
diff --git a/MaBibli.Shared/Entites/TypeDocument.cs b/MaBibli.Shared/Entites/TypeDocument.cs
new file mode 100644
index 0000000..59c09d1
--- /dev/null
+++ b/MaBibli.Shared/Entites/TypeDocument.cs
@@ -0,0 +1,34 @@
+namespace MaBibli.Shared.Entites;
+
+///
+/// Nature de l'ouvrage, indépendante de son support (voir ).
+///
+///
+/// Une BD est déjà catalogable telle quelle — elle a un ISBN et la BnF la connaît. Ce qui
+/// manquait était de pouvoir la distinguer : dit physique ou
+/// numérique, pas roman ou bande dessinée.
+///
+/// ⚠️ est la valeur par défaut, et ce n'est pas « roman ». Les
+/// fiches saisies avant l'arrivée de ce champ ne l'ont pas été avec cette question en tête : les
+/// déclarer romans d'office affirmerait quelque chose qu'on ne sait pas. Même raisonnement que
+/// l'absence de ligne de statut, qui vaut « non commencé » et non « à lire ».
+///
+///
+/// La liste est volontairement courte. Une valeur s'ajoute sans migration — une énumération est
+/// stockée en entier — alors que renommer ou fusionner des valeurs déjà posées sur des centaines
+/// de fiches, non. Dans le doute, on n'ajoute pas.
+///
+///
+/// ⚠️ Les périodiques n'y figurent pas : leur catalogage demande un ISSN et une table de
+/// numéros, pas une étiquette de plus. Voir IDEES.md.
+///
+///
+public enum TypeDocument
+{
+ /// On ne sait pas, et on ne prétend pas le savoir. Valeur par défaut.
+ NonPrecise = 0,
+
+ Roman = 1,
+
+ BandeDessinee = 2,
+}
diff --git a/MaBibli.Tests/FiltreLivresLocalTests.cs b/MaBibli.Tests/FiltreLivresLocalTests.cs
index e271dcb..8ca7e16 100644
--- a/MaBibli.Tests/FiltreLivresLocalTests.cs
+++ b/MaBibli.Tests/FiltreLivresLocalTests.cs
@@ -30,9 +30,14 @@ public class FiltreLivresLocalTests
private static readonly Auteur Zola = Auteur(1, "Émile Zola");
private static readonly Auteur Maupassant = Auteur(2, "Guy de Maupassant");
- private static Livre Livre(int id, string titre, Auteur? auteur, Format format, Statut? statut)
+ private static Livre Livre(
+ int id, string titre, Auteur? auteur, Format format, Statut? statut,
+ TypeDocument type = TypeDocument.NonPrecise)
{
- var livre = new Livre { Id = id, Titre = titre, Format = format, AjoutePar = Lecteur };
+ var livre = new Livre
+ {
+ Id = id, Titre = titre, Format = format, TypeDocument = type, AjoutePar = Lecteur,
+ };
livre.RecalculerFormes();
if (auteur is not null)
@@ -50,9 +55,9 @@ public class FiltreLivresLocalTests
private static readonly Livre[] Entites =
[
- Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu),
- Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire),
- Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours),
+ Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, TypeDocument.Roman),
+ 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(4, "Bel-Ami", Maupassant, Format.Numerique, null),
Livre(5, "Œuvres complètes", Zola, Format.Physique, Statut.ALire),
Livre(6, "L'Éducation sentimentale", null, Format.Physique, null),
@@ -68,6 +73,7 @@ public class FiltreLivresLocalTests
Id = l.Id,
Titre = l.Titre,
Format = l.Format,
+ TypeDocument = l.TypeDocument,
DateAjout = DateTime.UtcNow,
Statut = l.Statuts.FirstOrDefault(s => s.Utilisateur == Lecteur)?.Statut,
Auteurs = l.Auteurs
@@ -89,12 +95,20 @@ public class FiltreLivresLocalTests
new CritereLivres { Recherche = "introuvable" },
new CritereLivres { Format = Format.Physique },
new CritereLivres { Format = Format.Numerique },
+ new CritereLivres { TypeDocument = TypeDocument.Roman },
+ new CritereLivres { TypeDocument = TypeDocument.BandeDessinee },
+ // « Non précisé » est une valeur filtrable, pas l'absence de filtre.
+ new CritereLivres { TypeDocument = TypeDocument.NonPrecise },
new CritereLivres { Statut = Statut.Lu },
new CritereLivres { Statut = Statut.ALire },
new CritereLivres { AuteurId = 1 },
new CritereLivres { AuteurId = 2 },
new CritereLivres { AuteurId = 99 },
new CritereLivres { Recherche = "zola", Format = Format.Physique, Statut = Statut.ALire },
+ new CritereLivres
+ {
+ Recherche = "zola", TypeDocument = TypeDocument.BandeDessinee, Format = Format.Numerique,
+ },
];
[Theory]
diff --git a/MaBibli.Tests/ServiceCatalogueTests.cs b/MaBibli.Tests/ServiceCatalogueTests.cs
index f196f36..4d69944 100644
--- a/MaBibli.Tests/ServiceCatalogueTests.cs
+++ b/MaBibli.Tests/ServiceCatalogueTests.cs
@@ -348,6 +348,48 @@ public class ServiceCatalogueTests : IDisposable
Assert.Equal(["Guy de Maupassant"], relu.Auteurs.Select(a => a.Nom));
}
+ [Fact]
+ public async Task Un_livre_sans_type_precise_reste_non_precise()
+ {
+ var livre = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
+
+ // ⚠️ Le défaut n'est PAS « Roman » : on ne remplit pas une case à la place de
+ // l'utilisateur, et l'interface n'affiche que ce qui a été choisi.
+ Assert.Equal(TypeDocument.NonPrecise, livre.TypeDocument);
+ }
+
+ [Fact]
+ public async Task Le_type_de_document_est_enregistre_puis_modifiable()
+ {
+ var saisie = Saisie("Les fourmis", "Bernard Werber");
+ saisie.TypeDocument = TypeDocument.BandeDessinee;
+
+ var cree = (await _service.CreerAsync(saisie, "mathieu")).Livre!;
+ Assert.Equal(TypeDocument.BandeDessinee, cree.TypeDocument);
+
+ saisie.TypeDocument = TypeDocument.Roman;
+ var modifie = (await _service.ModifierAsync(cree.Id, saisie, "mathieu")).Livre!;
+
+ Assert.Equal(TypeDocument.Roman, modifie.TypeDocument);
+ }
+
+ [Fact]
+ public async Task Le_catalogue_se_filtre_par_type_de_document()
+ {
+ var bd = Saisie("Les fourmis", "Bernard Werber");
+ bd.TypeDocument = TypeDocument.BandeDessinee;
+ await _service.CreerAsync(bd, "mathieu");
+ await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
+
+ var bds = await _service.ListerAsync(
+ new CritereLivres { TypeDocument = TypeDocument.BandeDessinee }, "mathieu");
+ var nonPrecises = await _service.ListerAsync(
+ new CritereLivres { TypeDocument = TypeDocument.NonPrecise }, "mathieu");
+
+ Assert.Equal(["Les fourmis"], bds.Select(l => l.Titre));
+ Assert.Equal(["Germinal"], nonPrecises.Select(l => l.Titre));
+ }
+
// ─────────────────────────────────────────────────────────────────────────
// Doublons : un AVERTISSEMENT, jamais un refus. Posséder deux exemplaires est légitime —
// on garde le sien et on prête l'autre — et l'ISBN est facultatif, donc il ne peut pas