@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.
*@
Prêt
@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. *@
Un livre numérique ne se prête pas : il n'y a pas d'exemplaire à confier.
}
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)
{
Prêté
à @absent
@if (PreteDepuis is { } depuis)
{
@($" depuis le {Jour(depuis)}")
}
}
else
{
Ce livre était à la maison à la dernière synchronisation.
}
Hors ligne : l'historique des prêts n'est pas consultable, et prêter ou marquer un
retour demande le réseau.
}
else if (_prets is null)
{
Chargement…
}
else
{
@if (_erreur is not null)
{
@_erreur
}
@if (EnCours is { } pret)
{
Prêté
à @pret.Emprunteur
depuis le @Jour(pret.DatePret)
(@Duree(pret.JoursEcoules))
}
else if (!_formulaireOuvert)
{
Ce livre est à la maison.
}
else
{
@* Emprunteur en texte libre : ce sont des gens du dehors, sans compte YunoHost. *@
}
@if (Historique.Count > 0)
{
@* L'historique est la raison d'être de la table : un prêt rendu ne disparaît pas. *@
Prêts passés
@foreach (var passe in Historique)
{
@passe.Emprunteur
du @Jour(passe.DatePret) au @Jour(passe.DateRetour!.Value)
— @Duree(passe.JoursEcoules)
}
}
else if (EnCours is null)
{
Jamais prêté.
}
}
@code {
[Parameter, EditorRequired] public int LivreId { get; set; }
/// Format du livre : un ebook ne se prête pas.
[Parameter] public Format Format { get; set; }
///
/// Emprunteur actuel d'après la fiche. Sert hors-ligne, où l'historique n'est pas
/// consultable mais où l'état courant, lui, vient de l'instantané du catalogue.
///
[Parameter] public string? PreteA { get; set; }
/// Date du prêt en cours, même origine que .
[Parameter] public DateTime? PreteDepuis { get; set; }
/// Prévient la page parente qu'un prêt a changé, pour qu'elle rafraîchisse sa fiche.
[Parameter] public EventCallback OnChangement { get; set; }
private IReadOnlyList? _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 Historique =>
_prets?.Where(p => !p.EstEnCours).ToList() ?? [];
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
/// Au retour du réseau, l'historique devient consultable : on va le chercher.
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");
/// Durée en clair : « 3 jours », « 2 mois ». Le nombre de jours exact ne parle plus au-delà.
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",
};
}