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>
209 lines
7.3 KiB
C#
209 lines
7.3 KiB
C#
using MaBibli.Api.Data;
|
|
using MaBibli.Shared.Dtos;
|
|
using MaBibli.Shared.Entites;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace MaBibli.Api.Services.Souhaits;
|
|
|
|
/// <summary>Issue d'une écriture d'envie de revue : la ligne, un message, ou rien du tout.</summary>
|
|
public readonly record struct ResultatRevueSouhaitee(
|
|
RevueSouhaiteeDto? Envie, string? Erreur, bool NExistePas = false)
|
|
{
|
|
public static readonly ResultatRevueSouhaitee Introuvable = new(null, null, true);
|
|
|
|
public static ResultatRevueSouhaitee Ok(RevueSouhaiteeDto envie) => new(envie, null);
|
|
|
|
public static ResultatRevueSouhaitee Invalide(string message) => new(null, message);
|
|
}
|
|
|
|
public interface IServiceRevuesSouhaitees
|
|
{
|
|
Task<IReadOnlyList<RevueSouhaiteeDto>> ListerAsync(
|
|
string? utilisateur, CancellationToken ct = default);
|
|
|
|
Task<ResultatRevueSouhaitee> AjouterAsync(
|
|
EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default);
|
|
|
|
Task<ResultatRevueSouhaitee> ModifierAsync(
|
|
int id, EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default);
|
|
|
|
Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Les envies de revues et de numéros, <b>personnelles</b>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Mêmes règles que <see cref="ServiceSouhaits"/>, sans exception : toute méthode filtre sur
|
|
/// l'appelant, aucun point d'entrée n'accepte de nom d'utilisateur, et l'envie d'un autre est
|
|
/// simplement introuvable.
|
|
/// <para>
|
|
/// ⚠️ Il n'y a <b>pas</b> de rapprochement avec les revues possédées, contrairement aux envies
|
|
/// de livres. Ce n'est pas un oubli : « je possède la revue mais pas ce numéro » et « je possède
|
|
/// ce numéro » demandent deux réponses différentes, et un signalement approximatif sur une liste
|
|
/// d'envies coûte plus qu'il ne rapporte.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class ServiceRevuesSouhaitees(MaBibliDbContext db) : IServiceRevuesSouhaitees
|
|
{
|
|
public async Task<IReadOnlyList<RevueSouhaiteeDto>> ListerAsync(
|
|
string? utilisateur, CancellationToken ct = default)
|
|
{
|
|
if (utilisateur is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var envies = await db.RevuesSouhaitees
|
|
.AsNoTracking()
|
|
.Where(e => e.Utilisateur == utilisateur)
|
|
.OrderBy(e => e.Rang)
|
|
.ThenBy(e => e.Id)
|
|
.ToListAsync(ct);
|
|
|
|
return envies.Select(Projeter).ToList();
|
|
}
|
|
|
|
public async Task<ResultatRevueSouhaitee> AjouterAsync(
|
|
EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default)
|
|
{
|
|
if (utilisateur is null)
|
|
{
|
|
return ResultatRevueSouhaitee.Invalide(
|
|
"Impossible d'ajouter une envie sans savoir à qui elle appartient.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(saisie.Titre))
|
|
{
|
|
return ResultatRevueSouhaitee.Invalide("Le titre de la revue est obligatoire.");
|
|
}
|
|
|
|
var envie = new RevueSouhaitee
|
|
{
|
|
Utilisateur = utilisateur,
|
|
Titre = saisie.Titre,
|
|
Numero = saisie.Numero,
|
|
Issn = saisie.Issn,
|
|
Note = Vide(saisie.Note),
|
|
DateAjout = DateTime.UtcNow,
|
|
};
|
|
|
|
// ⚠️ Canonise l'ISSN au passage : « 24666718 » devient « 2466-6718 », la seule forme
|
|
// rangée en base et la seule que la BnF interroge.
|
|
envie.RecalculerFormes();
|
|
|
|
if (await ExisteDejaAsync(envie, null, ct))
|
|
{
|
|
return ResultatRevueSouhaitee.Invalide(Deja(envie));
|
|
}
|
|
|
|
// En fin de section, comme une envie de livre : on vient de la noter, on n'a pas dit
|
|
// qu'on la voulait plus que les autres.
|
|
var dernierRang = await db.RevuesSouhaitees
|
|
.Where(e => e.Utilisateur == utilisateur)
|
|
.Select(e => (int?)e.Rang)
|
|
.MaxAsync(ct);
|
|
|
|
envie.Rang = (dernierRang ?? -1) + 1;
|
|
|
|
db.RevuesSouhaitees.Add(envie);
|
|
await db.SaveChangesAsync(ct);
|
|
|
|
return ResultatRevueSouhaitee.Ok(Projeter(envie));
|
|
}
|
|
|
|
public async Task<ResultatRevueSouhaitee> ModifierAsync(
|
|
int id, EnregistrementRevueSouhaitee saisie, string? utilisateur, CancellationToken ct = default)
|
|
{
|
|
if (utilisateur is null)
|
|
{
|
|
return ResultatRevueSouhaitee.Introuvable;
|
|
}
|
|
|
|
// Le filtre fait partie de la clé de recherche : l'envie d'un autre n'est pas refusée,
|
|
// elle n'est pas trouvée.
|
|
var envie = await db.RevuesSouhaitees
|
|
.FirstOrDefaultAsync(e => e.Id == id && e.Utilisateur == utilisateur, ct);
|
|
|
|
if (envie is null)
|
|
{
|
|
return ResultatRevueSouhaitee.Introuvable;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(saisie.Titre))
|
|
{
|
|
return ResultatRevueSouhaitee.Invalide("Le titre de la revue est obligatoire.");
|
|
}
|
|
|
|
envie.Titre = saisie.Titre;
|
|
envie.Numero = saisie.Numero;
|
|
envie.Issn = saisie.Issn;
|
|
envie.Note = Vide(saisie.Note);
|
|
envie.RecalculerFormes();
|
|
|
|
if (await ExisteDejaAsync(envie, envie.Id, ct))
|
|
{
|
|
return ResultatRevueSouhaitee.Invalide(Deja(envie));
|
|
}
|
|
|
|
// Rang inchangé : corriger un numéro ne dit rien de l'ordre voulu.
|
|
await db.SaveChangesAsync(ct);
|
|
|
|
return ResultatRevueSouhaitee.Ok(Projeter(envie));
|
|
}
|
|
|
|
public async Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default)
|
|
{
|
|
if (utilisateur is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var envie = await db.RevuesSouhaitees
|
|
.FirstOrDefaultAsync(e => e.Id == id && e.Utilisateur == utilisateur, ct);
|
|
|
|
if (envie is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
db.RevuesSouhaitees.Remove(envie);
|
|
await db.SaveChangesAsync(ct);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Le doublon est refusé <b>avec un message</b>, jamais laissé à l'index unique.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ L'entité est déjà rattachée au contexte lors d'une modification : d'où l'exclusion
|
|
/// explicite de sa propre ligne, sans quoi toute édition se dirait doublon d'elle-même.
|
|
/// </remarks>
|
|
private Task<bool> ExisteDejaAsync(RevueSouhaitee envie, int? saufId, CancellationToken ct) =>
|
|
db.RevuesSouhaitees.AnyAsync(
|
|
e => e.Utilisateur == envie.Utilisateur
|
|
&& e.TitreNormalise == envie.TitreNormalise
|
|
&& e.NumeroNormalise == envie.NumeroNormalise
|
|
&& (saufId == null || e.Id != saufId),
|
|
ct);
|
|
|
|
private static string Deja(RevueSouhaitee envie) =>
|
|
envie.Numero is null
|
|
? $"« {envie.Titre} » est déjà dans votre liste d'envies."
|
|
: $"« {envie.Titre} n° {envie.Numero} » est déjà dans votre liste d'envies.";
|
|
|
|
private static string? Vide(string? valeur) =>
|
|
string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim();
|
|
|
|
/// <summary>Projection vers le DTO, <b>sans</b> le champ <c>Utilisateur</c>.</summary>
|
|
private static RevueSouhaiteeDto Projeter(RevueSouhaitee envie) => new()
|
|
{
|
|
Id = envie.Id,
|
|
Titre = envie.Titre,
|
|
Numero = envie.Numero,
|
|
Issn = envie.Issn,
|
|
Note = envie.Note,
|
|
DateAjout = envie.DateAjout,
|
|
};
|
|
}
|