Implémente C1 à C5 pour les séries et cycles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
LIMONIER Mathieu
2026-08-20 13:27:49 +02:00
co-authored by Copilot
parent 3d5e73838e
commit ddc60898da
12 changed files with 534 additions and 45 deletions
+199 -8
View File
@@ -34,7 +34,8 @@ else
}
else
{
<text>@_serie.NombrePossedes sur @_serie.Elements.Count tomes dans la bibliothèque.</text>
var total = Total(_serie);
<text>@total.Possedes sur @total.Total tomes dans la bibliothèque.</text>
}
</p>
@@ -50,19 +51,19 @@ else
<p class="message-avertissement" role="status">@_message</p>
}
@* Les sous-séries d'un cycle : on les liste, on ne les déplie pas — chacune a son écran. *@
@* L'arbre est rendu récursivement ; le chemin protège le rendu si une base ancienne contient
malgré tout une boucle. *@
@if (!ModeEdition && _filles.Count > 0)
{
<h2 class="titre-section">Séries de ce cycle</h2>
<ul class="liste-sous-series">
@foreach (var fille in _filles)
{
<li @key="fille.Id">
<a href="@($"series/{fille.Id}")">@fille.Titre</a>
<span class="carte-details">
@fille.NombrePossedes sur @fille.Elements.Count
</span>
</li>
<ArbreSerie Serie="fille" Toutes="_toutes" Livres="_livresCatalogue"
Totaux="_totaux" Deplies="_deplies"
Chemin="@CheminRacine"
ActionsActives="Reseau.EnLigne" MotifBlocage="@MotifBlocage"
OnDeplacer="DeplacerSousSerieAsync" />
}
</ul>
}
@@ -149,6 +150,12 @@ else
@onclick="() => OuvrirRattachement(element.Id)">
Je l'ai
</button>
<button type="button" class="bouton bouton-compact"
disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
@onclick="() => OuvrirLookup(element)">
Ajouter par ISBN
</button>
}
else
{
@@ -183,6 +190,47 @@ else
}
</div>
}
@if (_lookupElement?.Id == element.Id)
{
<div class="recherche-livre">
<input class="champ-saisie" type="text" inputmode="numeric"
placeholder="ISBN du tome…"
@bind="_lookupIsbn" @bind:event="oninput"
@onkeydown="SurToucheLookup" />
<button type="button" class="bouton bouton-principal"
disabled="@(_enCours || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_lookupIsbn))"
title="@MotifBlocage" @onclick="ChercherLookupAsync">
@(_lookupEnCours ? "Recherche…" : "Préremplir")
</button>
@if (_lookupCandidats.Count > 0)
{
@foreach (var candidat in _lookupCandidats)
{
<div class="carte-candidat">
<div class="carte-corps">
<p class="carte-titre">@candidat.Titre</p>
<p class="carte-auteur">@candidat.Auteur</p>
<p class="carte-details">
@(candidat.Editeur ?? "éditeur inconnu")
· @(candidat.Annee ?? "année inconnue")
</p>
<button type="button" class="bouton bouton-principal"
disabled="@_enCours"
@onclick="() => AjouterCommeLivreAsync(candidat)">
Ajouter comme livre
</button>
<button type="button" class="bouton bouton-discret"
disabled="@_enCours"
@onclick="() => AjouterCommeEnvieAsync(candidat)">
Ajouter comme envie
</button>
</div>
</div>
}
}
</div>
}
</div>
</li>
}
@@ -312,6 +360,9 @@ else
private SerieDto? _serie;
private SerieDto? _parente;
private List<SerieDto> _filles = [];
private IReadOnlyDictionary<int, (int Possedes, int Total)> _totaux =
new Dictionary<int, (int, int)>();
private readonly HashSet<int> _deplies = [];
private bool _chargement = true;
private bool _enCours;
@@ -326,8 +377,13 @@ else
private int? _rattachement;
private string _recherche = string.Empty;
private IReadOnlyList<LivreDto> _resultats = [];
private ElementSerieDto? _lookupElement;
private string _lookupIsbn = string.Empty;
private IReadOnlyList<CandidatLivre> _lookupCandidats = [];
private bool _lookupEnCours;
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
private IReadOnlySet<int> CheminRacine => new HashSet<int> { Id };
private bool ModeEdition =>
Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/')
@@ -410,6 +466,7 @@ else
: null;
_filles = _toutes.Where(s => s.SerieParenteId == Id).OrderBy(s => s.Position).ToList();
_totaux = TotauxSeries.Calculer(_toutes);
_titre = _serie.Titre;
_parenteId = _serie.SerieParenteId ?? 0;
@@ -478,6 +535,140 @@ else
}
}
private (int Possedes, int Total) Total(SerieDto serie) =>
_totaux.TryGetValue(serie.Id, out var total)
? total
: (serie.NombrePossedes, serie.Elements.Count);
private async Task DeplacerSousSerieAsync(DeplacementSerie mouvement)
{
var soeurs = _toutes
.Where(s => s.SerieParenteId == mouvement.ParenteId)
.OrderBy(s => s.Position)
.ThenBy(s => s.Id)
.ToList();
var index = soeurs.FindIndex(s => s.Id == mouvement.SerieId);
var cible = index + mouvement.Delta;
if (index < 0 || cible < 0 || cible >= soeurs.Count)
{
return;
}
var ids = soeurs.Select(s => s.Id).ToList();
(ids[index], ids[cible]) = (ids[cible], ids[index]);
await EcrireAsync(() => Api.ReordonnerSousSeriesAsync(mouvement.ParenteId, ids));
if (_erreur is null)
{
await ChargerAsync();
}
}
private void OuvrirLookup(ElementSerieDto element)
{
_lookupElement = element;
_lookupIsbn = string.Empty;
_lookupCandidats = [];
_erreur = null;
}
private async Task SurToucheLookup(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_lookupIsbn))
{
await ChercherLookupAsync();
}
}
private async Task ChercherLookupAsync()
{
_lookupEnCours = true;
_erreur = null;
_lookupCandidats = [];
var resultat = await Api.ChercherIsbnAsync(_lookupIsbn);
_lookupEnCours = false;
if (resultat is null)
{
_erreur = Reseau.EnLigne
? "La recherche n'a rien pu ramener."
: EtatReseau.MotifHorsLigne;
return;
}
if (resultat.Periodique is not null)
{
_erreur = "Les revues ne peuvent pas être rattachées à une série.";
return;
}
_lookupCandidats = resultat.Candidats;
if (_lookupCandidats.Count == 0)
{
_erreur = "Aucune notice ne correspond à cet ISBN.";
}
}
private async Task AjouterCommeEnvieAsync(CandidatLivre candidat)
{
_enCours = true;
_erreur = null;
var resultat = await Api.AjouterSouhaitAsync(new EnregistrementSouhait
{
Titre = candidat.Titre,
Auteur = candidat.Auteur,
Editeur = candidat.Editeur,
Annee = candidat.Annee,
Isbn = candidat.IsbnInterroge,
CoverUrl = candidat.CoverUrl,
});
_enCours = false;
_erreur = resultat.Erreur;
if (resultat.EstOk)
{
_message = $"« {candidat.Titre} » est dans votre liste d'envies.";
_lookupElement = null;
}
}
private async Task AjouterCommeLivreAsync(CandidatLivre candidat)
{
if (_lookupElement is null)
{
return;
}
_enCours = true;
_erreur = null;
var saisie = new EnregistrementLivre
{
Isbn = candidat.IsbnInterroge,
Titre = candidat.Titre,
Auteurs = candidat.Auteurs.Select(a => new AuteurSaisi(a)).ToList(),
Editeur = candidat.Editeur,
CoverUrl = candidat.CoverUrl,
};
var resultat = await Api.CreerAsync(saisie);
_enCours = false;
if (resultat.Doublons is not null)
{
_erreur = resultat.Doublons.Message;
return;
}
if (!resultat.EstOk)
{
_erreur = resultat.Erreur;
return;
}
await EcrireAsync(() => Api.RattacherLivreSerieAsync(_lookupElement.Id, resultat.Livre!.Id));
if (_erreur is null)
{
_lookupElement = null;
}
}
private string? _message;
private async Task DeplacerAsync(int depuis, int vers)