From 3d5e73838ebb664abfba01c8cf57744cc7c17438 Mon Sep 17 00:00:00 2001 From: LIMONIER Mathieu Date: Thu, 20 Aug 2026 13:11:43 +0200 Subject: [PATCH] =?UTF-8?q?S=C3=A9pare=20consultation=20et=20=C3=A9dition?= =?UTF-8?q?=20pour=20les=20lots=20B1=20B2=20B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Réutilise la carte livre du catalogue, sépare les modes des séries et des envies, et déplie les œuvres bibliographiques non possédées. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 20 ++++++ IDEES.md | 4 +- MaBibli.Client/Composants/CarteLivre.razor | 57 +++++++++++++++++ MaBibli.Client/Pages/Bibliographie.razor | 42 ++++++++++++- MaBibli.Client/Pages/Catalogue.razor | 65 +------------------ MaBibli.Client/Pages/Serie.razor | 72 ++++++++++++++++++---- MaBibli.Client/Pages/Souhaits.razor | 49 ++++++++++++--- MaBibli.Client/wwwroot/css/app.css | 16 +++++ 8 files changed, 238 insertions(+), 87 deletions(-) create mode 100644 MaBibli.Client/Composants/CarteLivre.razor diff --git a/CLAUDE.md b/CLAUDE.md index 52ba41b..670242d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1766,6 +1766,26 @@ affichée, convertir en WebP). IDEES.md posait la question à mesurer avant de s règle que pour l'AOT WASM — non fait tant que la taille du cache image n'a pas montré de problème réel en usage. +## Lot B — consulter d'abord, modifier ensuite (2026-08-20) + +`CarteLivre.razor` est le composant partagé des cartes du catalogue. Il reçoit un `LivreDto`, +porte la couverture, les auteurs et les étiquettes de format, type, statut, prêt et éditeur, +et le catalogue l'utilise lui-même. Une œuvre bibliographique possédée réutilise cette carte +quand son livre est disponible. + +Pour une série, `/series/{id}` est la consultation : parenté, tomes, carte du livre rattaché, +réordonnancement, rattachement, ajout et retrait restent des gestes courants. Seuls le titre, +la parenté dans un cycle et la suppression sont dans `/series/{id}/edition`. Comme pour la +fiche livre, le composant s'abonne à `LocationChanged` car les routes partagent le même paramètre. + +Pour les envies, `/souhaits` consulte et conserve l'export et la suppression courante. +`/souhaits/ordre` porte exclusivement le réordonnancement (flèches et glisser-déposer), afin +que ces contrôles ne puissent pas être déclenchés en faisant défiler la liste. + +Une œuvre bibliographique non possédée n'a pas de fiche locale. Son titre est donc un bouton +qui déplie inline son ISBN et son nombre d'éditions, tandis que les œuvres possédées ouvrent +leur fiche via la carte partagée. + ## Installation YunoHost — éprouvée en production le 2026-08-18 Le paquet vit dans le dépôt `mabibli_ynh` (voir « Deux dépôts distincts »). Installé, diff --git a/IDEES.md b/IDEES.md index 7de93fa..20d850d 100644 --- a/IDEES.md +++ b/IDEES.md @@ -130,7 +130,7 @@ mesure montre un gain réel. Pas encore mesuré à l'échelle d'un vrai fonds. --- -## Lot B — Consulter d'abord, modifier ensuite +## Lot B — Consulter d'abord, modifier ensuite — traité le 2026-08-20 Un seul motif, déjà acté et éprouvé pour la fiche livre (`/livres/{id}` consulte, `/livres/{id}/edition` modifie, le mode est **dans l'URL**), à étendre aux écrans qui présentent @@ -145,6 +145,8 @@ encore un formulaire vivant sous le pouce. - **B3. Bibliographie d'un auteur** : même carte que le catalogue, et un **clic ouvre le détail** de l'œuvre au lieu de s'arrêter à une ligne. +✅ Traité : voir la décision et les limites retenues dans `CLAUDE.md`. + ⚠️ Le point commun des trois n'est pas cosmétique : c'est **une seule carte de livre** réutilisée partout. Si les trois écrans réimplémentent la carte chacun de leur côté, les pastilles divergeront comme le filtre a divergé (voir A2). diff --git a/MaBibli.Client/Composants/CarteLivre.razor b/MaBibli.Client/Composants/CarteLivre.razor new file mode 100644 index 0000000..32c9b4b --- /dev/null +++ b/MaBibli.Client/Composants/CarteLivre.razor @@ -0,0 +1,57 @@ +
+ + +
+ @Livre.Titre + + @if (Livre.Auteurs.Count > 0) + { +

+ @foreach (var (auteur, index) in Livre.Auteurs.Select((a, i) => (a, i))) + { + @(index > 0 ? ", " : "") + @auteur.Nom + @if (auteur.Role != RoleAuteur.NonPrecise) + { + (@Libelles.RoleAuteur(auteur.Role).ToLowerInvariant()) + } + } +

+ } + +

