}
@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.
*@
Rapprochements possibles
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.
@* Une fusion est une écriture : elle demande le réseau, comme le reste. *@
}
}
@if (_auteurs is null)
{
Chargement…
}
else if (_auteurs.Count == 0)
{
Aucun auteur : la bibliothèque est vide.
}
else
{
@*
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 ».
*@
@* Le compteur dit ce qu'on regarde : sur une liste filtrée, afficher le total ferait
croire que la recherche n'a rien fait. *@
@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.
*@
@auteur.NombreDeLivres
@Livres(auteur.NombreDeLivres)@auteur.Nom
@* Le declencheur decrit dans CLAUDE.md : depuis un auteur qu'on a deja, voir
tout ce qu'il a ecrit et marquer ce qui manque. *@
Bibliographie
@if (_auteurRenomme == auteur.Id)
{
}
else
{
}
}
}
@if (_collisionRenommage is not null)
{
Un auteur existe déjà sous le nom « @_collisionRenommage.Nom ».
Les livres seront regroupés sous cette fiche.
Confirmer la fusion ?
}
@code {
/// Les deux façons de trier une liste d'auteurs, et il n'y en a pas de troisième.
private enum TriAuteurs
{
/// Ordre alphabétique : « où est untel ? ».
Nom = 0,
/// Du plus présent au moins présent : « qui ai-je le plus ? ».
NombreDeLivres = 1,
}
private IReadOnlyList? _auteurs;
private string _recherche = string.Empty;
private TriAuteurs _tri = TriAuteurs.Nom;
private IReadOnlyList? _suggestions;
private bool _enCours;
private bool _renommageEnCours;
private int? _auteurRenomme;
private string _nomRenomme = string.Empty;
private AuteurDto? _collisionRenommage;
private string? _erreur;
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
private void SurChangementReseau() => _ = InvokeAsync(async () =>
{
await ChargerAsync();
StateHasChanged();
});
public void Dispose() => Reseau.Change -= SurChangementReseau;
/// Raison du blocage des fusions, ou null en ligne.
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
protected override Task OnInitializedAsync() => ChargerAsync();
private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";
private void SurRecherche(ChangeEventArgs e) => _recherche = e.Value?.ToString() ?? string.Empty;
///
/// La liste telle qu'elle s'affiche : filtrée par la recherche, puis triée.
///
///
/// La recherche est une sous-chaîne du nom normalisé, 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.
///
/// Le tri par nom est ordinal sur la forme normalisée, pour ne pas dépendre de la
/// culture du navigateur : c'est la même règle que FiltreLivresLocal, et elle évite
/// qu'un appareil classe « Éluard » avant « Duras » et un autre après.
///
///
private IReadOnlyList 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()
{
_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 void CommencerRenommage(AuteurDto auteur)
{
_auteurRenomme = auteur.Id;
_nomRenomme = auteur.Nom;
_collisionRenommage = null;
_erreur = null;
}
private void AnnulerRenommage()
{
_auteurRenomme = null;
_collisionRenommage = null;
_nomRenomme = string.Empty;
}
private Task RenommerAsync() => EnvoyerRenommageAsync(false);
private Task ConfirmerFusionAsync() => EnvoyerRenommageAsync(true);
private async Task EnvoyerRenommageAsync(bool confirmerFusion)
{
if (_auteurRenomme is null || string.IsNullOrWhiteSpace(_nomRenomme))
{
_erreur = "Le nom de l'auteur est obligatoire.";
return;
}
_renommageEnCours = true;
_erreur = null;
var resultat = await Api.RenommerAuteurAsync(
_auteurRenomme.Value, _nomRenomme, confirmerFusion);
_renommageEnCours = false;
if (resultat is null)
{
_erreur = "Le renommage n'a pas pu être enregistré.";
return;
}
if (resultat.Collision is not null)
{
_collisionRenommage = resultat.Collision;
return;
}
AnnulerRenommage();
await ChargerAsync();
}
private async Task DeciderAsync(Func> 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();
}
}