Fusionner la phase base de donnees (statut par utilisateur, table Auteur)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-18 02:24:09 +02:00
co-authored by Claude Opus 5
38 changed files with 3377 additions and 168 deletions
+129
View File
@@ -0,0 +1,129 @@
@page "/auteurs"
@inject ServiceLivresApi Api
<PageTitle>MaBibli — auteurs</PageTitle>
<h1 class="titre-page">Auteurs</h1>
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@if (_suggestions is { Count: > 0 })
{
@*
Ce qui arrive ici est ce que la machine REFUSE de trancher. Les variantes sûres — casse,
accents, ordre « Nom, Prénom », initiales abrégées — ont déjà été réunies à l'écriture et
n'apparaissent jamais dans cette liste. Restent les cas ambigus, typiquement un patronyme
seul face à un nom complet : deux personnes d'une même famille écrivent aussi des livres.
*@
<section class="rapprochements">
<h2 class="titre-section">Rapprochements possibles</h2>
<p class="message-discret">
Ces noms se ressemblent sans qu'on puisse l'affirmer. Un patronyme seul peut désigner
une autre personne de la même famille — à vous de trancher. Une fusion se défait mal.
</p>
<ul class="liste-rapprochements">
@foreach (var suggestion in _suggestions)
{
<li class="carte-rapprochement" @key="(suggestion.Source.Id, suggestion.Cible.Id)">
<p class="rapprochement-noms">
<strong>@suggestion.Source.Nom</strong>
(@Livres(suggestion.Source.NombreDeLivres))
est-il <strong>@suggestion.Cible.Nom</strong>
(@Livres(suggestion.Cible.NombreDeLivres)) ?
</p>
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal" disabled="@_enCours"
@onclick="() => AccepterAsync(suggestion)">
Oui, réunir sous « @suggestion.Cible.Nom »
</button>
<button type="button" class="bouton bouton-discret" disabled="@_enCours"
@onclick="() => RefuserAsync(suggestion)">
Non, deux personnes
</button>
</div>
</li>
}
</ul>
</section>
}
@if (_auteurs is null)
{
<p class="message-discret">Chargement…</p>
}
else if (_auteurs.Count == 0)
{
<p class="message-discret">Aucun auteur : la bibliothèque est vide.</p>
}
else
{
<p class="compteur">@_auteurs.Count auteur@(_auteurs.Count > 1 ? "s" : "")</p>
<ul class="liste-auteurs">
@foreach (var auteur in _auteurs)
{
<li class="carte-auteur-ligne" @key="auteur.Id">
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
<span class="etiquette">@Livres(auteur.NombreDeLivres)</span>
</li>
}
</ul>
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
</nav>
@code {
private IReadOnlyList<AuteurDto>? _auteurs;
private IReadOnlyList<SuggestionRapprochement>? _suggestions;
private bool _enCours;
private string? _erreur;
protected override Task OnInitializedAsync() => ChargerAsync();
private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";
private async Task ChargerAsync()
{
_erreur = null;
try
{
_auteurs = await Api.ListerAuteursAsync();
_suggestions = await Api.ListerRapprochementsAsync();
}
catch (Exception ex)
{
_erreur = $"Impossible de charger les auteurs : {ex.Message}";
}
}
private Task AccepterAsync(SuggestionRapprochement suggestion) =>
DeciderAsync(() => Api.AccepterRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));
private Task RefuserAsync(SuggestionRapprochement suggestion) =>
DeciderAsync(() => Api.RefuserRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));
private async Task DeciderAsync(Func<Task<string?>> action)
{
_enCours = true;
var erreur = await action();
_enCours = false;
if (erreur is not null)
{
_erreur = erreur;
return;
}
// Une fusion peut en rendre d'autres caduques (l'un des deux noms disparaît) : on relit
// la liste complète plutôt que de retirer la ligne traitée à la main.
await ChargerAsync();
}
}
+52 -18
View File
@@ -6,6 +6,15 @@
<h1 class="titre-page">Catalogue</h1>
@if (_auteur is not null)
{
@* Le « cliquer sur un auteur » d'IDEES.md : le catalogue est ici restreint à ses livres. *@
<p class="message-discret">
Livres de <strong>@_auteur.Nom</strong>.
<a href="">Revenir à toute la bibliothèque</a>
</p>
}
<div class="barre-recherche">
<input class="champ-saisie" type="search" placeholder="Titre ou auteur…"
value="@_recherche" @oninput="SurRecherche" aria-label="Rechercher un livre" />
@@ -22,7 +31,9 @@
}
</div>
<div class="segments" role="group" aria-label="Filtrer par statut">
@* Ce filtre porte sur MON statut, pas sur celui du foyer : un livre sans statut pour moi
ne remonte sous aucune valeur, il reste visible sous « Tous ». *@
<div class="segments" role="group" aria-label="Filtrer par mon statut de lecture">
<button type="button" class="segment @(_statut is null ? "segment-actif" : null)"
@onclick="() => FiltrerStatut(null)">Tous</button>
@foreach (var statut in Enum.GetValues<Statut>())
@@ -61,9 +72,15 @@ else if (_livres is not null)
<div class="carte-corps">
<a class="carte-titre" href="@($"livres/{livre.Id}")">@livre.Titre</a>
@if (!string.IsNullOrWhiteSpace(livre.Auteur))
@if (livre.Auteurs.Count > 0)
{
<p class="carte-auteur">@livre.Auteur</p>
<p class="carte-auteur">
@foreach (var (auteur, index) in livre.Auteurs.Select((a, i) => (a, i)))
{
@(index > 0 ? ", " : "")
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
}
</p>
}
<p class="carte-details">
@@ -75,17 +92,10 @@ else if (_livres is not null)
</p>
@* Changement de statut : action la plus fréquente, donc directement dans la liste. *@
<div class="segments segments-compacts" role="group" aria-label="Statut de lecture">
@foreach (var statut in Enum.GetValues<Statut>())
{
<button type="button"
class="segment @Libelles.ClasseStatut(statut) @(livre.Statut == statut ? "segment-actif" : null)"
disabled="@(_statutEnCours == livre.Id)"
@onclick="() => ChangerStatut(livre, statut)">
@Libelles.Statut(statut)
</button>
}
</div>
<ChoixStatut Valeur="livre.Statut"
ValeurChanged="s => ChangerStatut(livre, s)"
Desactive="_statutEnCours == livre.Id"
Compact="true" />
</div>
</li>
}
@@ -95,10 +105,17 @@ else if (_livres is not null)
<nav class="actions-flottantes">
<a class="bouton bouton-principal" href="ajout/isbn">Ajouter par ISBN</a>
<a class="bouton bouton-discret" href="ajout/manuel">Saisie manuelle</a>
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
</nav>
@code {
/// <summary>Restreint le catalogue à un auteur. Vient de « ?auteur=… », posé par les liens de la liste.</summary>
[SupplyParameterFromQuery(Name = "auteur")]
public int? AuteurId { get; set; }
private IReadOnlyList<LivreDto>? _livres;
private AuteurDto? _auteur;
private int? _auteurCharge;
private string _recherche = string.Empty;
private Format? _format;
private Statut? _statut;
@@ -110,9 +127,19 @@ else if (_livres is not null)
private CancellationTokenSource _cts = new();
private bool EstFiltre =>
!string.IsNullOrWhiteSpace(_recherche) || _format is not null || _statut is not null;
!string.IsNullOrWhiteSpace(_recherche) || _format is not null || _statut is not null
|| AuteurId is not null;
protected override Task OnInitializedAsync() => ChargerAsync();
protected override async Task OnParametersSetAsync()
{
if (_auteurCharge != AuteurId)
{
_auteurCharge = AuteurId;
_auteur = AuteurId is { } id ? await Api.ObtenirAuteurAsync(id) : null;
}
await ChargerAsync();
}
private async Task SurRecherche(ChangeEventArgs e)
{
@@ -144,7 +171,14 @@ else if (_livres is not null)
try
{
var criteres = new CritereLivres { Recherche = _recherche, Format = _format, Statut = _statut };
var criteres = new CritereLivres
{
Recherche = _recherche,
Format = _format,
Statut = _statut,
AuteurId = AuteurId,
};
_livres = await Api.ListerAsync(criteres, jeton);
}
catch (OperationCanceledException)
@@ -164,7 +198,7 @@ else if (_livres is not null)
}
}
private async Task ChangerStatut(LivreDto livre, Statut statut)
private async Task ChangerStatut(LivreDto livre, Statut? statut)
{
if (livre.Statut == statut)
{
+4 -1
View File
@@ -97,7 +97,10 @@ else
{
Isbn = _livre.Isbn,
Titre = _livre.Titre,
Auteur = _livre.Auteur,
// Les auteurs sont repris tels que rattachés au livre. Le statut, lui, est celui de
// la personne qui édite : enregistrer ne touche jamais à la lecture de quelqu'un d'autre.
Auteurs = _livre.Auteurs.Select(a => a.Nom).ToList(),
Editeur = _livre.Editeur,
Format = _livre.Format,
Statut = _livre.Statut,