diff --git a/MaBibli.Client/Composants/FormulaireLotLivres.razor b/MaBibli.Client/Composants/FormulaireLotLivres.razor
new file mode 100644
index 0000000..8281198
--- /dev/null
+++ b/MaBibli.Client/Composants/FormulaireLotLivres.razor
@@ -0,0 +1,226 @@
+@inject ServiceLivresApi Api
+
+@*
+ Panneau de modification en lot, ouvert depuis la fiche série une fois des tomes cochés.
+
+ ⚠️ Reprend le DESIGN de FormulaireLivre.razor (mêmes composants, même ordre de champs), mais
+ chaque champ est précédé d'une case « Modifier… » : sans elle, la valeur par défaut d'un champ
+ (« NonPrecise », « Physique »…) est déjà une vraie valeur, et l'appliquer à tous les livres
+ sélectionnés écraserait un champ qu'on ne voulait pas toucher (CLAUDE.md, invariant 10).
+ Un champ non coché ne s'affiche pas — c'est ce qui rend visible ce qui va être écrasé.
+*@
+
+
+ Les champs cochés remplacent la valeur existante sur chacun des @NombreLivres livres
+ sélectionnés — pour les auteurs, une liste vide les retire tous.
+
+ }
+
@*
⚠️ L'ordre est STOCKÉ, jamais déduit d'une année de publication : une préquelle se
lit avant le livre paru dix ans plus tôt. Les flèches sont le seul moyen de le
@@ -342,6 +388,17 @@ else
@foreach (var (element, index) in TomesAffiches.Select((e, i) => (e, i)))
{
@if (ModeOrdre)
{
@@ -641,6 +698,14 @@ else
private bool _enCours;
private string? _erreur;
+ /// Le panneau de modification en lot est déplié.
+ private bool _lotOuvert;
+
+ /// Identifiants des LIVRES (pas des places) cochés pour la modification en lot.
+ private readonly HashSet _selectionLot = [];
+
+ private SaisieLot _saisieLot = new();
+
private string _titre = string.Empty;
private int _parenteId;
private bool _confirmeSuppression;
@@ -765,6 +830,11 @@ else
{
Ouvrir(VoieAjout.Aucune);
}
+ else
+ {
+ // Les deux panneaux se marcheraient dessus sur un petit écran : un seul à la fois.
+ _lotOuvert = false;
+ }
}
/// Vrai quand ce qui est tapé dans la saisie manuelle est un code et non un titre.
@@ -860,6 +930,116 @@ else
/// Ni édition de la fiche, ni réordonnancement : l'écran qu'on ouvre pour regarder.
private bool ModeConsultation => !ModeEdition && !ModeOrdre;
+ ///
+ /// Le panneau de lot n'a de sens qu'en consultation : la case à cocher se confondrait avec
+ /// les flèches de l'écran d'ordre, qui reste dédié au déplacement des tomes.
+ ///
+ private bool LotActif => _lotOuvert && ModeConsultation;
+
+ /// Tomes de la série effectivement rattachés à un livre du catalogue local.
+ private int TomesModifiablesEnLot =>
+ _serie?.Elements.Count(e => e.LivreId is { } id && _livresCatalogue.Any(l => l.Id == id)) ?? 0;
+
+ ///
+ /// 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 : un panneau qui se
+ /// rouvrirait sur l'état d'avant laisserait croire à une saisie encore active.
+ ///
+ private void BasculerLot()
+ {
+ _lotOuvert = !_lotOuvert;
+ _erreur = null;
+ _selectionLot.Clear();
+ _saisieLot = new SaisieLot();
+
+ // Les deux panneaux se marcheraient dessus sur un petit écran : un seul à la fois.
+ if (_lotOuvert)
+ {
+ _ajoutOuvert = false;
+ Ouvrir(VoieAjout.Aucune);
+ }
+ }
+
+ private void SelectionnerLot(int livreId, bool selectionne)
+ {
+ if (selectionne)
+ {
+ _selectionLot.Add(livreId);
+ }
+ else
+ {
+ _selectionLot.Remove(livreId);
+ }
+ }
+
+ private void AnnulerLot()
+ {
+ _selectionLot.Clear();
+ _saisieLot = new SaisieLot();
+ _erreur = null;
+ }
+
+ ///
+ /// Applique la saisie en lot à chacun des livres cochés, un appel à la fois — même patron que
+ /// MasquerSelectionAsync de la bibliographie : 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()
+ {
+ _enCours = true;
+ _erreur = null;
+
+ var erreurs = new List();
+
+ foreach (var livreId in _selectionLot)
+ {
+ var livre = _livresCatalogue.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!);
+ }
+ }
+
+ _enCours = false;
+
+ if (erreurs.Count > 0)
+ {
+ _erreur = string.Join(" ", erreurs.Distinct());
+ await ChargerAsync();
+ return;
+ }
+
+ _lotOuvert = false;
+ _selectionLot.Clear();
+ _saisieLot = new SaisieLot();
+ _message = "Les livres sélectionnés ont été modifiés.";
+ await ChargerAsync();
+ }
+
private bool Suffixe(string fin) =>
Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/')
.EndsWith(fin, StringComparison.OrdinalIgnoreCase);
diff --git a/MaBibli.Client/wwwroot/css/app.css b/MaBibli.Client/wwwroot/css/app.css
index b8cb6e0..77e91f5 100644
--- a/MaBibli.Client/wwwroot/css/app.css
+++ b/MaBibli.Client/wwwroot/css/app.css
@@ -164,6 +164,11 @@ body {
.entete-fiche-actions {
flex: 0 0 auto;
+ /* Sans elle, un flex-basis « auto » prend la largeur de TOUS les boutons sur une seule
+ ligne (leur taille de contenu), et le flex-wrap ci-dessous ne peut jamais s'en servir :
+ la ligne déborde au lieu de se replier. Quatre boutons (dont « Modifier en lot ») y
+ tiennent tout juste à 1280px, mais débordent à 375px sans ce plafond. */
+ max-width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;