API - CRUD complet sur /api/livres : liste filtrable (format, statut) et cherchable (titre, auteur), consultation, creation, modification, changement de statut isole, suppression. - Aucune lecture ne filtre sur AjoutePar : la bibliotheque est commune au foyer. Le critere n'existe meme pas dans CritereLivres, pour qu'on ne puisse pas s'en servir par inadvertance. Deux tests verrouillent l'invariant, dont un par reflexion sur le type des criteres. - AjoutePar est renseigne par le serveur a la creation, jamais par le client, et n'est pas reecrit a l'edition (pas plus que DateAjout). - Identite lue dans les en-tetes SSOwat (YNH_USER, YNH_USER_EMAIL, YNH_USER_FULLNAME), avec repli sur un utilisateur simule configure en developpement uniquement. Sans en-tete ni simulation, l'identite reste inconnue plutot qu'inventee. Expose par GET /api/moi. - Migration appliquee au demarrage : pas d'etape manuelle a l'installation. Interface Blazor, pensee mobile d'abord - Catalogue unique (physiques et numeriques melanges, distingues par le format) avec recherche, filtres et changement de statut directement dans la liste. - Ajout par ISBN : quand la cascade rend plusieurs notices, un ecran de choix les presente en mettant en avant editeur et annee, seuls elements qui les departagent. Le formulaire reste ensuite entierement modifiable. - Ajout manuel, edition, suppression avec confirmation explicite. - Couvertures : l'URL OpenLibrary n'est jamais validee par un appel prealable (502 intermittents mesures en phase 2) ; un attribut onerror masque l'image cassee et laisse apparaitre un substitut. Decisions prises la ou CLAUDE.md etait muet - Tri alphabetique par titre. - Un ISBN saisi doit etre valide, mais reste facultatif. - Ecran de choix affiche seulement a partir de deux candidats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
113 lines
4.0 KiB
Plaintext
113 lines
4.0 KiB
Plaintext
@*
|
|
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).
|
|
*@
|
|
<div class="formulaire">
|
|
@if (!string.IsNullOrEmpty(Saisie.CoverUrl) || !string.IsNullOrWhiteSpace(Saisie.Titre))
|
|
{
|
|
<div class="formulaire-apercu">
|
|
<Couverture Url="@Saisie.CoverUrl" Titre="@Saisie.Titre" Classe="couverture-moyenne" />
|
|
</div>
|
|
}
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Titre <span class="obligatoire" aria-hidden="true">*</span></span>
|
|
<input class="champ-saisie" type="text" @bind="Saisie.Titre" @bind:event="oninput"
|
|
placeholder="Titre du livre" autocomplete="off" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Auteur</span>
|
|
<input class="champ-saisie" type="text" @bind="Saisie.Auteur" @bind:event="oninput"
|
|
placeholder="Prénom Nom" autocomplete="off" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">Éditeur</span>
|
|
<input class="champ-saisie" type="text" @bind="Saisie.Editeur" @bind:event="oninput"
|
|
autocomplete="off" />
|
|
</label>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">ISBN <span class="champ-aide">(facultatif)</span></span>
|
|
<input class="champ-saisie" type="text" inputmode="numeric" @bind="Saisie.Isbn" @bind:event="oninput"
|
|
placeholder="978…" autocomplete="off" />
|
|
</label>
|
|
|
|
<fieldset class="champ">
|
|
<legend class="champ-libelle">Format</legend>
|
|
<div class="segments">
|
|
@foreach (var format in Formats)
|
|
{
|
|
<button type="button"
|
|
class="segment @(Saisie.Format == format ? "segment-actif" : null)"
|
|
@onclick="() => Saisie.Format = format">
|
|
@Libelles.Format(format)
|
|
</button>
|
|
}
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset class="champ">
|
|
<legend class="champ-libelle">Statut de lecture</legend>
|
|
<div class="segments">
|
|
@foreach (var statut in Statuts)
|
|
{
|
|
<button type="button"
|
|
class="segment @(Saisie.Statut == statut ? "segment-actif" : null)"
|
|
@onclick="() => Saisie.Statut = statut">
|
|
@Libelles.Statut(statut)
|
|
</button>
|
|
}
|
|
</div>
|
|
</fieldset>
|
|
|
|
<label class="champ">
|
|
<span class="champ-libelle">URL de couverture</span>
|
|
<input class="champ-saisie" type="url" @bind="Saisie.CoverUrl" @bind:event="oninput"
|
|
placeholder="https://…" autocomplete="off" />
|
|
</label>
|
|
|
|
@if (!string.IsNullOrEmpty(Erreur))
|
|
{
|
|
<p class="message-erreur" role="alert">@Erreur</p>
|
|
}
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(EnCours || string.IsNullOrWhiteSpace(Saisie.Titre))"
|
|
@onclick="Valider">
|
|
@(EnCours ? "Enregistrement…" : LibelleValidation)
|
|
</button>
|
|
|
|
@if (OnAnnuler.HasDelegate)
|
|
{
|
|
<button type="button" class="bouton bouton-discret" disabled="@EnCours"
|
|
@onclick="OnAnnuler">
|
|
Annuler
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private static readonly Format[] Formats = Enum.GetValues<Format>();
|
|
private static readonly Statut[] Statuts = Enum.GetValues<Statut>();
|
|
|
|
[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; }
|
|
|
|
[Parameter] public EventCallback OnValider { get; set; }
|
|
|
|
[Parameter] public EventCallback OnAnnuler { get; set; }
|
|
|
|
private Task Valider() => OnValider.InvokeAsync();
|
|
}
|