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>
157 lines
5.3 KiB
Plaintext
157 lines
5.3 KiB
Plaintext
@page "/prets"
|
|
@using MaBibli.Shared.Textes
|
|
@inject ServiceLivresApi Api
|
|
@inject EtatReseau Reseau
|
|
@implements IDisposable
|
|
|
|
@*
|
|
Ce qui n'est pas à la maison. C'est l'usage principal des prêts : savoir d'un coup d'œil
|
|
quels livres sont dehors, et chez qui.
|
|
|
|
La liste est commune à tout le foyer, sans filtre par utilisateur : n'importe qui doit
|
|
pouvoir constater qu'un livre manque, et noter son retour quand il le récupère.
|
|
*@
|
|
|
|
<PageTitle>MaBibli — prêts en cours</PageTitle>
|
|
|
|
<h1 class="titre-page">Prêts en cours</h1>
|
|
|
|
@if (_erreur is not null)
|
|
{
|
|
<p class="message-erreur" role="alert">@_erreur</p>
|
|
}
|
|
|
|
@if (_prets is null)
|
|
{
|
|
<p class="message-discret">Chargement…</p>
|
|
}
|
|
else if (_prets.Count == 0)
|
|
{
|
|
@* Hors-ligne, une liste vide peut aussi vouloir dire « rien n'a encore été enregistré ici » :
|
|
on ne l'affirme comme un fait que si l'information vient bien d'une synchronisation. *@
|
|
<p class="message-discret">
|
|
@(Reseau.EnLigne || Reseau.DerniereSynchro is not null
|
|
? "Tous les livres sont à la maison."
|
|
: "Aucune donnée enregistrée sur cet appareil : impossible de dire ce qui est prêté.")
|
|
</p>
|
|
}
|
|
else
|
|
{
|
|
<p class="compteur">@_prets.Count livre@(_prets.Count > 1 ? "s" : "") dehors</p>
|
|
|
|
@* Filtre en mémoire : la liste des prêts en cours est courte, une forme normalisée en
|
|
base serait disproportionnée pour ce seul besoin (voir CLAUDE.md, lot G). La recherche
|
|
reste insensible aux accents et à la casse, comme partout ailleurs dans l'application. *@
|
|
@if (_prets.Count > 1)
|
|
{
|
|
<div class="barre-recherche">
|
|
<input class="champ-saisie" type="search" placeholder="Filtrer par emprunteur…"
|
|
value="@_recherche" @oninput="SurRecherche" aria-label="Filtrer par emprunteur" />
|
|
</div>
|
|
}
|
|
|
|
@if (PretsAffiches().Count == 0)
|
|
{
|
|
<p class="message-discret">Aucun prêt ne correspond à « @_recherche ».</p>
|
|
}
|
|
|
|
<ul class="liste-livres">
|
|
@foreach (var pret in PretsAffiches())
|
|
{
|
|
<li class="carte-livre" @key="pret.Id">
|
|
<Couverture Url="@pret.LivreCoverUrl" Titre="@pret.LivreTitre"
|
|
Classe="couverture-petite" Agrandissable="true" />
|
|
|
|
<div class="carte-corps">
|
|
<a class="carte-titre" href="@($"livres/{pret.LivreId}")">@pret.LivreTitre</a>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(pret.LivreAuteurs))
|
|
{
|
|
<p class="carte-auteur">@pret.LivreAuteurs</p>
|
|
}
|
|
|
|
<p class="carte-details">
|
|
<span class="etiquette etiquette-prete">@pret.Emprunteur</span>
|
|
<span>depuis le @pret.DatePret.ToLocalTime().ToString("dd/MM/yyyy")</span>
|
|
<span class="pret-duree">@PretsLivre.Duree(pret.JoursEcoules)</span>
|
|
</p>
|
|
|
|
@* Le retour se note ici : on récupère un livre, on ouvre la liste des prêts,
|
|
on coche. Passer par la fiche du livre serait un détour. *@
|
|
<button type="button" class="bouton bouton-discret bouton-compact"
|
|
disabled="@(_enCours == pret.Id || !Reseau.EnLigne)"
|
|
title="@(Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne)"
|
|
@onclick="() => RendreAsync(pret)">
|
|
@(_enCours == pret.Id ? "Enregistrement…" : "Rendu")
|
|
</button>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
|
|
@code {
|
|
private IReadOnlyList<PretDto>? _prets;
|
|
private string? _erreur;
|
|
private int? _enCours;
|
|
private string _recherche = string.Empty;
|
|
|
|
private void SurRecherche(ChangeEventArgs e) => _recherche = e.Value?.ToString() ?? string.Empty;
|
|
|
|
private IReadOnlyList<PretDto> PretsAffiches()
|
|
{
|
|
if (_prets is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var terme = NormalisationTexte.Normaliser(_recherche);
|
|
return terme.Length == 0
|
|
? _prets
|
|
: _prets.Where(p => NormalisationTexte.Normaliser(p.Emprunteur).Contains(terme)).ToList();
|
|
}
|
|
|
|
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
|
|
|
|
private void SurChangementReseau() => _ = InvokeAsync(async () =>
|
|
{
|
|
await ChargerAsync();
|
|
StateHasChanged();
|
|
});
|
|
|
|
public void Dispose() => Reseau.Change -= SurChangementReseau;
|
|
|
|
protected override Task OnInitializedAsync() => ChargerAsync();
|
|
|
|
private async Task ChargerAsync()
|
|
{
|
|
_erreur = null;
|
|
|
|
try
|
|
{
|
|
_prets = await Api.ListerPretsEnCoursAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_erreur = $"Impossible de charger les prêts : {ex.Message}";
|
|
_prets = [];
|
|
}
|
|
}
|
|
|
|
private async Task RendreAsync(PretDto pret)
|
|
{
|
|
_enCours = pret.Id;
|
|
var resultat = await Api.RendreAsync(pret.Id);
|
|
_enCours = null;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
}
|
|
|
|
// Rechargement complet plutôt que retrait local : quelqu'un d'autre du foyer a pu rendre
|
|
// ou prêter un livre entre-temps, et cette vue est justement celle qu'on garde ouverte.
|
|
await ChargerAsync();
|
|
}
|
|
}
|