Découverte à la mesure : l'EAN-2 est DÉSACTIVÉ par défaut dans zbar. Il ne suffisait donc pas de cesser de le filtrer — il n'était jamais produit. Vérifié sur un EAN-13 + EAN-2 rendus en canvas : un symbole par défaut, deux après setConfig(ZBAR_EAN2, ZBAR_CFG_ENABLE, 1). L'EAN-5 reste désactivé : il porte un prix, pas un numéro. Ce qui reste non vérifié — que les magazines réels y mettent bien leur numéro de parution — est dit à l'utilisateur : le numéro est proposé dans un champ modifiable, sous une invitation à le comparer à la couverture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
333 lines
10 KiB
Plaintext
333 lines
10 KiB
Plaintext
@page "/revues/{Id:int}"
|
|
@inject ServiceLivresApi Api
|
|
@inject NavigationManager Navigation
|
|
@inject EtatReseau Reseau
|
|
@implements IDisposable
|
|
|
|
<PageTitle>MaBibli — @(_revue?.Titre ?? "revue")</PageTitle>
|
|
|
|
@if (_revue is null)
|
|
{
|
|
<p class="message-discret" role="status">
|
|
@(_chargement ? "Chargement…" : "Cette revue n'est pas dans les données enregistrées sur cet appareil.")
|
|
</p>
|
|
}
|
|
else
|
|
{
|
|
<h1 class="titre-page">@_revue.Titre</h1>
|
|
|
|
<p class="carte-details">
|
|
@if (!string.IsNullOrWhiteSpace(_revue.Issn))
|
|
{
|
|
<span class="code-isbn">ISSN @_revue.Issn</span>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(_revue.Editeur))
|
|
{
|
|
<span>@_revue.Editeur</span>
|
|
}
|
|
</p>
|
|
|
|
@if (_erreur is not null)
|
|
{
|
|
<p class="message-erreur" role="alert">@_erreur</p>
|
|
}
|
|
|
|
<h2 class="titre-section">Numéros possédés</h2>
|
|
|
|
@if (_revue.Numeros.Count == 0)
|
|
{
|
|
<p class="message-discret">
|
|
Aucun numéro recensé pour l'instant.
|
|
</p>
|
|
}
|
|
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. *@
|
|
<ul class="liste-numeros">
|
|
@foreach (var numero in _revue.Numeros)
|
|
{
|
|
<li class="carte-numero" @key="numero.Id">
|
|
<div class="carte-corps">
|
|
<p class="carte-titre">N° @numero.Numero</p>
|
|
|
|
<p class="carte-details">
|
|
@if (numero.DateParution is { } parution)
|
|
{
|
|
<span>@parution.ToLocalTime().ToString("MMMM yyyy")</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Date de parution inconnue</span>
|
|
}
|
|
</p>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(numero.Note))
|
|
{
|
|
<p class="souhait-note">@numero.Note</p>
|
|
}
|
|
</div>
|
|
|
|
<button type="button" class="bouton bouton-compact bouton-danger"
|
|
disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
|
|
aria-label="@($"Retirer le numéro {numero.Numero}")"
|
|
@onclick="() => RetirerAsync(numero)">
|
|
Retirer
|
|
</button>
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
|
|
<h2 class="titre-section">Ajouter un numéro</h2>
|
|
|
|
@*
|
|
⚠️ 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))
|
|
{
|
|
<p class="message-avertissement" role="status">
|
|
Le code-barres portait « @NumeroScanne » à côté du code principal, ce qui est
|
|
normalement le numéro de parution. <strong>Vérifiez-le sur la couverture</strong>
|
|
avant d'enregistrer.
|
|
</p>
|
|
}
|
|
|
|
<div class="champs-numero">
|
|
<label class="champ">
|
|
<span class="champ-libelle">Numéro</span>
|
|
<input class="champ-saisie" type="text" placeholder="43" @bind="_numero"
|
|
@bind:event="oninput" @onkeydown="SurTouche" autocomplete="off" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Parution <span class="champ-aide">(facultatif)</span></span>
|
|
<input class="champ-saisie" type="date" @bind="_parution" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Note <span class="champ-aide">(facultatif)</span></span>
|
|
<input class="champ-saisie" type="text" @bind="_note" @bind:event="oninput"
|
|
placeholder="dossier sur l'eau" autocomplete="off" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(_enCours || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_numero))"
|
|
title="@MotifBlocage"
|
|
@onclick="AjouterAsync">
|
|
Ajouter ce numéro
|
|
</button>
|
|
</div>
|
|
|
|
<h2 class="titre-section">La revue</h2>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Nom</span>
|
|
<input class="champ-saisie" type="text" @bind="_titre" @bind:event="oninput" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">ISSN <span class="champ-aide">(identifiant de la revue)</span></span>
|
|
<input class="champ-saisie" type="text" @bind="_issn" @bind:event="oninput"
|
|
placeholder="2466-6718" autocomplete="off" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Éditeur</span>
|
|
<input class="champ-saisie" type="text" @bind="_editeur" @bind:event="oninput" />
|
|
</label>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(_enCours || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_titre))"
|
|
title="@MotifBlocage"
|
|
@onclick="EnregistrerAsync">
|
|
Enregistrer
|
|
</button>
|
|
|
|
<button type="button" class="bouton bouton-discret bouton-danger"
|
|
disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
|
|
@onclick="SupprimerAsync">
|
|
@(_confirmeSuppression ? "Confirmer la suppression" : "Supprimer la revue")
|
|
</button>
|
|
</div>
|
|
|
|
@if (_confirmeSuppression)
|
|
{
|
|
<p class="message-avertissement" role="status">
|
|
Les @_revue.Numeros.Count numéro(s) recensés disparaîtront avec elle : ils n'existent
|
|
que par leur revue.
|
|
</p>
|
|
}
|
|
}
|
|
|
|
@code {
|
|
[Parameter] public int Id { get; set; }
|
|
|
|
/// <summary>
|
|
/// Numéro lu sur l'add-on <b>EAN-2</b> du code-barres, transmis par l'écran de scan.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ <b>Proposé, pas acquis.</b> L'EAN-2 est normativement le numéro de parution, mais
|
|
/// rien ne l'a confirmé sur des magazines réels : il atterrit donc dans le champ, où
|
|
/// l'utilisateur le compare à la couverture et le corrige. C'est aussi pourquoi l'écran le
|
|
/// dit explicitement au lieu de le glisser en douce.
|
|
/// </remarks>
|
|
[SupplyParameterFromQuery(Name = "numero")]
|
|
public string? NumeroScanne { get; set; }
|
|
|
|
/// <summary>Le numéro scanné n'est proposé qu'une fois : ensuite, le champ appartient à l'utilisateur.</summary>
|
|
private bool _numeroPropose;
|
|
|
|
private RevueDto? _revue;
|
|
private bool _chargement = true;
|
|
private bool _enCours;
|
|
private string? _erreur;
|
|
private bool _confirmeSuppression;
|
|
|
|
private string _titre = string.Empty;
|
|
private string? _issn;
|
|
private string? _editeur;
|
|
|
|
private string _numero = string.Empty;
|
|
private DateTime? _parution;
|
|
private string? _note;
|
|
|
|
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
|
|
|
|
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
|
|
|
|
private void SurChangementReseau() => _ = InvokeAsync(async () =>
|
|
{
|
|
await ChargerAsync();
|
|
StateHasChanged();
|
|
});
|
|
|
|
public void Dispose() => Reseau.Change -= SurChangementReseau;
|
|
|
|
protected override Task OnParametersSetAsync() => ChargerAsync();
|
|
|
|
private async Task ChargerAsync()
|
|
{
|
|
_chargement = true;
|
|
_revue = (await Api.ListerRevuesAsync()).FirstOrDefault(r => r.Id == Id);
|
|
_chargement = false;
|
|
|
|
if (_revue is not null)
|
|
{
|
|
_titre = _revue.Titre;
|
|
_issn = _revue.Issn;
|
|
_editeur = _revue.Editeur;
|
|
}
|
|
|
|
if (!_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne))
|
|
{
|
|
_numeroPropose = true;
|
|
_numero = NumeroScanne.Trim();
|
|
}
|
|
}
|
|
|
|
private void Appliquer(RevueDto revue)
|
|
{
|
|
_revue = revue;
|
|
_titre = revue.Titre;
|
|
_issn = revue.Issn;
|
|
_editeur = revue.Editeur;
|
|
}
|
|
|
|
private async Task SurTouche(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_numero) && Reseau.EnLigne)
|
|
{
|
|
await AjouterAsync();
|
|
}
|
|
}
|
|
|
|
private async Task AjouterAsync()
|
|
{
|
|
_enCours = true;
|
|
_erreur = null;
|
|
|
|
// Comme pour les prêts : le <input type="date"> produit une date SANS fuseau. Elle est
|
|
// déclarée locale avant conversion, sans quoi la parution se décalerait d'un jour pour
|
|
// la moitié du globe.
|
|
var parution = _parution is { } date
|
|
? DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime()
|
|
: (DateTime?)null;
|
|
|
|
var resultat = await Api.AjouterNumeroAsync(Id, new AjoutNumeroRevue
|
|
{
|
|
Numero = _numero,
|
|
DateParution = parution,
|
|
Note = _note,
|
|
});
|
|
|
|
_enCours = false;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
return;
|
|
}
|
|
|
|
Appliquer(resultat.Valeur!);
|
|
_numero = string.Empty;
|
|
_parution = null;
|
|
_note = null;
|
|
}
|
|
|
|
private async Task RetirerAsync(NumeroRevueDto numero)
|
|
{
|
|
_enCours = true;
|
|
_erreur = await Api.RetirerNumeroAsync(numero.Id);
|
|
_enCours = false;
|
|
|
|
if (_erreur is null)
|
|
{
|
|
await ChargerAsync();
|
|
}
|
|
}
|
|
|
|
private async Task EnregistrerAsync()
|
|
{
|
|
_enCours = true;
|
|
_erreur = null;
|
|
|
|
var resultat = await Api.ModifierRevueAsync(
|
|
Id, new EnregistrementRevue { Titre = _titre, Issn = _issn, Editeur = _editeur });
|
|
|
|
_enCours = false;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
return;
|
|
}
|
|
|
|
Appliquer(resultat.Valeur!);
|
|
}
|
|
|
|
private async Task SupprimerAsync()
|
|
{
|
|
if (!_confirmeSuppression)
|
|
{
|
|
_confirmeSuppression = true;
|
|
return;
|
|
}
|
|
|
|
_enCours = true;
|
|
_erreur = await Api.SupprimerRevueAsync(Id);
|
|
_enCours = false;
|
|
|
|
if (_erreur is null)
|
|
{
|
|
Navigation.NavigateTo("revues");
|
|
}
|
|
}
|
|
}
|