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>
177 lines
6.2 KiB
Plaintext
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");
|
|
}
|
|
}
|