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>
296 lines
9.9 KiB
Plaintext
296 lines
9.9 KiB
Plaintext
@inject ServiceLivresApi Api
|
|
@inject EtatReseau Reseau
|
|
@implements IDisposable
|
|
|
|
@*
|
|
Prêts d'un livre : l'état courant, l'action qui s'impose, puis l'historique.
|
|
|
|
Mobile d'abord : une seule colonne, un seul bouton visible à la fois. Prêter et rendre sont
|
|
les deux gestes du quotidien — ils sont en haut, l'historique se déroule dessous.
|
|
*@
|
|
<section class="bloc-prets">
|
|
<h2 class="titre-section">Prêt</h2>
|
|
|
|
@if (Format == MaBibli.Shared.Entites.Format.Numerique)
|
|
{
|
|
@* CLAUDE.md : un ebook n'est qu'une fiche, il n'y a pas d'exemplaire à confier.
|
|
L'API le refuse aussi — ici on évite simplement de proposer un geste impossible. *@
|
|
<p class="message-discret">
|
|
Un livre numérique ne se prête pas : il n'y a pas d'exemplaire à confier.
|
|
</p>
|
|
}
|
|
else if (!Reseau.EnLigne)
|
|
{
|
|
@*
|
|
L'historique des prêts n'est pas mis en cache : une requête par livre pour une
|
|
information rarement consultée. En revanche l'état COURANT vient de la fiche, donc
|
|
de l'instantané du catalogue — c'est lui qui répond à « où est ce livre ? », la
|
|
seule question qui se pose vraiment sans réseau, devant l'étagère.
|
|
|
|
Afficher une liste vide ferait croire à « jamais prêté » : on dit ce qu'on sait, et
|
|
on dit ce qu'on ignore.
|
|
*@
|
|
@if (PreteA is { } absent)
|
|
{
|
|
<p class="bandeau-pret">
|
|
<span class="etiquette etiquette-prete">Prêté</span>
|
|
à <strong>@absent</strong>
|
|
@if (PreteDepuis is { } depuis)
|
|
{
|
|
@($" depuis le {Jour(depuis)}")
|
|
}
|
|
</p>
|
|
}
|
|
else
|
|
{
|
|
<p class="message-discret">Ce livre était à la maison à la dernière synchronisation.</p>
|
|
}
|
|
|
|
<p class="message-discret">
|
|
Hors ligne : l'historique des prêts n'est pas consultable, et prêter ou marquer un
|
|
retour demande le réseau.
|
|
</p>
|
|
}
|
|
else if (_prets is null)
|
|
{
|
|
<p class="message-discret">Chargement…</p>
|
|
}
|
|
else
|
|
{
|
|
@if (_erreur is not null)
|
|
{
|
|
<p class="message-erreur" role="alert">@_erreur</p>
|
|
}
|
|
|
|
@if (EnCours is { } pret)
|
|
{
|
|
<p class="bandeau-pret">
|
|
<span class="etiquette etiquette-prete">Prêté</span>
|
|
à <strong>@pret.Emprunteur</strong>
|
|
depuis le @Jour(pret.DatePret)
|
|
<span class="pret-duree">(@Duree(pret.JoursEcoules))</span>
|
|
</p>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal" disabled="@_occupe"
|
|
@onclick="RendreAsync">
|
|
@(_occupe ? "Enregistrement…" : "Marquer comme rendu")
|
|
</button>
|
|
</div>
|
|
}
|
|
else if (!_formulaireOuvert)
|
|
{
|
|
<p class="message-discret">Ce livre est à la maison.</p>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal" @onclick="OuvrirFormulaire">
|
|
Prêter ce livre
|
|
</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@* Emprunteur en texte libre : ce sont des gens du dehors, sans compte YunoHost. *@
|
|
<label class="champ">
|
|
<span class="champ-libelle">Prêté à <span class="obligatoire" aria-hidden="true">*</span></span>
|
|
<input class="champ-saisie" type="text" @bind="_emprunteur" @bind:event="oninput"
|
|
@onkeydown="SurTouche" placeholder="Prénom, nom…" autocomplete="off" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">
|
|
Depuis le<span class="champ-aide"> (on note souvent un prêt après coup)</span>
|
|
</span>
|
|
<input class="champ-saisie" type="date" @bind="_datePret"
|
|
max="@DateTime.Today.ToString("yyyy-MM-dd")" />
|
|
</label>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(_occupe || string.IsNullOrWhiteSpace(_emprunteur))"
|
|
@onclick="PreterAsync">
|
|
@(_occupe ? "Enregistrement…" : "Prêter")
|
|
</button>
|
|
<button type="button" class="bouton bouton-discret" disabled="@_occupe"
|
|
@onclick="() => _formulaireOuvert = false">
|
|
Annuler
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
@if (Historique.Count > 0)
|
|
{
|
|
@* L'historique est la raison d'être de la table : un prêt rendu ne disparaît pas. *@
|
|
<h3 class="titre-sous-section">Prêts passés</h3>
|
|
|
|
<ul class="liste-historique">
|
|
@foreach (var passe in Historique)
|
|
{
|
|
<li @key="passe.Id">
|
|
<strong>@passe.Emprunteur</strong>
|
|
<span class="historique-dates">
|
|
du @Jour(passe.DatePret) au @Jour(passe.DateRetour!.Value)
|
|
— @Duree(passe.JoursEcoules)
|
|
</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
else if (EnCours is null)
|
|
{
|
|
<p class="message-discret">Jamais prêté.</p>
|
|
}
|
|
}
|
|
</section>
|
|
|
|
@code {
|
|
[Parameter, EditorRequired] public int LivreId { get; set; }
|
|
|
|
/// <summary>Format du livre : un ebook ne se prête pas.</summary>
|
|
[Parameter] public Format Format { get; set; }
|
|
|
|
/// <summary>
|
|
/// Emprunteur actuel d'après la fiche. Sert <b>hors-ligne</b>, où l'historique n'est pas
|
|
/// consultable mais où l'état courant, lui, vient de l'instantané du catalogue.
|
|
/// </summary>
|
|
[Parameter] public string? PreteA { get; set; }
|
|
|
|
/// <summary>Date du prêt en cours, même origine que <see cref="PreteA"/>.</summary>
|
|
[Parameter] public DateTime? PreteDepuis { get; set; }
|
|
|
|
/// <summary>Prévient la page parente qu'un prêt a changé, pour qu'elle rafraîchisse sa fiche.</summary>
|
|
[Parameter] public EventCallback OnChangement { get; set; }
|
|
|
|
private IReadOnlyList<PretDto>? _prets;
|
|
private bool _formulaireOuvert;
|
|
private bool _occupe;
|
|
private string _emprunteur = string.Empty;
|
|
private DateTime _datePret = DateTime.Today;
|
|
private string? _erreur;
|
|
private int _livreCharge;
|
|
|
|
private PretDto? EnCours => _prets?.FirstOrDefault(p => p.EstEnCours);
|
|
|
|
private IReadOnlyList<PretDto> Historique =>
|
|
_prets?.Where(p => !p.EstEnCours).ToList() ?? [];
|
|
|
|
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
|
|
|
|
/// <summary>Au retour du réseau, l'historique devient consultable : on va le chercher.</summary>
|
|
private void SurChangementReseau() => _ = InvokeAsync(async () =>
|
|
{
|
|
if (Reseau.EnLigne)
|
|
{
|
|
await ChargerAsync();
|
|
}
|
|
|
|
StateHasChanged();
|
|
});
|
|
|
|
public void Dispose() => Reseau.Change -= SurChangementReseau;
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
if (_livreCharge != LivreId)
|
|
{
|
|
_livreCharge = LivreId;
|
|
_prets = null;
|
|
await ChargerAsync();
|
|
}
|
|
}
|
|
|
|
private async Task ChargerAsync()
|
|
{
|
|
if (!Reseau.EnLigne)
|
|
{
|
|
return; // L'historique n'est pas en cache : rien à demander, l'affichage le dit.
|
|
}
|
|
|
|
try
|
|
{
|
|
_prets = await Api.ListerPretsLivreAsync(LivreId) ?? [];
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_erreur = $"Impossible de charger les prêts : {ex.Message}";
|
|
_prets = [];
|
|
}
|
|
}
|
|
|
|
private void OuvrirFormulaire()
|
|
{
|
|
_emprunteur = string.Empty;
|
|
_datePret = DateTime.Today;
|
|
_erreur = null;
|
|
_formulaireOuvert = true;
|
|
}
|
|
|
|
private async Task SurTouche(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_emprunteur))
|
|
{
|
|
await PreterAsync();
|
|
}
|
|
}
|
|
|
|
private async Task PreterAsync()
|
|
{
|
|
_occupe = true;
|
|
_erreur = null;
|
|
|
|
// La date n'est transmise que si elle diffère d'aujourd'hui : sinon on laisse le serveur
|
|
// horodater à l'instant présent, plus précis que « minuit ce matin ».
|
|
var date = _datePret.Date == DateTime.Today ? (DateTime?)null : _datePret;
|
|
var resultat = await Api.PreterAsync(LivreId, _emprunteur.Trim(), date);
|
|
|
|
_occupe = false;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
return;
|
|
}
|
|
|
|
_formulaireOuvert = false;
|
|
await ChargerAsync();
|
|
await OnChangement.InvokeAsync();
|
|
}
|
|
|
|
private async Task RendreAsync()
|
|
{
|
|
_occupe = true;
|
|
_erreur = null;
|
|
|
|
var resultat = await Api.RendreAsync(EnCours!.Id);
|
|
|
|
_occupe = false;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
|
|
// Le prêt a peut-être été clos ailleurs (autre membre du foyer, autre onglet) :
|
|
// recharger vaut mieux que laisser un bouton qui ne peut plus rien faire.
|
|
await ChargerAsync();
|
|
return;
|
|
}
|
|
|
|
await ChargerAsync();
|
|
await OnChangement.InvokeAsync();
|
|
}
|
|
|
|
private static string Jour(DateTime instant) =>
|
|
instant.ToLocalTime().ToString("dd/MM/yyyy");
|
|
|
|
/// <summary>Durée en clair : « 3 jours », « 2 mois ». Le nombre de jours exact ne parle plus au-delà.</summary>
|
|
internal static string Duree(int jours) => jours switch
|
|
{
|
|
0 => "aujourd'hui",
|
|
1 => "1 jour",
|
|
< 31 => $"{jours} jours",
|
|
< 365 => $"{jours / 30} mois",
|
|
< 730 => "plus d'un an",
|
|
_ => $"plus de {jours / 365} ans",
|
|
};
|
|
}
|