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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user