@inject ServiceLivresApi Api
@*
Formulaire d'une fiche livre, partagé par l'ajout (ISBN ou manuel) et l'édition.
Le pré-remplissage par ISBN est une AIDE, pas une contrainte : tous les champs restent
modifiables, et une fiche incomplète peut toujours être complétée à la main (CLAUDE.md).
*@
}
@*
⚠️ L'ORDRE DES CHAMPS EST UNE DÉCISION (lot Q, 2026-08-21), pas une mise en page.
Type de document, puis thèmes, puis auteurs (chacun avec son rôle) : c'est l'ordre dans
lequel on regarde un livre qu'on tient en main, et surtout le type COMMANDE LA SUITE de
la saisie — les rôles ne se posent que là. Relégué en bas, il obligeait à descendre tout le formulaire
pour dire « c'est une BD », c'est-à-dire après avoir saisi ce qui en dépend.
⚠️ Remonter le champ le rend visible ; le PRÉREMPLIR écrirait quelque chose de faux. Le
défaut reste « non précisé », qui ne prétend rien (décision actée) — c'est aussi ce qui
rend l'étiquette honnête dans les listes : elle ne s'affiche que si un type a été choisi.
*@
@*
⚠️ Thèmes et auteurs se saisissent UN PAR UN, et non plus sur une ligne à séparateurs.
Le champ unique se reliait à chaque frappe sur une propriété qui découpait puis
recomposait la liste : le point-virgule tapé y était élagué (entrée vide) et l'espace
qui le suit rogné — le séparateur était le seul caractère qu'on ne pouvait pas taper.
Voir CLAUDE.md.
*@
@*
⚠️ Les thèmes déjà employés sont PROPOSÉS : un thème est un vocabulaire, il ne vaut que
réutilisé. « fantasy » et « Fantaisie » saisis séparément ne regroupent rien, et le
filtre par thème n'y verrait que deux étiquettes sans rapport.
*@
@*
Un auteur par ligne, et son RÔLE avec lui.
⚠️ Le rôle vit sur le LIEN, pas sur la personne : le même auteur scénarise un album et
dessine le suivant. C'est aussi pourquoi renommer une ligne conserve son rôle
(NomsAuteurs) — on corrige un nom, on ne désigne pas quelqu'un d'autre.
⚠️ Les rôles n'apparaissent qu'à partir de DEUX auteurs, décision inchangée : un roman
n'a qu'un signataire dont le rôle ne se pose pas. Le défaut reste « non précisé ».
*@
@if (Saisie.Auteurs.Count > 1)
{
@foreach (var role in Roles)
{
}
}
@*
Le format n'est plus affiché dans les listes que pour les ebooks : un livre y est donc
physique par défaut. Cette valeur DOIT rester présélectionnée ici, sinon on créerait
des fiches sans format en croyant que « rien d'affiché = physique ».
C'est le cas sans code particulier : Format.Physique vaut 0, valeur par défaut de
l'énumération, et le segment correspondant est donc actif dès l'ouverture.
*@
@*
⚠️ Le champ reste VIDE quand on ne sait pas : « 0 » se lirait comme une donnée. C'est
aussi pourquoi la valeur proposée par la BnF arrive ici et non en base directement —
une pagination lue dans une phrase (« 1 vol. (349 p.) ») se corrige d'un geste.
*@
@if (!string.IsNullOrEmpty(Erreur))
{
@Erreur
}
@* Un enregistrement impossible se dit AVANT le clic : un bouton qui échoue en silence est
pire que pas de bouton du tout (CLAUDE.md, stratégie hors-ligne). *@
@if (!string.IsNullOrEmpty(MessageBlocage))
{
@MessageBlocage
}
@if (OnAnnuler.HasDelegate)
{
}
@code {
private static readonly Format[] Formats = Enum.GetValues();
private static readonly TypeDocument[] Types = Enum.GetValues();
private static readonly RoleAuteur[] Roles = Enum.GetValues();
///
/// Pose le rôle d'un auteur.
///
///
/// est un record : on remplace l'entrée plutôt que de la muter,
/// et la liste reste celle que la propriété Auteur saura relire pour préserver les
/// rôles à la frappe suivante.
///
private void PoserRole(int index, RoleAuteur role) =>
Saisie.Auteurs[index] = Saisie.Auteurs[index] with { Role = role };
///
/// Les noms des auteurs, vus comme une liste de chaînes par SaisieListe.
///
///
/// ⚠️ Sa référence doit rester stable tant que la saisie ne change pas : le composant
/// se remet à zéro quand la liste qu'on lui prête change d'identité, et il perdrait donc à
/// chaque rendu ce qui est en cours de frappe. D'où la construction paresseuse, invalidée
/// par OnParametersSet et par elle seule.
///
/// ⚠️ Elle ne peut PAS être construite dans OnParametersSet uniquement : dès qu'un
/// composant a un OnInitializedAsync qui attend vraiment (ici la lecture des thèmes
/// connus), Blazor rend une première fois avant d'appeler OnParametersSet. Ce
/// rendu-là voyait un null, et l'enfant levait une NullReferenceException —
/// invisible à l'écran, qui finissait par s'afficher, mais bien présente en console.
///
///
private NomsAuteurs? _noms;
private NomsAuteurs Noms => _noms ??= new NomsAuteurs(Saisie.Auteurs);
private EnregistrementLivre? _saisiePrecedente;
///
/// Les thèmes déjà employés dans la bibliothèque, proposés sous le champ.
///
///
/// ⚠️ Déduits du catalogue, et non d'un point d'entrée « /api/themes » : la liste des
/// livres est déjà lue partout, elle porte les thèmes, et surtout elle a son instantané —
/// une nouvelle vue de l'API en exigerait un autre, faute de quoi les propositions
/// disparaîtraient hors-ligne. Le catalogue d'un foyer se charge entier, c'est déjà le parti
/// pris de la détection de doublons.
///
/// Une lecture ratée ne coûte rien : sans propositions, le champ redevient celui d'avant.
///
///
private IReadOnlyList _themesConnus = [];
protected override async Task OnInitializedAsync()
{
try
{
_themesConnus =
[
.. (await Api.ListerAsync(new CritereLivres()))
.SelectMany(l => l.Themes)
.DistinctBy(NormalisationTexte.Normaliser, StringComparer.Ordinal)
.OrderBy(NormalisationTexte.Normaliser, StringComparer.Ordinal),
];
}
catch (Exception)
{
_themesConnus = [];
}
}
protected override void OnParametersSet()
{
if (!ReferenceEquals(_saisiePrecedente, Saisie))
{
_saisiePrecedente = Saisie;
_noms = null;
}
}
[Parameter, EditorRequired] public EnregistrementLivre Saisie { get; set; } = new();
[Parameter] public string LibelleValidation { get; set; } = "Enregistrer";
[Parameter] public string? Erreur { get; set; }
[Parameter] public bool EnCours { get; set; }
///
/// Raison pour laquelle l'enregistrement est impossible (typiquement l'absence de réseau).
/// Renseignée, elle désactive la validation et s'affiche : la saisie reste lisible, mais
/// l'utilisateur sait pourquoi il ne peut pas valider.
///
[Parameter] public string? MessageBlocage { get; set; }
private bool Bloque => !string.IsNullOrEmpty(MessageBlocage);
[Parameter] public EventCallback OnValider { get; set; }
[Parameter] public EventCallback OnAnnuler { get; set; }
private Task Valider() => OnValider.InvokeAsync();
}