namespace MaBibli.Shared.Catalogue;
///
/// Où en est un code scanné dans une rafale.
///
///
/// ⚠️ C'est l'existence de ces états qui rend la reprise possible, et la reprise est ce
/// qui distingue une rafale d'une simple liste de codes. Sans , reprendre une
/// rafale à moitié traitée recréerait les livres déjà enregistrés — la case « passer si déjà
/// existant » les rattraperait sans doute, mais par accident, pas par construction.
///
public enum EtatCodeRafale
{
/// Scanné, pas encore interrogé.
ATraiter = 0,
/// Livre enregistré. Ne jamais le refaire à la reprise.
Cree,
/// Déjà au catalogue, écarté par la case à cocher.
Passe,
/// Plusieurs notices : l'utilisateur doit choisir l'édition.
AChoisir,
/// Aucune source ne connaît ce code. À saisir à la main.
Introuvable,
/// Code de périodique (préfixe 977) : mis de côté, traité à la fin.
Revue,
/// La source n'a pas répondu. ⚠️ À distinguer d'.
Echec,
}
///
/// Un code de la file, et ce qu'on en sait.
///
public record CodeRafale
{
public required string Code { get; init; }
public EtatCodeRafale Etat { get; init; } = EtatCodeRafale.ATraiter;
/// Titre trouvé, ou saisi. Sert à reconnaître la ligne dans le compte rendu.
public string? Titre { get; init; }
/// Renseigné quand l'état est .
public int? LivreId { get; init; }
///
/// Ce qui s'est passé, écrit pour un humain.
///
///
/// ⚠️ Jamais un message HTTP brut : c'est la règle déjà tenue partout ailleurs, et le compte
/// rendu d'une rafale est précisément un endroit où l'on est tenté de recopier l'erreur telle
/// quelle pour aller vite.
///
public string? Motif { get; init; }
/// Reste-t-il quelque chose à faire sur cette ligne ?
public bool Termine => Etat is EtatCodeRafale.Cree or EtatCodeRafale.Passe;
}
///
/// Une rafale de catalogage : les codes scannés, et où en est chacun.
///
///
/// Persistée dans IndexedDB, magasin rafale — séparé des instantanés, qui portent
/// ce que le serveur a rendu. Celle-ci porte ce que l'utilisateur a scanné, et qui n'existe
/// nulle part ailleurs.
///
/// ⚠️ Ce ne sont PAS des écritures en attente, et la nuance est ce qui la rend acceptable
/// dans un projet qui refuse toute file de synchronisation : ce sont des codes à
/// interroger. Rien n'est promis à la base, rien ne peut entrer en conflit, et l'utilisateur
/// voit sa liste. Ne pas confondre les deux, sous peine de rouvrir la question.
///
///
public record Rafale
{
public List Codes { get; init; } = [];
///
/// Passer les livres déjà au catalogue plutôt que de demander à chaque fois.
///
///
/// Coché par défaut : quand on catalogue cinquante livres, on ne veut pas être arrêté
/// cinquante fois. ⚠️ Mais le compte rendu doit dire combien ont été passés — posséder
/// deux exemplaires est légitime (c'est pourquoi l'ISBN ne porte pas d'index unique), et
/// passer en silence contredirait cet esprit.
///
public bool PasserLesDoublons { get; init; } = true;
///
/// Série à laquelle rattacher chaque livre créé, quand la rafale a été lancée depuis une saga.
///
///
/// ⚠️ Elle vit dans la file et non dans l'écran : une rafale se reprend plus tard, et
/// la reprise doit rattacher au même endroit. Elle est reprise du paramètre d'adresse au
/// lancement, une fois pour toutes.
///
public int? SerieId { get; init; }
public DateTimeOffset Debut { get; init; } = DateTimeOffset.UtcNow;
public int Total => Codes.Count;
public int Traites => Codes.Count(c => c.Etat != EtatCodeRafale.ATraiter);
public int Crees => Codes.Count(c => c.Etat == EtatCodeRafale.Cree);
public int Passes => Codes.Count(c => c.Etat == EtatCodeRafale.Passe);
/// Ce qui attend encore une décision de l'utilisateur.
public IReadOnlyList Residus =>
[.. Codes.Where(c => c.Etat
is EtatCodeRafale.AChoisir or EtatCodeRafale.Introuvable
or EtatCodeRafale.Revue or EtatCodeRafale.Echec)];
public bool ResteATraiter => Codes.Any(c => c.Etat == EtatCodeRafale.ATraiter);
/// Pourcentage traité, pour l'indicateur d'avancement.
public int Pourcentage => Total == 0 ? 0 : Traites * 100 / Total;
}
///
/// Découpe une saisie en codes, dans l'ordre, sans doublon.
///
public static class SaisieRafale
{
///
/// Un code par ligne — c'est ce que produit une douchette, qui envoie Entrée après
/// chaque lecture. Les espaces et tirets sont tolérés, la casse aussi.
///
///
/// ⚠️ Les doublons de la saisie sont écartés ici, et non au traitement : scanner deux
/// fois le même livre par mégarde est le geste le plus banal d'une rafale, et interroger la
/// BnF deux fois pour le même code ne peut rien apporter. Le doublon avec le catalogue,
/// lui, est une tout autre question, traitée par PasserLesDoublons.
///
public static IReadOnlyList Decouper(string? saisie)
{
if (string.IsNullOrWhiteSpace(saisie))
{
return [];
}
var vus = new HashSet(StringComparer.Ordinal);
var codes = new List();
foreach (var ligne in saisie.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var code = new string([.. ligne.Where(c => char.IsAsciiDigit(c) || c is 'X' or 'x')])
.ToUpperInvariant();
if (code.Length > 0 && vus.Add(code))
{
codes.Add(code);
}
}
return codes;
}
}