- Cataloguer en rafale — nouvel écran /ajout/rafale : on scanne une pile de livres à la suite dans une zone de texte, chaque code est traité (BnF puis OpenLibrary), les doublons connus sont passés automatiquement. La collecte marche même hors-ligne. Le compte rendu liste maintenant les livres créés, en lien vers leur fiche, et reste consultable en revenant sur l'écran même après une rafale entièrement réussie. Le catalogue reconnaît un ISBN dans sa barre de recherche (13 ou 10 chiffres, avec ou sans tirets) : scanner un livre en main ouvre directement sa fiche s'il n'y en a qu'un. Un bouton « Scanner » l'alimente, actif hors-ligne. - Ajouter un tome à une série accepte aussi un ISBN dans le champ manuel : le catalogue est cherché d'abord (rattachement direct si un seul exemplaire), sinon la BnF prend le relais. Séries et sagas - Numéro de tome distinct de la position de lecture : on peut indiquer « c'est le tome 7 » même si on ne possède pas les six premiers ; l'ordre de lecture reste un réglage séparé (utile pour les préquelles). - Tri par numéro en plus du tri par ordre de lecture quand des tomes en portent un. - Panneau « Ajouter » regroupé et repliable sur la fiche d'une série (manuellement / en rafale / depuis le catalogue / depuis les envies), au lieu de quatre formulaires ouverts en permanence. - Filtre catalogue « sans couverture » pour repérer les livres à illustrer. Le catalogue groupe les tomes d'une même série sous un bloc repliable, avec un décompte plus clair (affichés / possédés / total). - Corrections directes sur la fiche - Effacer un prêt saisi par erreur (bouton ✕ sur chaque ligne, avec confirmation), sans passer par « rendre ». - Corriger une couverture manquante ou cassée en cliquant dessus : le champ d'adresse s'ouvre focalisé, Entrée enregistre. Étendu aux numéros de revue dans la dernière modification. - Les thèmes déjà utilisés dans la bibliothèque sont proposés à la frappe. - La recherche d'un livre à rattacher montre des suggestions dès le focus, sans attendre de taper. - Visuel : un rendu manquant après une écriture asynchrone dans le formulaire de livre, une bascule de rôle cassée, des débordements à 320 px, et le style d'un bouton-lien qui restait souligné.
114 lines
5.2 KiB
C#
114 lines
5.2 KiB
C#
using MaBibli.Api.Services.Prets;
|
|
using MaBibli.Shared.Dtos;
|
|
|
|
namespace MaBibli.Api.Endpoints;
|
|
|
|
public static class PretsEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Prêts : qui a quoi, depuis quand, et l'historique de ce qui est revenu.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <b>Aucun point d'entrée ne filtre sur l'utilisateur courant</b>, et aucun n'enregistre qui
|
|
/// a saisi le prêt. La bibliothèque est commune : tout le foyer voit les mêmes prêts, et
|
|
/// n'importe qui peut noter le retour d'un livre qu'il a récupéré.
|
|
/// <para>
|
|
/// L'emprunteur est du <b>texte libre</b> : ce sont des personnes extérieures au foyer, sans
|
|
/// compte YunoHost. Ne jamais le rapprocher d'un identifiant d'utilisateur.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IEndpointRouteBuilder MapPretsEndpoints(this IEndpointRouteBuilder routes)
|
|
{
|
|
var prets = routes.MapGroup("/api/prets").WithTags("Prêts");
|
|
|
|
prets.MapGet("/en-cours", async (IServicePrets service, CancellationToken ct) =>
|
|
Results.Ok(await service.EnCoursAsync(ct)))
|
|
.WithName("ListerPretsEnCours")
|
|
.WithSummary("Les livres qui ne sont pas à la maison, du prêt le plus ancien au plus récent.")
|
|
.WithDescription(
|
|
"L'ordre est volontairement croissant : ce qu'on cherche ici, c'est le livre "
|
|
+ "sorti depuis des mois qu'on avait oublié, pas celui prêté hier.")
|
|
.Produces<IReadOnlyList<PretDto>>();
|
|
|
|
prets.MapPost("/{id:int}/retour", async (
|
|
int id,
|
|
RetourPret retour,
|
|
IServicePrets service,
|
|
CancellationToken ct) =>
|
|
{
|
|
var resultat = await service.RendreAsync(id, retour, ct);
|
|
|
|
if (resultat.EstIntrouvable)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return resultat.Erreur is not null
|
|
? Results.BadRequest(new { erreur = resultat.Erreur })
|
|
: Results.Ok(resultat.Pret);
|
|
})
|
|
.WithName("MarquerPretRendu")
|
|
.WithSummary("Clôt un prêt en posant sa date de retour. Le prêt n'est pas supprimé : il devient l'historique.")
|
|
.Produces<PretDto>()
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
prets.MapDelete("/{id:int}", async (int id, IServicePrets service, CancellationToken ct) =>
|
|
{
|
|
var supprime = await service.SupprimerAsync(id, ct);
|
|
return supprime ? Results.NoContent() : Results.NotFound();
|
|
})
|
|
.WithName("SupprimerPret")
|
|
.WithSummary("Efface un prêt saisi par erreur — ce n'est pas « rendre ».")
|
|
.WithDescription(
|
|
"Rendre pose une date de retour et conserve la ligne : c'est l'histoire du livre. "
|
|
+ "Effacer défait une saisie fautive (mauvais emprunteur, mauvaise date, prêt noté "
|
|
+ "deux fois). Un historique juste vaut mieux qu'un historique complet mais faux.")
|
|
.Produces(StatusCodes.Status204NoContent)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
var parLivre = routes.MapGroup("/api/livres/{livreId:int}/prets").WithTags("Prêts");
|
|
|
|
parLivre.MapGet("/", async (int livreId, IServicePrets service, CancellationToken ct) =>
|
|
{
|
|
var historique = await service.HistoriqueAsync(livreId, ct);
|
|
return historique is null ? Results.NotFound() : Results.Ok(historique);
|
|
})
|
|
.WithName("HistoriquePretsLivre")
|
|
.WithSummary("Tous les prêts de ce livre, clos comme en cours, du plus récent au plus ancien.")
|
|
.WithDescription(
|
|
"Une liste vide signifie « jamais prêté » ; un livre inconnu répond 404. "
|
|
+ "Les prêts clos ne disparaissent jamais : c'est la raison d'être de la table.")
|
|
.Produces<IReadOnlyList<PretDto>>()
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
parLivre.MapPost("/", async (
|
|
int livreId,
|
|
EnregistrementPret saisie,
|
|
IServicePrets service,
|
|
CancellationToken ct) =>
|
|
{
|
|
var resultat = await service.PreterAsync(livreId, saisie, ct);
|
|
|
|
if (resultat.EstIntrouvable)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return resultat.Erreur is not null
|
|
? Results.BadRequest(new { erreur = resultat.Erreur })
|
|
: Results.Created($"/api/prets/{resultat.Pret!.Id}", resultat.Pret);
|
|
})
|
|
.WithName("PreterLivre")
|
|
.WithSummary("Prête ce livre à quelqu'un.")
|
|
.WithDescription(
|
|
"Refusé si le livre est déjà sorti (un exemplaire ne se prête pas deux fois) ou "
|
|
+ "s'il est au format numérique (une fiche d'ebook n'a pas d'exemplaire à confier).")
|
|
.Produces<PretDto>(StatusCodes.Status201Created)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
return routes;
|
|
}
|
|
}
|