diff --git a/MaBibli.Client/Pages/Revue.razor b/MaBibli.Client/Pages/Revue.razor index cf5ef6c..7f04f3f 100644 --- a/MaBibli.Client/Pages/Revue.razor +++ b/MaBibli.Client/Pages/Revue.razor @@ -1,4 +1,5 @@ @page "/revues/{Id:int}" +@page "/revues/{Id:int}/edition" @inject ServiceLivresApi Api @inject NavigationManager Navigation @inject EtatReseau Reseau @@ -14,26 +15,139 @@ } else { -

@_revue.Titre

+ @* + Titre, identité de la revue et actions sur la MÊME ligne (lot W) : disposition + symétrique de celle de la fiche d'une série, et déjà tenue par la bibliographie et la + liste des auteurs. Elle rend les deux actions visibles sans descendre au bas d'une + collection de trente numéros. + *@ +
+
+

@_revue.Titre

-

- @if (!string.IsNullOrWhiteSpace(_revue.Issn)) +

+ @if (!string.IsNullOrWhiteSpace(_revue.Issn)) + { + @* K2 : un ISSN se coupe TOUJOURS au même endroit, contrairement aux tranches + d'un ISBN. Aucune table à embarquer, et aucune coupure ne peut être fausse. *@ + ISSN @FormatageIssn.Afficher(_revue.Issn) + } + @if (!string.IsNullOrWhiteSpace(_revue.Editeur)) + { + @_revue.Editeur + } +

+
+ + @if (!ModeEdition) { - @* K2 : un ISSN se coupe TOUJOURS au même endroit, contrairement aux tranches d'un - ISBN. Aucune table à embarquer, et aucune coupure ne peut être fausse. *@ - ISSN @FormatageIssn.Afficher(_revue.Issn) +
+ @* Ajouter un numéro reste en CONSULTATION, et ce n'est pas une entorse à + « consulter d'abord, modifier ensuite » : le geste ne touche pas à la fiche de + la revue, il range un objet de plus — comme noter un prêt depuis la fiche d'un + livre. Ce sont la modification et le retrait d'un numéro qui vivent, elles, + uniquement dans l'écran d'édition. *@ + + + @if (Reseau.EnLigne) + { + Modifier la revue + } + else + { + + } +
} - @if (!string.IsNullOrWhiteSpace(_revue.Editeur)) - { - @_revue.Editeur - } -

+
@if (_erreur is not null) { } + @if (!ModeEdition && _ajout) + { +

Ajouter un numéro

+ + @* + ⚠️ Le numéro se saisit à la main, et ce n'est pas un oubli : les deux chiffres de + parution du code-barres ne sont PAS un numéro fiable (CLAUDE.md), et l'add-on EAN-2 + qui le porterait vraiment n'a pas pu être vérifié sur un magazine réel. + *@ + @if (_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne)) + { +

+ Le code-barres portait « @NumeroScanne » à côté du code principal, ce qui est + normalement le numéro de parution. Vérifiez-le sur la couverture + avant d'enregistrer. +

+ } + + + +
+ +
+ } + + @if (ModeEdition) + { +

La revue

+ + + + + + + +
+ + + +
+ + @if (_confirmeSuppression) + { +

+ Les @_revue.Numeros.Count numéro(s) recensés disparaîtront avec elle : ils + n'existent que par leur revue. +

+ } + } +

Numéros possédés

@if (_revue.Numeros.Count == 0) @@ -46,12 +160,13 @@ else { @* Du plus récent au plus ancien : devant un kiosque, la question est « ai-je le dernier ? », pas « par quoi ai-je commencé ». C'est l'inverse d'une série, qui suit - son ordre de lecture. *@ + son ordre de lecture. L'ordre est celui rendu par l'API — les numéros sans date + ferment la liste, une date inconnue n'étant pas une date ancienne. *@ } - -

Ajouter un numéro

- - @* - ⚠️ Le numéro se saisit à la main, et ce n'est pas un oubli : les deux chiffres de - parution du code-barres ne sont PAS un numéro fiable (CLAUDE.md), et l'add-on EAN-2 qui - le porterait vraiment n'a pas pu être vérifié sur un magazine réel. - *@ - @if (_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne)) - { -

- Le code-barres portait « @NumeroScanne » à côté du code principal, ce qui est - normalement le numéro de parution. Vérifiez-le sur la couverture - avant d'enregistrer. -

- } - - - -
- -
- -

