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; } }