Gerer les prets : API, ecrans et contrainte en base

L'entite Pret existait depuis le squelette sans jamais etre exploitee. Cette
phase la met en service de bout en bout.

API
- GET  /api/prets/en-cours       ce qui n'est pas a la maison, du plus ancien
                                 au plus recent : on cherche le livre oublie,
                                 pas celui prete hier
- POST /api/livres/{id}/prets    preter
- GET  /api/livres/{id}/prets    historique complet, du plus recent au plus
                                 ancien
- POST /api/prets/{id}/retour    clore le pret sans le supprimer

Un livre deja sorti ne peut pas etre prete une seconde fois. Le service le
verifie et nomme celui qui l'a deja, mais entre sa verification et l'insertion
il reste une fenetre : un index unique PARTIEL (LivreId WHERE DateRetour IS
NULL) la ferme, tout en laissant l'historique accumuler autant de prets clos
que necessaire sur le meme livre.

Les ebooks sont refuses cote API, pas seulement grises dans l'interface : une
fiche n'a pas d'exemplaire a confier.

Les prets sont COMMUNS au foyer, symetrique inverse du statut de lecture. Le
service ne recoit meme pas d'identite, pour qu'on ne puisse pas s'en servir
par inadvertance.

Interface, pensee mobile d'abord
- ecran « Prets en cours » avec bouton « Rendu » a meme la liste
- bloc pret sur la fiche d'un livre : etat courant, action, puis historique
- etiquette « Prete a X » dans le catalogue
- la barre d'actions passe sur deux lignes plutot que de comprimer ses
  libelles maintenant qu'elle compte quatre entrees

Toutes les dates sont en UTC ; le client convertit la date locale du
<input type="date"> avant l'envoi, faute de quoi le pret se decalerait d'un
jour pour la moitie du globe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-18 02:45:01 +02:00
co-authored by Claude Opus 5
parent ab878b4d3c
commit 2a3dfb88de
18 changed files with 1656 additions and 13 deletions
+107
View File
@@ -0,0 +1,107 @@
@page "/prets"
@inject ServiceLivresApi Api
@*
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)
{
<p class="message-discret">Tous les livres sont à la maison.</p>
}
else
{
<p class="compteur">@_prets.Count livre@(_prets.Count > 1 ? "s" : "") dehors</p>
<ul class="liste-livres">
@foreach (var pret in _prets)
{
<li class="carte-livre" @key="pret.Id">
<Couverture Url="@pret.LivreCoverUrl" Titre="@pret.LivreTitre" Classe="couverture-petite" />
<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)"
@onclick="() => RendreAsync(pret)">
@(_enCours == pret.Id ? "Enregistrement…" : "Rendu")
</button>
</div>
</li>
}
</ul>
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="">Catalogue</a>
</nav>
@code {
private IReadOnlyList<PretDto>? _prets;
private string? _erreur;
private int? _enCours;
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();
}
}