@page "/auteurs" @inject ServiceLivresApi Api @inject EtatReseau Reseau @implements IDisposable MaBibli — auteurs

Auteurs

@if (_erreur is not null) { } @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.

} @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. *@

@Affiches.Count auteur@(Affiches.Count > 1 ? "s" : "") @if (Affiches.Count != _auteurs.Count) { sur @_auteurs.Count }

@if (Affiches.Count == 0) {

Aucun auteur ne correspond à « @_recherche ».

} } @if (_collisionRenommage is not null) { } @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(); } }