Fusionner la gestion des prets

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-18 02:45:50 +02:00
co-authored by Claude Opus 5
18 changed files with 1656 additions and 13 deletions
+232
View File
@@ -0,0 +1,232 @@
@inject ServiceLivresApi Api
@*
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 (_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>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 async Task OnParametersSetAsync()
{
if (_livreCharge != LivreId)
{
_livreCharge = LivreId;
_prets = null;
await ChargerAsync();
}
}
private async Task ChargerAsync()
{
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",
};
}