Ajouter les sagas et cycles, avec leurs tomes manquants

Première relation entre livres du projet. Une série contient des séries
(un cycle EST une série de séries) et des PLACES dans l'ordre de lecture —
pas des livres : une place sans livre est le trou qu'on vient voir.

L'ordre est stocké, jamais déduit d'une année : une préquelle se lit avant
le livre paru dix ans plus tôt. Les tomes non possédés se saisissent à la
main, aucune source ne donnant l'ordre de lecture d'une saga.

Le titre de la place survit à la suppression du livre (SetNull, pas de
cascade), sans quoi perdre un exemplaire effacerait le tome 3 de la série.

Les séries sont communes au foyer. Seule exception : la mise en envies d'un
tome manquant, qui écrit dans une liste personnelle — c'est le seul point
où les deux portées se rencontrent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-19 22:03:08 +02:00
co-authored by Claude Opus 5
parent 49eb8e1c98
commit 4e372ab694
20 changed files with 2768 additions and 36 deletions
+95
View File
@@ -0,0 +1,95 @@
namespace MaBibli.Shared.Dtos;
/// <summary>
/// Une série telle qu'exposée par l'API — trilogie, cycle, intégrale.
/// </summary>
/// <remarks>
/// <b>La liste est rendue à plat</b>, chaque série portant l'identifiant de sa parente, et c'est
/// le client qui rebâtit l'arbre. Deux raisons : un JSON récursif se cache mal dans un
/// instantané hors-ligne, et une seule lecture suffit alors à servir la liste <i>et</i> le
/// détail — donc un seul instantané à tenir à jour.
/// </remarks>
public record SerieDto
{
public required int Id { get; init; }
public required string Titre { get; init; }
/// <summary>La série qui contient celle-ci, si elle fait partie d'un cycle.</summary>
public int? SerieParenteId { get; init; }
/// <summary>Rang dans la série parente. Sans objet à la racine.</summary>
public int Position { get; init; }
/// <summary>Les tomes, dans l'ordre de <b>lecture</b>.</summary>
public IReadOnlyList<ElementSerieDto> Elements { get; init; } = [];
/// <summary>Qui a créé la série. Trace d'affichage : la série est commune au foyer.</summary>
public string? AjoutePar { get; init; }
/// <summary>Tomes possédés sur tomes recensés — « 4 sur 7 », le cœur de l'écran.</summary>
public int NombrePossedes => Elements.Count(e => e.Possede);
}
/// <summary>
/// Une place dans l'ordre de lecture, occupée ou non.
/// </summary>
/// <remarks>
/// ⚠️ Un élément sans <see cref="LivreId"/> n'est <b>pas</b> une anomalie : c'est un tome qu'on
/// ne possède pas, et c'est précisément ce que l'écran doit montrer.
/// </remarks>
public record ElementSerieDto
{
public required int Id { get; init; }
public int Position { get; init; }
/// <summary>Titre du tome : celui du livre quand il est rattaché, sinon celui qui a été saisi.</summary>
public required string Titre { get; init; }
public int? LivreId { get; init; }
public bool Possede => LivreId is not null;
/// <summary>Auteurs du livre rattaché, pour ne pas avoir à ouvrir sa fiche.</summary>
public string? Auteurs { get; init; }
/// <summary>À qui le tome est prêté, s'il est dehors. Commun au foyer, comme tout prêt.</summary>
public string? PreteA { get; init; }
}
/// <summary>Charge utile de création ou de renommage d'une série.</summary>
public record EnregistrementSerie
{
public string Titre { get; set; } = string.Empty;
/// <summary>Série parente, pour ranger une trilogie dans un cycle. <c>null</c> = à la racine.</summary>
public int? SerieParenteId { get; set; }
}
/// <summary>
/// Charge utile d'ajout d'un tome : soit un livre du catalogue, soit un simple titre.
/// </summary>
/// <remarks>
/// Les deux voies existent parce qu'une saga se recense d'un coup — souvent avant d'en posséder
/// la moitié — puis se remplit au fil des achats.
/// </remarks>
public record AjoutElementSerie
{
/// <summary>Livre du catalogue à placer ici. <c>null</c> = un tome qu'on ne possède pas.</summary>
public int? LivreId { get; set; }
/// <summary>Titre du tome. Obligatoire sans livre ; repris du livre sinon.</summary>
public string? Titre { get; set; }
}
/// <summary>Nouvel ordre de lecture : la liste <b>entière</b> des identifiants d'éléments.</summary>
/// <remarks>
/// Comme pour la liste d'envies, on prend la liste complète plutôt qu'un déplacement unitaire :
/// une seule opération à vérifier, et c'est ce dont a besoin le glisser-déposer comme les
/// flèches.
/// </remarks>
public record OrdreElementsSerie
{
public List<int> Ids { get; set; } = [];
}