+ @if (Livre.Format == Format.Numerique) + { + @Libelles.Format(Livre.Format) + } + @if (Livre.TypeDocument != TypeDocument.NonPrecise) + { + + @Libelles.TypeDocument(Livre.TypeDocument) + + } + @if (Livre.Statut is { } statut) + { + + @Libelles.Statut(statut) + + } + @if (Livre.PreteA is { } emprunteur) + { + Prêté à @emprunteur + } + @if (!string.IsNullOrWhiteSpace(Livre.Editeur)) + { + @Livre.Editeur + } +

+ + @Actions +
+
+ +@code { + [Parameter, EditorRequired] public LivreDto Livre { get; set; } = default!; + [Parameter] public RenderFragment? Actions { get; set; } +} diff --git a/MaBibli.Client/Pages/Bibliographie.razor b/MaBibli.Client/Pages/Bibliographie.razor index 6f82d41..0e641dd 100644 --- a/MaBibli.Client/Pages/Bibliographie.razor +++ b/MaBibli.Client/Pages/Bibliographie.razor @@ -140,15 +140,26 @@ else if (_bibliographie is { } biblio) { @* La classe « oeuvre-possedee » grise la ligne : le livre est déjà sur l'étagère. *@
  • + @if (oeuvre is { Possede: true, LivreId: { } livreId } + && _livresCatalogue.FirstOrDefault(l => l.Id == livreId) is { } livre) + { + + } + @if (!oeuvre.Possede || !_livresCatalogue.Any(l => l.Id == oeuvre.LivreId)) + {

    - @if (oeuvre is { Possede: true, LivreId: { } livreId }) + @if (oeuvre is { Possede: true, LivreId: { } livreIdPossede }) { - @oeuvre.Titre + @oeuvre.Titre } else { - @oeuvre.Titre + }

    @@ -176,6 +187,20 @@ else if (_bibliographie is { } biblio) }

    + @if (_oeuvresOuvertes.Contains(oeuvre.Titre)) + { +

    + @if (!string.IsNullOrWhiteSpace(oeuvre.Isbn)) + { + ISBN : @FormatageIsbn.Afficher(oeuvre.Isbn) + } + @if (oeuvre.NombreEditions > 1) + { + @oeuvre.NombreEditions éditions recensées + } +

    + } + } @if (oeuvre.ADecouvrir) { @@ -225,6 +250,8 @@ else if (_bibliographie is { } biblio) private string? _message; private int _possedees; + private IReadOnlyList _livresCatalogue = []; + private readonly HashSet _oeuvresOuvertes = []; protected override async Task OnParametersSetAsync() => await ChargerAsync(); @@ -240,6 +267,7 @@ else if (_bibliographie is { } biblio) try { _bibliographie = await Api.ObtenirBibliographieAsync(AuteurId); + _livresCatalogue = await Api.ListerAsync(new CritereLivres()); if (_bibliographie is null) { @@ -272,6 +300,14 @@ else if (_bibliographie is { } biblio) } } + private void BasculerOeuvre(string titre) + { + if (!_oeuvresOuvertes.Add(titre)) + { + _oeuvresOuvertes.Remove(titre); + } + } + /// /// Ajoute une œuvre à la liste d'envies, en emportant ce que la BnF en sait. /// diff --git a/MaBibli.Client/Pages/Catalogue.razor b/MaBibli.Client/Pages/Catalogue.razor index e57eab0..752a6a7 100644 --- a/MaBibli.Client/Pages/Catalogue.razor +++ b/MaBibli.Client/Pages/Catalogue.razor @@ -115,69 +115,8 @@ else if (_livres is not null)
      @foreach (var livre in _livres) { -
    • - @* La vignette s'agrandit au clic ; le titre, lui, ouvre la fiche. *@ - - -
      - @livre.Titre - - @if (livre.Auteurs.Count > 0) - { -

      - @foreach (var (auteur, index) in livre.Auteurs.Select((a, i) => (a, i))) - { - @(index > 0 ? ", " : "") - @auteur.Nom@if (auteur.Role != RoleAuteur.NonPrecise) - { - @* Comme le format et le type : seul ce qui a été choisi s'affiche. *@ - (@Libelles.RoleAuteur(auteur.Role).ToLowerInvariant()) - } - } -

      - } - - @* Une seule ligne d'étiquettes, en lecture seule (IDEES.md) : la liste se - parcourt, elle ne se manipule pas. Tout se modifie depuis la fiche. *@ -

      - @* Un livre est physique par défaut : ne l'afficher que pour les ebooks, - sinon l'étiquette est sur toutes les lignes et n'apprend rien. *@ - @if (livre.Format == Format.Numerique) - { - @Libelles.Format(livre.Format) - } - - @* Même règle que le format : on n'affiche que ce qui a été CHOISI. - « Non précisé » n'apprendrait rien et serait sur presque chaque ligne. *@ - @if (livre.TypeDocument != TypeDocument.NonPrecise) - { - - @Libelles.TypeDocument(livre.TypeDocument) - - } - - @* Personnel : ce que montre cette pastille est MA lecture, pas celle du foyer. - Pas de pastille = non commencé, l'absence de ligne en base. *@ - @if (livre.Statut is { } statut) - { - - @Libelles.Statut(statut) - - } - - @* Un livre dehors l'est pour tout le foyer, contrairement au statut. *@ - @if (livre.PreteA is { } emprunteur) - { - Prêté à @emprunteur - } - - @if (!string.IsNullOrWhiteSpace(livre.Editeur)) - { - @livre.Editeur - } -

      -
      +
    • +
    • }
    diff --git a/MaBibli.Client/Pages/Serie.razor b/MaBibli.Client/Pages/Serie.razor index 5b049e4..9daaf8d 100644 --- a/MaBibli.Client/Pages/Serie.razor +++ b/MaBibli.Client/Pages/Serie.razor @@ -1,4 +1,5 @@ @page "/series/{Id:int}" +@page "/series/{Id:int}/edition" @inject ServiceLivresApi Api @inject NavigationManager Navigation @inject EtatReseau Reseau @@ -14,11 +15,14 @@ } else { - @if (_parente is { } parente) + @if (!ModeEdition && _parente is { } parente) { + @if (!ModeEdition) + {

    Fait partie de @parente.Titre.

    + } }

    @_serie.Titre

    @@ -47,7 +51,7 @@ else } @* Les sous-séries d'un cycle : on les liste, on ne les déplie pas — chacune a son écran. *@ - @if (_filles.Count > 0) + @if (!ModeEdition && _filles.Count > 0) {

    Séries de ce cycle

      @@ -63,7 +67,7 @@ else
    } - @if (_serie.Elements.Count > 0) + @if (!ModeEdition && _serie.Elements.Count > 0) {

    Ordre de lecture

    @@ -94,20 +98,27 @@ else
    - @if (element.LivreId is { } livreId) + @if (element.LivreId is { } livreId && _livresCatalogue.FirstOrDefault(l => l.Id == livreId) is { } livre) { - @element.Titre + + } + else if (element.LivreId is { } idRattache) + { + @element.Titre } else {

    @element.Titre

    } - @if (!string.IsNullOrWhiteSpace(element.Auteurs)) + @if ((element.LivreId is null || !_livresCatalogue.Any(l => l.Id == element.LivreId)) + && !string.IsNullOrWhiteSpace(element.Auteurs)) {

    @element.Auteurs

    } + @if (element.LivreId is null || !_livresCatalogue.Any(l => l.Id == element.LivreId)) + {

    @if (!element.Possede) { @@ -120,6 +131,7 @@ else Prêté à @emprunteur }

    + }
    @if (!element.Possede) @@ -161,12 +173,12 @@ else value="@_recherche" @oninput="ChercherAsync" aria-label="Chercher un livre à rattacher" /> - @foreach (var livre in _resultats) + @foreach (var resultat in _resultats) { }
    @@ -177,6 +189,8 @@ else } + @if (!ModeEdition) + {

    Ajouter un tome

    @* @@ -221,7 +235,10 @@ else }
    } + } + @if (ModeEdition) + {

    La série

  • - @if (_souhaits.Count > 1) + @if (ModeOrdre && _souhaits.Count > 1) {
    + }
  • } @@ -197,6 +202,14 @@ else @* L'ajout est passé dans son propre écran : la page ne montre plus que la liste, comme la fiche livre ne montre plus son formulaire. *@