La revue

- - - - - - - -
- - - -
- - @if (_confirmeSuppression) - { -

- Les @_revue.Numeros.Count numéro(s) recensés disparaîtront avec elle : ils n'existent - que par leur revue. -

- } } @code { @@ -232,16 +288,54 @@ else private string? _issn; private string? _editeur; - /// Saisie du numéro qu'on ajoute. + /// Saisie du numéro qu'on ajoute, et repli du formulaire qui la porte. private AjoutNumeroRevue _saisie = new(); + private bool _ajout; /// Numéro en cours de modification, et sa saisie — null = aucun. private int? _edition; private AjoutNumeroRevue _saisieEdition = new(); + /// Numéro dont le retrait attend confirmation — null = aucun. + private int? _confirmeRetrait; + + /// + /// Écran d'édition de la revue : nom, ISSN, éditeur, suppression, et la seule place où un + /// numéro se modifie ou se retire. + /// + /// + /// ⚠️ Le mode est dans l'ADRESSE, pas dans un booléen interne : convention du projet depuis + /// la fiche livre. Comme les deux routes partagent le même paramètre Id, le routeur + /// ne déclenche aucun nouveau rendu en passant de l'une à l'autre — d'où l'abonnement + /// à LocationChanged, sans lequel l'URL changerait et l'écran resterait le même. + /// + private bool ModeEdition => + Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/') + .EndsWith("/edition", StringComparison.OrdinalIgnoreCase); + private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne; - protected override void OnInitialized() => Reseau.Change += SurChangementReseau; + protected override void OnInitialized() + { + Reseau.Change += SurChangementReseau; + Navigation.LocationChanged += SurNavigation; + } + + /// + /// Quitter l'édition referme ce qui n'a de sens que là : un formulaire de numéro resté + /// ouvert, et une confirmation de retrait en attente. + /// + private void SurNavigation(object? _, LocationChangedEventArgs __) => InvokeAsync(() => + { + if (!ModeEdition) + { + _edition = null; + _confirmeRetrait = null; + _confirmeSuppression = false; + } + + StateHasChanged(); + }); private void SurChangementReseau() => _ = InvokeAsync(async () => { @@ -249,7 +343,11 @@ else StateHasChanged(); }); - public void Dispose() => Reseau.Change -= SurChangementReseau; + public void Dispose() + { + Reseau.Change -= SurChangementReseau; + Navigation.LocationChanged -= SurNavigation; + } protected override Task OnParametersSetAsync() => ChargerAsync(); @@ -266,13 +364,22 @@ else _editeur = _revue.Editeur; } + // Un numéro lu au code-barres ouvre le formulaire de lui-même : on vient de scanner, + // demander un clic de plus pour saisir ce qu'on tient en main serait un détour. if (!_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne)) { _numeroPropose = true; _saisie.Numero = NumeroScanne.Trim(); + _ajout = true; } } + private void BasculerAjout() + { + _erreur = null; + _ajout = !_ajout; + } + private void Appliquer(RevueDto revue) { _revue = revue; @@ -318,6 +425,7 @@ else private void OuvrirEdition(NumeroRevueDto numero) { _erreur = null; + _confirmeRetrait = null; _edition = numero.Id; _saisieEdition = new AjoutNumeroRevue { @@ -329,6 +437,8 @@ else }; } + private void FermerEdition() => _edition = null; + private async Task EnregistrerNumeroAsync() { if (_edition is not { } numeroId @@ -358,9 +468,19 @@ else private async Task RetirerAsync(NumeroRevueDto numero) { + // Premier clic : on demande confirmation. Un numéro retiré ne se récupère pas, et la + // liste s'égrène sous le pouce. + if (_confirmeRetrait != numero.Id) + { + _erreur = null; + _confirmeRetrait = numero.Id; + return; + } + _enCours = true; _erreur = await Api.RetirerNumeroAsync(numero.Id); _enCours = false; + _confirmeRetrait = null; // Le numéro retiré était peut-être celui qu'on modifiait : un formulaire ouvert sur une // ligne disparue n'aurait plus rien à enregistrer. @@ -392,6 +512,10 @@ else } Appliquer(resultat.Valeur!); + + // La fiche est à jour : on retombe sur la consultation, comme après l'édition d'un + // livre ou d'une série. Rester en édition inviterait à réenregistrer. + Navigation.NavigateTo($"revues/{Id}"); } private async Task SupprimerAsync() diff --git a/MaBibli.Client/Services/RemonteeRoutes.cs b/MaBibli.Client/Services/RemonteeRoutes.cs index 685c569..d983a71 100644 --- a/MaBibli.Client/Services/RemonteeRoutes.cs +++ b/MaBibli.Client/Services/RemonteeRoutes.cs @@ -67,6 +67,7 @@ public static class RemonteeRoutes // Revues. ⚠️ « /revues/ajout » est écrit AVANT « /revues/{id} » : « ajout » n'est pas un // identifiant, mais l'ordre de lecture est ce qui le garantit sans ambiguïté. ("/revues/ajout", "/revues"), + ("/revues/{id}/edition", "/revues/{id}"), ("/revues/{id}", "/revues"), ("/revues", Racine), diff --git a/MaBibli.Tests/RemonteeRoutesTests.cs b/MaBibli.Tests/RemonteeRoutesTests.cs index 7b91cda..a2536fc 100644 --- a/MaBibli.Tests/RemonteeRoutesTests.cs +++ b/MaBibli.Tests/RemonteeRoutesTests.cs @@ -29,6 +29,7 @@ public class RemonteeRoutesTests [InlineData("/ajout/manuel", "/")] // Revues [InlineData("/revues/ajout", "/revues")] + [InlineData("/revues/12/edition", "/revues/12")] [InlineData("/revues/12", "/revues")] [InlineData("/revues", "/")] // Auteurs @@ -74,7 +75,7 @@ public class RemonteeRoutesTests /// deviné, et jamais rien d'externe. /// [Theory] - [InlineData("/revues/12/edition", "/revues")] + [InlineData("/revues/12/inconnu", "/revues")] [InlineData("/series/7/inconnu", "/series")] [InlineData("/inconnu/total", "/")] [InlineData("/livres", "/")] diff --git a/MaBibli.Tests/ServiceRevuesTests.cs b/MaBibli.Tests/ServiceRevuesTests.cs index c8675be..7d8367a 100644 --- a/MaBibli.Tests/ServiceRevuesTests.cs +++ b/MaBibli.Tests/ServiceRevuesTests.cs @@ -307,6 +307,65 @@ public class ServiceRevuesTests : IDisposable Assert.Equal(2, (await _db.ArticlesUne.ToListAsync()).Count); } + /// + /// L'écran d'édition (lot W) affiche telle quelle la revue rendue par la + /// modification d'un numéro : elle doit donc revenir déjà rangée. + /// + /// + /// ⚠️ C'est ce qui empêche de croire qu'on a mal saisi une date : corriger la parution d'un + /// numéro le déplace dans la liste, et l'écran ne recharge rien de son côté. Les numéros + /// sans date ferment la liste, ici comme partout — une date inconnue n'est pas une date + /// ancienne. + /// + [Fact] + public async Task Corriger_la_parution_range_le_numero_a_sa_place() + { + var revue = await RevueAsync(); + + await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue + { + Numero = "43", + DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue + { + Numero = "hors-série 1", + })).Revue!; + + Assert.Equal(["43", "hors-série 1"], cree.Numeros.Select(n => n.Numero)); + + var horsSerie = cree.Numeros.First(n => n.Numero == "hors-série 1").Id; + + var apres = await _revues.ModifierNumeroAsync(horsSerie, new AjoutNumeroRevue + { + Numero = "hors-série 1", + DateParution = new DateTime(2025, 3, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + Assert.Equal(["hors-série 1", "43"], apres.Revue!.Numeros.Select(n => n.Numero)); + } + + /// + /// Retirer un numéro depuis l'écran d'édition ne touche qu'à lui : les autres parutions et + /// la fiche de la revue restent en place. + /// + [Fact] + public async Task Retirer_un_numero_laisse_la_revue_et_ses_autres_numeros() + { + var revue = await RevueAsync(); + + await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" }); + var cree = (await _revues.AjouterNumeroAsync( + revue.Id, new AjoutNumeroRevue { Numero = "44" })).Revue!; + + Assert.True(await _revues.RetirerNumeroAsync(cree.Numeros.First(n => n.Numero == "44").Id)); + + var restantes = await _revues.ListerAsync(); + + Assert.Equal(["43"], Assert.Single(restantes).Numeros.Select(n => n.Numero)); + } + public void Dispose() { _db.Dispose();