diff --git a/MaBibli.Client/Composants/GroupeCatalogue.razor b/MaBibli.Client/Composants/GroupeCatalogue.razor index 1ec6c05..1dfc0b5 100644 --- a/MaBibli.Client/Composants/GroupeCatalogue.razor +++ b/MaBibli.Client/Composants/GroupeCatalogue.razor @@ -43,7 +43,8 @@ } @@ -53,9 +54,24 @@ } @@ -85,6 +101,19 @@ /// [Parameter] public IReadOnlyDictionary? Totaux { get; set; } + /// Modification en lot du catalogue : voir la remarque équivalente sur Catalogue.razor. + [Parameter] public bool LotActif { get; set; } + + [Parameter] public ISet? SelectionLot { get; set; } + + /// + /// ⚠️ EventCallback, et non un Action brut : coché depuis un tome niché dans ce bloc, l'écran + /// du catalogue (le VRAI propriétaire de la sélection) ne serait sinon jamais prévenu du + /// changement, et sa barre de sélection resterait figée sur l'état d'avant — le second piège + /// Blazor de CLAUDE.md. + /// + [Parameter] public EventCallback<(int LivreId, bool Coche)> OnSelectionner { get; set; } + private (int Possedes, int Total) Compte => Totaux is not null && Totaux.TryGetValue(Entree.Serie!.Id, out var connu) ? connu diff --git a/MaBibli.Client/Pages/Catalogue.razor b/MaBibli.Client/Pages/Catalogue.razor index 5215618..315124a 100644 --- a/MaBibli.Client/Pages/Catalogue.razor +++ b/MaBibli.Client/Pages/Catalogue.razor @@ -40,6 +40,20 @@ } + @* + « Modifier en lot » n'apparaît qu'à partir de DEUX livres — même seuil que sur la fiche + série. Reste possible hors-ligne : l'écriture, elle, est bloquée par livre comme + partout ailleurs (⚠️ MotifBlocage). + *@ + @if (Reseau.EnLigne && _livres is { Count: > 1 }) + { + + } + @if (Reseau.EnLigne) { Ajouter un ouvrage @@ -226,6 +240,47 @@ else if (_livres is not null) {

@_livres.Count livre@(_livres.Count > 1 ? "s" : "")

+ @* + Modification en lot : même patron que la fiche série (barre collante, formulaire affiché + sous elle) — sauf que la sélection porte ici sur le catalogue ENTIER, groupé ou non, d'où + le bouton « Tout sélectionner » qui coche d'un coup tous les livres actuellement chargés + (donc déjà filtrés : cocher les 400 livres du fonds derrière un filtre à trois résultats + n'aurait aucun sens). + *@ + @if (_lotOuvert) + { +
+ @if (_selectionLot.Count > 0) + { +
+ + @_selectionLot.Count sélectionné@(_selectionLot.Count > 1 ? "s" : "") + + + +
+ + + } + else + { +

+ Cochez les livres à modifier ensemble, ou + . +

+ } +
+ } + @* Les tomes d'une série tiennent sous son nom, à la place qu'occuperait le premier d'entre eux : l'ordre général du catalogue ne bouge pas, on ne perd donc pas un livre qu'on @@ -247,7 +302,21 @@ else if (_livres is not null) { @if (entree.EstGroupe) { - + + } + else if (_lotOuvert) + { +
  • + + +
  • } else { @@ -348,6 +417,18 @@ else if (_livres is not null) private bool _filtresOuverts; private string? _erreur; + /// Le panneau de modification en lot est déplié. Même patron que la fiche série. + private bool _lotOuvert; + + /// Identifiants des livres cochés pour la modification en lot. + private readonly HashSet _selectionLot = []; + + private SaisieLot _saisieLot = new(); + private bool _enCoursLot; + private string? _erreurLot; + + private string? MotifBlocageLot => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne; + /// /// Formats réellement présents dans le fonds, ou null tant qu'on ne le sait pas. /// @@ -606,6 +687,110 @@ else if (_livres is not null) } } + /// + /// Ouvre ou ferme le panneau de modification en lot. Le refermer vide aussi la sélection et + /// la saisie en cours — même règle que sur la fiche série : un panneau qui se rouvrirait sur + /// l'état d'avant laisserait croire à une saisie encore active. + /// + private void BasculerLot() + { + _lotOuvert = !_lotOuvert; + _erreurLot = null; + _selectionLot.Clear(); + _saisieLot = new SaisieLot(); + } + + private void SelectionnerLot(int livreId, bool selectionne) + { + if (selectionne) + { + _selectionLot.Add(livreId); + } + else + { + _selectionLot.Remove(livreId); + } + } + + /// + /// Coche d'un coup tous les livres actuellement CHARGÉS — donc déjà filtrés. Un groupe replié + /// n'y échappe pas : la sélection porte sur _livres, pas sur ce que l'écran affiche. + /// + private void ToutSelectionnerLot() + { + foreach (var livre in _livres ?? []) + { + _selectionLot.Add(livre.Id); + } + } + + private void AnnulerLot() + { + _selectionLot.Clear(); + _saisieLot = new SaisieLot(); + _erreurLot = null; + } + + /// + /// Applique la saisie en lot à chacun des livres cochés, un appel à la fois — même patron que + /// la fiche série : pas de point d'entrée dédié, la boucle réutilise + /// , en ne remplaçant que les champs cochés et en + /// reprenant tels quels ceux qui ne le sont pas. + /// + private async Task EnregistrerLotAsync() + { + _enCoursLot = true; + _erreurLot = null; + + var erreurs = new List(); + + foreach (var livreId in _selectionLot) + { + var livre = (_livres ?? []).FirstOrDefault(l => l.Id == livreId); + if (livre is null) + { + continue; + } + + var saisie = new EnregistrementLivre + { + Isbn = livre.Isbn, + Titre = livre.Titre, + Auteurs = _saisieLot.ModifierAuteurs + ? _saisieLot.Auteurs + : livre.Auteurs.Select(a => new AuteurSaisi(a.Nom, a.Role)).ToList(), + Themes = _saisieLot.ModifierThemes ? _saisieLot.Themes : [.. livre.Themes], + Editeur = _saisieLot.ModifierEditeur ? _saisieLot.Editeur : livre.Editeur, + Format = _saisieLot.ModifierFormat ? _saisieLot.Format : livre.Format, + TypeDocument = _saisieLot.ModifierType ? _saisieLot.TypeDocument : livre.TypeDocument, + Statut = _saisieLot.ModifierStatut ? _saisieLot.Statut : livre.Statut, + NombrePages = livre.NombrePages, + CoverUrl = livre.CoverUrl, + UrlNotice = livre.UrlNotice, + }; + + var resultat = await Api.ModifierAsync(livre.Id, saisie); + if (!resultat.EstOk) + { + erreurs.Add(resultat.Erreur!); + } + } + + _enCoursLot = false; + + if (erreurs.Count > 0) + { + _erreurLot = string.Join(" ", erreurs.Distinct()); + await ChargerAsync(); + return; + } + + _lotOuvert = false; + _selectionLot.Clear(); + _saisieLot = new SaisieLot(); + await ChargerAsync(); + } + public void Dispose() { Reseau.Change -= SurChangementReseau;