Ajout d'un moyen de modifier par lot les ouvrages dans les séries.

This commit is contained in:
mathieu
2026-09-10 01:08:57 +02:00
parent c5b4e117ba
commit 61d4fb1678
4 changed files with 460 additions and 0 deletions
+180
View File
@@ -74,6 +74,20 @@ else
{
<a class="bouton bouton-compact bouton-discret" href="@($"series/{Id}/ordre")">Changer l'ordre</a>
}
@*
« Modifier en lot » n'apparaît qu'à partir de DEUX tomes possédés : sur un
seul, l'écran de la fiche fait aussi bien. Même seuil que « Changer l'ordre ».
*@
@if (TomesModifiablesEnLot > 1)
{
<button type="button" class="bouton bouton-compact bouton-discret"
aria-expanded="@Aria.Etat(_lotOuvert)" aria-controls="lot-tomes"
@onclick="BasculerLot">
Modifier en lot
</button>
}
<a class="bouton bouton-compact bouton-principal" href="@($"series/{Id}/edition")">Modifier</a>
}
else
@@ -332,6 +346,38 @@ else
}
</div>
@*
Modification en lot : on coche des tomes possédés, puis on choisit ce qu'on leur
applique. Même patron que la sélection de la bibliographie (barre en haut, collante),
sauf que l'action n'est pas un simple bouton mais un formulaire — il reste donc
affiché sous la barre plutôt que de se déclencher au clic.
*@
@if (LotActif)
{
<div id="lot-tomes">
@if (_selectionLot.Count > 0)
{
<div class="barre-selection" role="group" aria-label="Actions sur la sélection">
<span class="selection-compte">
@_selectionLot.Count sélectionné@(_selectionLot.Count > 1 ? "s" : "")
</span>
<button type="button" class="bouton bouton-compact bouton-discret"
disabled="@_enCours" @onclick="() => _selectionLot.Clear()">
Tout décocher
</button>
</div>
<FormulaireLotLivres Saisie="_saisieLot" NombreLivres="_selectionLot.Count"
Erreur="@_erreur" EnCours="_enCours" MessageBlocage="@MotifBlocage"
OnValider="EnregistrerLotAsync" OnAnnuler="AnnulerLot" />
}
else
{
<p class="message-discret">Cochez les tomes à modifier ensemble.</p>
}
</div>
}
@*
⚠️ 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)))
{
<li class="carte-tome @(element.Possede ? null : "tome-manquant")" @key="element.Id">
@if (LotActif && element.LivreId is { } livreLot
&& _livresCatalogue.Any(l => l.Id == livreLot))
{
<label class="oeuvre-case">
<input type="checkbox"
checked="@_selectionLot.Contains(livreLot)"
@onchange="e => SelectionnerLot(livreLot, e.Value is true)" />
<span class="lecture-ecran">Sélectionner « @element.Titre »</span>
</label>
}
<div class="rang-actions">
@if (ModeOrdre)
{
@@ -641,6 +698,14 @@ else
private bool _enCours;
private string? _erreur;
/// <summary>Le panneau de modification en lot est déplié.</summary>
private bool _lotOuvert;
/// <summary>Identifiants des LIVRES (pas des places) cochés pour la modification en lot.</summary>
private readonly HashSet<int> _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;
}
}
/// <summary>Vrai quand ce qui est tapé dans la saisie manuelle est un code et non un titre.</summary>
@@ -860,6 +930,116 @@ else
/// <summary>Ni édition de la fiche, ni réordonnancement : l'écran qu'on ouvre pour regarder.</summary>
private bool ModeConsultation => !ModeEdition && !ModeOrdre;
/// <summary>
/// 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.
/// </summary>
private bool LotActif => _lotOuvert && ModeConsultation;
/// <summary>Tomes de la série effectivement rattachés à un livre du catalogue local.</summary>
private int TomesModifiablesEnLot =>
_serie?.Elements.Count(e => e.LivreId is { } id && _livresCatalogue.Any(l => l.Id == id)) ?? 0;
/// <summary>
/// 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 <see cref="BasculerAjout"/> : un panneau qui se
/// rouvrirait sur l'état d'avant laisserait croire à une saisie encore active.
/// </summary>
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;
}
/// <summary>
/// Applique la saisie en lot à chacun des livres cochés, un appel à la fois — même patron que
/// <c>MasquerSelectionAsync</c> de la bibliographie : pas de point d'entrée dédié, la boucle
/// réutilise <see cref="ServiceLivresApi.ModifierAsync"/>, en ne remplaçant que les champs
/// cochés et en reprenant tels quels ceux qui ne le sont pas.
/// </summary>
private async Task EnregistrerLotAsync()
{
_enCours = true;
_erreur = null;
var erreurs = new List<string>();
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);