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>
160 lines
7.6 KiB
C#
160 lines
7.6 KiB
C#
using MaBibli.Api.Services.Identite;
|
|
using MaBibli.Api.Services.Series;
|
|
using MaBibli.Shared.Dtos;
|
|
|
|
namespace MaBibli.Api.Endpoints;
|
|
|
|
public static class SeriesEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Sagas, cycles et séries : ce qui se lit dans un ordre, et ce qui manque pour le lire.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ <b>Aucun point d'entrée n'accepte ni ne rend d'identité</b>, à une exception près :
|
|
/// la mise en envies. Les séries sont <b>communes au foyer</b>, comme le catalogue et les
|
|
/// prêts — l'ordre de lecture d'une saga ne dépend pas de qui regarde. La liste d'envies,
|
|
/// elle, reste personnelle, et c'est le serveur qui sait à qui il parle.
|
|
/// <para>
|
|
/// La lecture est <b>unique</b> : <c>GET /api/series</c> rend tout l'arbre à plat. Le détail
|
|
/// d'une série s'y lit sans second appel, ce qui donne un seul instantané hors-ligne à
|
|
/// tenir à jour plutôt qu'un par série consultée.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IEndpointRouteBuilder MapSeriesEndpoints(this IEndpointRouteBuilder routes)
|
|
{
|
|
var groupe = routes.MapGroup("/api/series").WithTags("Séries");
|
|
|
|
groupe.MapGet("/", async (IServiceSeries service, CancellationToken ct) =>
|
|
Results.Ok(await service.ListerAsync(ct)))
|
|
.WithName("ListerSeries")
|
|
.WithSummary("Toutes les séries, à plat, chacune avec ses tomes dans l'ordre de lecture.")
|
|
.WithDescription(
|
|
"Le client rebâtit l'arbre à partir de « serieParenteId ». Un tome sans "
|
|
+ "« livreId » est un tome qu'on ne possède pas : c'est ce que l'écran montre.")
|
|
.Produces<IReadOnlyList<SerieDto>>();
|
|
|
|
groupe.MapPost("/", async (
|
|
EnregistrementSerie saisie,
|
|
IServiceSeries service,
|
|
IFournisseurUtilisateur utilisateurs,
|
|
CancellationToken ct) =>
|
|
{
|
|
var resultat = await service.CreerAsync(saisie, utilisateurs.Obtenir().Identifiant, ct);
|
|
|
|
return resultat.Erreur is not null
|
|
? Results.BadRequest(new { erreur = resultat.Erreur })
|
|
: Results.Created($"/api/series/{resultat.Serie!.Id}", resultat.Serie);
|
|
})
|
|
.WithName("CreerSerie")
|
|
.WithSummary("Crée une série. « AjoutePar » est déterminé par le serveur, et n'est qu'une trace.")
|
|
.Produces<SerieDto>(StatusCodes.Status201Created)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
groupe.MapPut("/{id:int}", async (
|
|
int id, EnregistrementSerie saisie, IServiceSeries service, CancellationToken ct) =>
|
|
Repondre(await service.ModifierAsync(id, saisie, ct)))
|
|
.WithName("ModifierSerie")
|
|
.WithSummary("Renomme la série, ou la range dans un cycle.")
|
|
.Produces<SerieDto>()
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapDelete("/{id:int}", async (int id, IServiceSeries service, CancellationToken ct) =>
|
|
await service.SupprimerAsync(id, ct) ? Results.NoContent() : Results.NotFound())
|
|
.WithName("SupprimerSerie")
|
|
.WithSummary("Supprime la série. Ses sous-séries remontent à la racine, elles ne sont pas détruites.")
|
|
.Produces(StatusCodes.Status204NoContent)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapPost("/{id:int}/elements", async (
|
|
int id, AjoutElementSerie saisie, IServiceSeries service, CancellationToken ct) =>
|
|
Repondre(await service.AjouterElementAsync(id, saisie, ct)))
|
|
.WithName("AjouterElementSerie")
|
|
.WithSummary("Ajoute un tome en fin de série : un livre du catalogue, ou un simple titre.")
|
|
.Produces<SerieDto>()
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapPut("/{id:int}/ordre", async (
|
|
int id, OrdreElementsSerie ordre, IServiceSeries service, CancellationToken ct) =>
|
|
Repondre(await service.ReordonnerAsync(id, ordre.Ids, ct)))
|
|
.WithName("ReordonnerSerie")
|
|
.WithSummary("Fixe l'ordre de lecture — la liste entière des tomes, dans l'ordre voulu.")
|
|
.Produces<SerieDto>()
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapPut("/{id:int}/sous-series/ordre", async (
|
|
int id, OrdreSeries ordre, IServiceSeries service, CancellationToken ct) =>
|
|
Repondre(await service.ReordonnerSousSeriesAsync(id, ordre.Ids, ct)))
|
|
.WithName("ReordonnerSousSeries")
|
|
.WithSummary("Fixe l'ordre de lecture des séries directement contenues dans un cycle.")
|
|
.Produces<SerieDto>()
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapPut("/elements/{elementId:int}/livre", async (
|
|
int elementId,
|
|
RattachementLivre rattachement,
|
|
IServiceSeries service,
|
|
CancellationToken ct) =>
|
|
Repondre(await service.RattacherLivreAsync(elementId, rattachement.LivreId, ct)))
|
|
.WithName("RattacherLivreSerie")
|
|
.WithSummary("Rattache un livre possédé à une place, ou l'en détache (« livreId » nul).")
|
|
.WithDescription(
|
|
"Détacher laisse un trou NOMMÉ : le titre du tome est conservé, il ne dépend "
|
|
+ "pas de l'exemplaire.")
|
|
.Produces<SerieDto>()
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapDelete("/elements/{elementId:int}", async (
|
|
int elementId, IServiceSeries service, CancellationToken ct) =>
|
|
await service.RetirerElementAsync(elementId, ct)
|
|
? Results.NoContent()
|
|
: Results.NotFound())
|
|
.WithName("RetirerElementSerie")
|
|
.WithSummary("Retire une place de la série. Le livre, lui, reste au catalogue.")
|
|
.Produces(StatusCodes.Status204NoContent)
|
|
.Produces(StatusCodes.Status404NotFound);
|
|
|
|
groupe.MapPost("/elements/{elementId:int}/souhait", async (
|
|
int elementId,
|
|
IServiceSeries service,
|
|
IFournisseurUtilisateur utilisateurs,
|
|
CancellationToken ct) =>
|
|
{
|
|
// ⚠️ Le seul endroit où une série (commune) touche à une liste d'envies
|
|
// (personnelle). L'envie créée est celle de l'appelant, jamais celle du foyer.
|
|
var resultat = await service.MettreEnEnviesAsync(
|
|
elementId, utilisateurs.Obtenir().Identifiant, ct);
|
|
|
|
return resultat.Erreur is not null
|
|
? Results.BadRequest(new { erreur = resultat.Erreur })
|
|
: Results.Ok(resultat.Souhait);
|
|
})
|
|
.WithName("SouhaiterTomeManquant")
|
|
.WithSummary("Met un tome manquant dans la liste d'envies de l'appelant.")
|
|
.Produces<SouhaitDto>()
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
return routes;
|
|
}
|
|
|
|
private static IResult Repondre(ResultatSerie resultat)
|
|
{
|
|
if (resultat.EstIntrouvable)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return resultat.Erreur is not null
|
|
? Results.BadRequest(new { erreur = resultat.Erreur })
|
|
: Results.Ok(resultat.Serie);
|
|
}
|
|
}
|
|
|
|
/// <summary>Charge utile du rattachement d'un livre à une place. <c>null</c> détache.</summary>
|
|
public record RattachementLivre
|
|
{
|
|
public int? LivreId { get; set; }
|
|
}
|