Files
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:16 +02:00

177 lines
6.2 KiB
Plaintext

@page "/souhaits/{Id:int}/edition"
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@inject NavigationManager Navigation
@implements IDisposable
@*
Écran dédié plutôt qu'un formulaire déplié dans la liste : convention du projet depuis la
fiche livre — le mode est dans l'ADRESSE, jamais dans un booléen interne. C'est ce qui rend
le bouton « retour » du navigateur une sortie d'édition naturelle, et ce qui empêche de
modifier une envie en faisant défiler sa liste.
⚠️ Cette page ne partage aucun paramètre avec une autre route : contrairement à la fiche
livre ou à la fiche série, elle n'a pas besoin de s'abonner à LocationChanged. L'abonnement
ci-dessous ne sert qu'à suivre l'état du réseau.
*@
<PageTitle>MaBibli — modifier une envie</PageTitle>
<h1 class="titre-page">Modifier une envie</h1>
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@if (_introuvable)
{
@* Hors-ligne, « supprimée » et « pas encore enregistrée ici » sont indiscernables : on ne
tranche pas à la place de l'utilisateur. Même règle que pour une fiche livre absente. *@
<p class="message-discret">
@(Reseau.EnLigne
? "Cette envie n'existe plus dans votre liste."
: "Cette envie n'est pas dans les données enregistrées sur cet appareil.")
</p>
}
else if (_saisie is null)
{
<Patience Message="Ouverture de l'envie…" />
}
else
{
<div class="formulaire">
<label class="champ">
<span class="champ-libelle">Titre <span class="obligatoire" aria-hidden="true">*</span></span>
<input class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Auteur</span>
<input class="champ-saisie" type="text" @bind="_saisie.Auteur" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Éditeur</span>
<input class="champ-saisie" type="text" @bind="_saisie.Editeur" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Année</span>
<input class="champ-saisie" type="text" @bind="_saisie.Annee" @bind:event="oninput"
placeholder="2012" autocomplete="off" />
</label>
@* Le champ de saisie garde la valeur nue : découper à la frappe se battrait avec le
curseur. Les tirets sont une affaire d'affichage, pas de saisie. *@
<label class="champ">
<span class="champ-libelle">ISBN <span class="champ-aide">(facultatif)</span></span>
<input class="champ-saisie" type="text" inputmode="numeric"
@bind="_saisie.Isbn" @bind:event="oninput" placeholder="978…" autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Note</span>
<input class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
placeholder="« demandé à Noël », « en poche seulement »…" autocomplete="off" />
</label>
@if (!Reseau.EnLigne)
{
<p class="message-avertissement" role="status">@EtatReseau.MotifHorsLigne</p>
}
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
disabled="@(_enCours || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_saisie.Titre))"
title="@MotifBlocage" @onclick="EnregistrerAsync">
@(_enCours ? "Enregistrement…" : "Enregistrer")
</button>
<a class="bouton bouton-discret" href="souhaits">Annuler</a>
</div>
</div>
}
@code {
[Parameter] public int Id { get; set; }
private EnregistrementSouhait? _saisie;
private bool _introuvable;
private bool _enCours;
private string? _erreur;
protected override async Task OnInitializedAsync()
{
Reseau.Change += SurChangementReseau;
await ChargerAsync();
}
private void SurChangementReseau() => _ = InvokeAsync(StateHasChanged);
public void Dispose() => Reseau.Change -= SurChangementReseau;
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
/// <summary>
/// Reprend l'envie depuis la <b>liste</b>, et non depuis un point d'entrée dédié.
/// </summary>
/// <remarks>
/// L'API n'expose pas <c>GET /api/souhaits/{id}</c>, et il n'y a pas lieu de l'ajouter : la
/// liste entière est déjà chargée, déjà mise en instantané, et c'est ce qui rend cet écran
/// consultable hors-ligne — même si l'enregistrement, lui, y est refusé.
/// </remarks>
private async Task ChargerAsync()
{
var souhaits = await Api.ListerSouhaitsAsync();
var souhait = souhaits.FirstOrDefault(s => s.Id == Id);
if (souhait is null)
{
_introuvable = true;
return;
}
// ⚠️ La couverture n'est PAS reprise ici : elle n'a pas de champ de saisie, et le
// service la conserve donc telle qu'elle est en base. La valeur portée par le DTO peut
// en outre être déduite de l'ISBN — la figer serait écrire en base ce qui se calcule.
_saisie = new EnregistrementSouhait
{
Titre = souhait.Titre,
Auteur = souhait.Auteur,
Editeur = souhait.Editeur,
Annee = souhait.Annee,
Isbn = souhait.Isbn,
Note = souhait.Note,
};
}
private async Task EnregistrerAsync()
{
if (_saisie is null)
{
return;
}
_enCours = true;
_erreur = null;
var resultat = await Api.ModifierSouhaitAsync(Id, _saisie);
_enCours = false;
if (!resultat.EstOk)
{
// Le message vient de l'API : doublon d'une autre envie, ISBN invalide, ou hors
// ligne. Aucun code HTTP brut n'arrive jusqu'ici.
_erreur = resultat.Erreur;
return;
}
Navigation.NavigateTo("souhaits");
}
}