Clôt la décision du premier lot, qui écartait les périodiques faute de modèle. Douze numéros d'un magazine ne font plus douze fiches identiques. Table à part, pour la troisième fois et pour le même motif que la liste d'envies et les séries : logée dans Livres, une revue entrerait dans le catalogue, ses compteurs, les doublons et les bibliographies, et il faudrait écrire « et qui n'est pas une revue » à chaque lecture. Un numéro est recensé, rien de plus — ni prêt ni statut, tous deux liés à Livre par clé étrangère. Réversible. « Créer ou retrouver » et non « créer » : scanner le numéro suivant du même magazine retombe forcément sur la même revue. L'unicité de l'ISSN est un index partiel, sans quoi les revues sans ISSN se bloqueraient entre elles. Le flux 977 mène désormais à la fiche de la revue, et ne pré-remplit plus le formulaire d'un livre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
303 lines
9.2 KiB
Plaintext
303 lines
9.2 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.
|
|
*@
|
|
<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; }
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|