Flexibele kluissoorten: soorten in de database, beheer op de website

- Tabellen vault_categories en vault_category_fields (db/21), startrijen handmatig (db/handmatig/21b)
- VaultCategoryService met regels voor zichtbaarheid, gebruik en eigen velden
- Beheer > Kluissoorten (/beheer/soorten) met gebruikstellers; soort in gebruik niet te verwijderen
- Kluis en Nalatenschap (web en app) tonen soorten en eigen velden uit de database
- Socketprotocol: vault.categories; GRANTs voor mijnbesluit_app in db/02
- db/20 herhaalbaar gemaakt
- App versie 1.1 (versiecode 3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MGLqt1uKfNARQHut9ifcx
This commit is contained in:
jf
2026-10-20 08:15:40 +00:00
co-authored by Claude Sonnet 5.5
parent a3a0bcfc05
commit af13d0e533
36 changed files with 2540 additions and 94 deletions
@@ -1,5 +1,7 @@
using MijnBesluit.Application.Models;
using MijnBesluit.Domain.Entities;
using MijnBesluit.Domain.Enums;
using MijnBesluit.Domain.Rules;
namespace MijnBesluit.Web.Components.Onderdelen;
@@ -14,8 +16,11 @@ namespace MijnBesluit.Web.Components.Onderdelen;
/// </summary>
public static class Kluisweergave
{
/// <summary>De ingevulde velden van een item, in de volgorde waarin je ze wilt lezen.</summary>
public static IEnumerable<(string Label, string Waarde)> Velden(VaultPayload p)
/// <summary>
/// De ingevulde velden van een item, in de volgorde waarin je ze wilt lezen. Bij een soort die
/// de beheerder zelf maakte staan de eigen velden erbij; geef daarvoor de soort mee.
/// </summary>
public static IEnumerable<(string Label, string Waarde)> Velden(VaultPayload p, KluisSoort? soort = null)
{
if (p.Website is not null) yield return ("Website", p.Website);
if (p.Username is not null) yield return ("Gebruikersnaam", p.Username);
@@ -33,9 +38,35 @@ public static class Kluisweergave
if (p.Music is not null) yield return ("Muziek", p.Music);
if (p.Speakers is not null) yield return ("Sprekers", p.Speakers);
if (p.Wishes is not null) yield return ("Wensen", p.Wishes);
foreach (var eigen in EigenVelden(p, soort)) yield return eigen;
if (p.Notes is not null) yield return ("Toelichting", p.Notes);
}
/// <summary>
/// De velden van een soort die de beheerder zelf maakte, met de naam die hij ze gaf. Alleen wat
/// is ingevuld; ook een veld dat inmiddels niet-actief is blijft zichtbaar zolang er iets in staat.
/// </summary>
public static IEnumerable<(string Label, string Waarde)> EigenVelden(VaultPayload p, KluisSoort? soort)
{
if (soort is null || p.Velden is null) yield break;
foreach (var veld in soort.Velden.Where(v => v.Soort != VeldSoort.Uitleg))
{
if (!p.Velden.TryGetValue(veld.Sleutel, out var waarde) || string.IsNullOrWhiteSpace(waarde)) continue;
if (veld.Soort == VeldSoort.Datum &&
DateOnly.TryParseExact(waarde, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var dag))
waarde = $"{dag.Day} {Datums.Maanden[dag.Month - 1]} {dag.Year}";
yield return (veld.Label, waarde);
}
}
/// <summary>De naam van een soort: die van de beheerder als de server hem kent, anders de vaste.</summary>
public static string Naam(VaultCategory c, IReadOnlyDictionary<int, string>? namen) =>
namen is not null && namen.TryGetValue((int)c, out var naam) ? naam : Naam(c);
public static string Naam(VaultCategory c) => c switch
{
VaultCategory.Uitvaartwens => "Uitvaartwens",
@@ -31,6 +31,10 @@ else
hoge uitzondering. *@
<p><a href="/beheer/adverteren">Advertenties beheren &rsaquo;</a></p>
@* Kluissoorten: wat mensen in hun kluis kunnen vastleggen, en wat je daar zelf bij zet.
Ook een eigen scherm: dat is inrichten, geen terugdraaien. *@
<p><a href="/beheer/soorten">Kluissoorten beheren &rsaquo;</a></p>
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
@* ------------------------------------------------------------ statistieken *@
@@ -0,0 +1,486 @@
@page "/beheer/soorten"
@attribute [Authorize]
@inject BeheerService Beheerkant
@inject VaultCategoryService SoortService
@rendermode InteractiveServer
<PageTitle>Kluissoorten - Beheer</PageTitle>
@* Het beheerscherm voor de soorten kluisitems: wat mensen in hun kluis kunnen vastleggen
(Wachtwoord, Polis, Uitvaartwens, en wat je er zelf bij zet, zoals "Niet reanimeren").
Drie regels, in de volgorde waarin ze ertoe doen:
1. Wat in gebruik is, verdwijnt nooit. Een soort met items is alleen op niet-actief te
zetten; wie hem al gebruikt blijft hem zien, wie niet ziet hem niet meer.
2. Je ziet hier aantallen, nooit inhoud of namen. Hoe vaak is een tel, wie en wat niet.
3. Een nieuwe soort vraagt geen nieuwe versie van de app: de app tekent het formulier naar de
velden die je hier opgeeft.
Zie docs/Kluissoorten.md. *@
@if (!Magdit)
{
<h1>Niet gevonden</h1>
<p class="zacht">Deze pagina bestaat niet, of hij is niet voor jou.</p>
}
else
{
<h1>Kluissoorten</h1>
<p class="zacht">
Wat mensen in hun kluis kunnen vastleggen. Een nieuwe soort verschijnt meteen in de app en op
de website, zonder dat er een nieuwe versie van de app nodig is. Bij elke soort zie je hoe
vaak hij gebruikt wordt; wat erin staat zie je nooit.
</p>
<p><a href="/beheer">&lsaquo; Terug naar beheer</a></p>
<div class="melding let-op">
<strong>De naam van een soort is leesbaar voor de server.</strong> Alleen de inhoud van een
kluisitem is versleuteld, de naam van de soort niet. Wie "Dementieverklaring" als soort
aanmaakt, zet dat woord dus in de database. Kies een neutrale naam als de soort zelf iets
over iemands gezondheid zegt, en zet het gevoelige deel in de velden: die waarden blijven
versleuteld.
</div>
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
@* ------------------------------------------------------------ nieuwe soort *@
<div class="kaart">
<h2 style="margin-top:0">Nieuwe soort</h2>
<div class="raster twee">
<div class="veld">
<label for="nieuwenaam">Naam</label>
<input id="nieuwenaam" type="text" maxlength="80" @bind="NieuweSoort.Naam" @bind:event="oninput"
placeholder="Bijvoorbeeld: Niet reanimeren" />
</div>
<div class="veld">
<label for="nieuweuitleg">Uitleg onder de keuzelijst (mag leeg)</label>
<input id="nieuweuitleg" type="text" maxlength="500" @bind="NieuweSoort.Uitleg" @bind:event="oninput" />
</div>
</div>
<div class="keuze">
<input type="checkbox" id="nieuwbespreken" @bind="NieuweSoort.BesprekenMet" />
<label for="nieuwbespreken">Bij deze soort kan de eigenaar iemand kiezen om het vooraf mee te bespreken</label>
</div>
<div class="rij" style="margin-top:.8rem">
<button class="knop" disabled="@Bezig" @onclick="Aanmaken">Soort toevoegen</button>
</div>
<p class="zacht klein" style="margin-bottom:0">
Na het toevoegen kun je de velden van het formulier invullen en bepalen of de soort
actief is. Een soort zonder velden toont alleen de titel en de toelichting.
</p>
</div>
@* ------------------------------------------------------------ de lijst *@
@if (Lijst is null)
{
<p class="zacht">Bezig met laden&hellip;</p>
}
else
{
<div class="stapel">
@foreach (var s in Lijst)
{
<div class="kaart">
<div class="rij">
<div class="groei">
<div class="groot">@s.Naam</div>
<div class="zacht">@Gebruik(s)</div>
<div style="margin-top:.35rem;display:flex;gap:.4rem;flex-wrap:wrap">
@if (s.Ingebouwd) { <span class="label">Ingebouwd</span> }
else { <span class="label geclaimd">Eigen soort</span> }
@if (!s.Actief) { <span class="label uitgenodigd">Niet actief</span> }
@if (s.BesprekenMet) { <span class="label">Bespreken met</span> }
</div>
</div>
<button class="knop rustig" @onclick="() => Openen(s)">
@(Open == s.Id ? "Sluiten" : "Beheren")
</button>
</div>
@if (Open == s.Id)
{
<hr />
<div class="raster twee">
<div class="veld">
<label>Naam</label>
<input type="text" maxlength="80" @bind="Bewerken.Naam" @bind:event="oninput" />
</div>
<div class="veld">
<label>Plek in de lijst</label>
<input type="number" @bind="Bewerken.Volgorde" />
<div class="uitleg">Kleinste nummer staat bovenaan.</div>
</div>
</div>
<div class="veld">
<label>Uitleg onder de keuzelijst</label>
<input type="text" maxlength="500" @bind="Bewerken.Uitleg" @bind:event="oninput" />
</div>
<div class="keuze">
<input type="checkbox" id="actief@(s.Id)" @bind="Bewerken.Actief" />
<label for="actief@(s.Id)">Actief: nieuwe gebruikers kunnen deze soort kiezen</label>
</div>
@if (!Bewerken.Actief)
{
<p class="zacht klein" style="margin:.2rem 0 .6rem 1.7rem">
Wie al een item van deze soort heeft, blijft hem zien en kan zijn items gewoon
bekijken en wijzigen. Voor iedereen die hem nog nooit gebruikte verdwijnt hij uit de lijst.
</p>
}
<div class="keuze">
<input type="checkbox" id="bespreken@(s.Id)" @bind="Bewerken.BesprekenMet" />
<label for="bespreken@(s.Id)">Bespreken met: de eigenaar kan iemand kiezen om dit vooraf mee te bespreken</label>
</div>
<div class="rij" style="margin-top:.8rem">
<button class="knop" disabled="@Bezig" @onclick="() => Opslaan(s.Id)">Wijzigingen opslaan</button>
@if (s.KanVerwijderen)
{
<button class="knop rustig" @onclick="() => WeghalenSoort = s.Id">Soort verwijderen</button>
}
</div>
@if (!s.KanVerwijderen)
{
<p class="zacht klein" style="margin-bottom:0">
@if (s.Ingebouwd) { <text>Een ingebouwde soort kun je niet verwijderen, alleen niet-actief maken.</text> }
else { <text>Deze soort is in gebruik en kan dus niet worden verwijderd. Zet hem op niet-actief als je hem niet meer wilt aanbieden.</text> }
</p>
}
@if (WeghalenSoort == s.Id)
{
<div class="melding let-op" style="margin-top:.9rem">
<strong>@s.Naam</strong> verwijderen? Dit kan, omdat nog niemand er een item mee heeft gemaakt.
<div class="rij" style="margin-top:.7rem">
<button class="knop gevaar" @onclick="() => Verwijderen(s.Id)">Ja, verwijderen</button>
<button class="knop rustig" @onclick="() => WeghalenSoort = null">Nee, bewaren</button>
</div>
</div>
}
<hr />
<h3>Velden van het formulier</h3>
@if (s.Ingebouwd)
{
<p class="zacht">
Het invulscherm van een ingebouwde soort zit in de app zelf en is hier niet te
wijzigen. Wil je voor zoiets als "Polis" andere velden, maak dan een eigen soort.
</p>
}
else
{
@if (s.Velden.Count == 0)
{
<p class="zacht">Nog geen velden. Zonder velden vult iemand alleen een titel en een toelichting in.</p>
}
<div class="stapel">
@foreach (var v in s.Velden)
{
<div class="kaart" style="background:var(--vlak-zacht)">
<div class="rij">
<div class="groei">
<div>@v.Label</div>
<div class="zacht klein">
@VeldSoort.Naam(v.Soort)
@if (v.Verplicht) { <text> &middot; verplicht</text> }
@if (!v.Actief) { <text> &middot; niet actief</text> }
</div>
</div>
<button class="knop rustig" @onclick="() => VeldOpenen(v)">
@(VeldBewerkId == v.Id ? "Sluiten" : "Wijzigen")
</button>
</div>
@if (VeldBewerkId == v.Id)
{
<div class="veld" style="margin-top:.8rem">
<label>Naam van het veld</label>
<input type="text" maxlength="100" @bind="VeldBewerken.Label" @bind:event="oninput" />
<div class="uitleg">
De naam hernoemen mag: wat mensen al hebben ingevuld blijft bij het veld horen.
Het soort veld ligt vast.
</div>
</div>
@if (v.Soort == VeldSoort.Keuze)
{
<div class="veld">
<label>Keuzes, één per regel</label>
<textarea rows="4" @bind="VeldBewerken.Opties" @bind:event="oninput"></textarea>
</div>
}
<div class="veld">
<label>@(v.Soort == VeldSoort.Uitleg ? "Tekst" : "Hulptekst onder het veld")</label>
<input type="text" maxlength="300" @bind="VeldBewerken.Hulptekst" @bind:event="oninput" />
</div>
<div class="raster twee">
<div class="veld">
<label>Plek in het formulier</label>
<input type="number" @bind="VeldBewerken.Volgorde" />
</div>
<div>
@if (v.Soort != VeldSoort.Uitleg)
{
<div class="keuze">
<input type="checkbox" id="vv@(v.Id)" @bind="VeldBewerken.Verplicht" />
<label for="vv@(v.Id)">Verplicht</label>
</div>
}
<div class="keuze">
<input type="checkbox" id="va@(v.Id)" @bind="VeldBewerken.Actief" />
<label for="va@(v.Id)">Actief</label>
</div>
</div>
</div>
<div class="rij">
<button class="knop" disabled="@Bezig" @onclick="() => VeldOpslaan(v.Id)">Veld opslaan</button>
<button class="knop rustig" @onclick="() => WeghalenVeld = v.Id">Veld verwijderen</button>
</div>
<p class="zacht klein" style="margin-bottom:0">
@if (s.Items > 0)
{
<text>Er zijn al items van deze soort: een veld verwijderen kan dan niet. Zet het op niet-actief; wat er is ingevuld blijft bewaard.</text>
}
else
{
<text>Er zijn nog geen items van deze soort, dus je kunt het veld nog verwijderen.</text>
}
</p>
@if (WeghalenVeld == v.Id)
{
<div class="melding let-op" style="margin-top:.7rem">
<strong>@v.Label</strong> verwijderen?
<div class="rij" style="margin-top:.6rem">
<button class="knop gevaar" @onclick="() => VeldVerwijderen(v.Id)">Ja, verwijderen</button>
<button class="knop rustig" @onclick="() => WeghalenVeld = null">Nee, bewaren</button>
</div>
</div>
}
}
</div>
}
</div>
@* ---------------------------------------------------- veld toevoegen *@
<h3 style="margin-top:1.2rem">Veld toevoegen</h3>
<div class="raster twee">
<div class="veld">
<label>Naam van het veld</label>
<input type="text" maxlength="100" @bind="NieuwVeld.Label" @bind:event="oninput"
placeholder="Bijvoorbeeld: Wil je gereanimeerd worden?" />
</div>
<div class="veld">
<label>Soort veld</label>
<select @bind="NieuwVeld.Soort">
@foreach (var soort in VeldSoort.Alle)
{
<option value="@soort">@VeldSoort.Naam(soort)</option>
}
</select>
</div>
</div>
@if (NieuwVeld.Soort == VeldSoort.Keuze)
{
<div class="veld">
<label>Keuzes, één per regel</label>
<textarea rows="4" @bind="NieuwVeld.Opties" @bind:event="oninput"
placeholder="Ja&#10;Nee&#10;Alleen na overleg"></textarea>
</div>
}
<div class="veld">
<label>@(NieuwVeld.Soort == VeldSoort.Uitleg ? "Tekst" : "Hulptekst onder het veld (mag leeg)")</label>
<input type="text" maxlength="300" @bind="NieuwVeld.Hulptekst" @bind:event="oninput" />
</div>
@if (NieuwVeld.Soort != VeldSoort.Uitleg)
{
<div class="keuze">
<input type="checkbox" id="nieuwverplicht@(s.Id)" @bind="NieuwVeld.Verplicht" />
<label for="nieuwverplicht@(s.Id)">Verplicht</label>
</div>
}
<div class="rij" style="margin-top:.8rem">
<button class="knop" disabled="@Bezig" @onclick="() => VeldToevoegen(s.Id)">Veld toevoegen</button>
</div>
}
}
</div>
}
</div>
}
}
@code {
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
private long _gebruikerId;
private bool Magdit, Bezig;
private string? Melding, MeldingSoort;
private IReadOnlyList<SoortBeheerbeeld>? Lijst;
/// <summary>Welke soort openstaat voor beheer.</summary>
private int? Open;
private int? WeghalenSoort;
private long? VeldBewerkId, WeghalenVeld;
private SoortFormulier NieuweSoort = new();
private SoortFormulier Bewerken = new();
private VeldFormulier NieuwVeld = new();
private VeldFormulier VeldBewerken = new();
protected override async Task OnInitializedAsync()
{
if (AuthState is null) return;
_gebruikerId = (await AuthState).User.UserId() ?? 0;
Magdit = await Beheerkant.IsBeheerderAsync(_gebruikerId);
if (!Magdit) return;
await Laden();
}
private async Task Laden()
{
var resultaat = await SoortService.OverzichtAsync(_gebruikerId);
if (resultaat.Succeeded) Lijst = resultaat.Value;
else Meld(resultaat.Error, false);
}
private void Meld(string? tekst, bool goed)
{
Melding = tekst;
MeldingSoort = goed ? "goed" : "fout";
}
private static string Aantal(int n, string een, string meer) => $"{n} {(n == 1 ? een : meer)}";
private static string Gebruik(SoortBeheerbeeld s) => s.Items == 0
? "Nog nooit gebruikt"
: $"Gebruikt in {Aantal(s.Items, "item", "items")} door {Aantal(s.Eigenaren, "persoon", "mensen")}";
private void Openen(SoortBeheerbeeld s)
{
if (Open == s.Id) { Open = null; return; }
Open = s.Id;
WeghalenSoort = null;
VeldBewerkId = null;
WeghalenVeld = null;
NieuwVeld = new VeldFormulier();
Bewerken = new SoortFormulier
{
Naam = s.Naam, Uitleg = s.Uitleg ?? "", Actief = s.Actief,
Volgorde = s.Volgorde, BesprekenMet = s.BesprekenMet
};
}
private async Task Aanmaken()
{
Bezig = true;
try
{
var resultaat = await SoortService.MaakAsync(_gebruikerId, NieuweSoort.Naam, NieuweSoort.Uitleg,
NieuweSoort.BesprekenMet);
if (resultaat.Succeeded)
{
Meld($"{NieuweSoort.Naam.Trim()} is toegevoegd. Vul nu de velden in.", true);
NieuweSoort = new SoortFormulier();
await Laden();
var gemaakt = Lijst?.FirstOrDefault(x => x.Id == resultaat.Value);
if (gemaakt is not null) { Open = null; Openen(gemaakt); }
}
else Meld(resultaat.Error, false);
}
finally { Bezig = false; }
}
private async Task Opslaan(int id)
{
Bezig = true;
try
{
var resultaat = await SoortService.WijzigAsync(_gebruikerId, id, Bewerken.Naam, Bewerken.Uitleg,
Bewerken.Actief, Bewerken.Volgorde, Bewerken.BesprekenMet);
if (resultaat.Succeeded) { Meld("Opgeslagen.", true); await Laden(); }
else Meld(resultaat.Error, false);
}
finally { Bezig = false; }
}
private async Task Verwijderen(int id)
{
var resultaat = await SoortService.VerwijderAsync(_gebruikerId, id);
WeghalenSoort = null;
if (resultaat.Succeeded) { Open = null; Meld("De soort is verwijderd.", true); await Laden(); }
else Meld(resultaat.Error, false);
}
private void VeldOpenen(VeldBeheerbeeld v)
{
if (VeldBewerkId == v.Id) { VeldBewerkId = null; return; }
VeldBewerkId = v.Id;
WeghalenVeld = null;
VeldBewerken = new VeldFormulier
{
Label = v.Label, Soort = v.Soort, Opties = v.Opties ?? "", Hulptekst = v.Hulptekst ?? "",
Verplicht = v.Verplicht, Actief = v.Actief, Volgorde = v.Volgorde
};
}
private async Task VeldToevoegen(int soortId)
{
Bezig = true;
try
{
var resultaat = await SoortService.VeldToevoegenAsync(_gebruikerId, soortId, NieuwVeld.Label, NieuwVeld.Soort,
NieuwVeld.Opties, NieuwVeld.Hulptekst, NieuwVeld.Verplicht);
if (resultaat.Succeeded) { Meld($"{NieuwVeld.Label.Trim()} is toegevoegd.", true); NieuwVeld = new VeldFormulier(); await Laden(); }
else Meld(resultaat.Error, false);
}
finally { Bezig = false; }
}
private async Task VeldOpslaan(long veldId)
{
Bezig = true;
try
{
var resultaat = await SoortService.VeldWijzigenAsync(_gebruikerId, veldId, VeldBewerken.Label, VeldBewerken.Opties,
VeldBewerken.Hulptekst, VeldBewerken.Verplicht,
VeldBewerken.Actief, VeldBewerken.Volgorde);
if (resultaat.Succeeded) { Meld("Het veld is opgeslagen.", true); VeldBewerkId = null; await Laden(); }
else Meld(resultaat.Error, false);
}
finally { Bezig = false; }
}
private async Task VeldVerwijderen(long veldId)
{
var resultaat = await SoortService.VeldVerwijderenAsync(_gebruikerId, veldId);
WeghalenVeld = null;
if (resultaat.Succeeded) { VeldBewerkId = null; Meld("Het veld is verwijderd.", true); await Laden(); }
else Meld(resultaat.Error, false);
}
public class SoortFormulier
{
public string Naam { get; set; } = "";
public string Uitleg { get; set; } = "";
public bool Actief { get; set; } = true;
public int Volgorde { get; set; }
public bool BesprekenMet { get; set; }
}
public class VeldFormulier
{
public string Label { get; set; } = "";
public string Soort { get; set; } = VeldSoort.Tekst;
public string Opties { get; set; } = "";
public string Hulptekst { get; set; } = "";
public bool Verplicht { get; set; }
public bool Actief { get; set; } = true;
public int Volgorde { get; set; }
}
}
+130 -18
View File
@@ -2,6 +2,7 @@
@attribute [Authorize]
@inject VaultService KluisService
@inject LegacyService BeheerService
@inject VaultCategoryService SoortService
@inject IJSRuntime JS
@rendermode InteractiveServer
@@ -21,9 +22,9 @@
<label for="cat">Soort</label>
<select id="cat" @bind="Filter" @bind:after="Verversen">
<option value="">Alles</option>
@foreach (var c in Categorieen)
@foreach (var s in Soorten)
{
<option value="@((int)c)">@Naam(c)</option>
<option value="@s.Id">@s.Naam</option>
}
</select>
</div>
@@ -43,9 +44,13 @@
OpBevestigen="Opslaan" OpSluiten="Annuleren">
<div class="veld">
<label for="soort">Wat wil je vastleggen?</label>
<InputSelect id="soort" @bind-Value="Nieuw.Categorie">
@foreach (var c in Categorieen) { <option value="@c">@Naam(c)</option> }
<InputSelect id="soort" @bind-Value="Nieuw.CategorieNummer">
@foreach (var s in Soorten) { <option value="@s.Id">@s.Naam</option> }
</InputSelect>
@if (!string.IsNullOrWhiteSpace(HuidigeSoort?.Uitleg))
{
<div class="uitleg">@HuidigeSoort!.Uitleg</div>
}
</div>
<div class="veld">
<label for="titel">Titel</label>
@@ -129,9 +134,20 @@
case VaultCategory.Donorcodicil:
break;
default:
@* Een soort die de beheerder zelf heeft gemaakt: het formulier komt uit zijn velden. *@
@if (HuidigeSoort is { Ingebouwd: false } eigen)
{
@foreach (var veld in eigen.Velden.Where(v => v.Actief || HeeftWaarde(v)))
{
@VeldInvoer(veld)
}
}
break;
}
@if (Nieuw.Categorie is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil)
@if (BesprekenMetAan)
{
<div class="veld">
<label>Bespreken met</label>
@@ -256,7 +272,7 @@ else
{
<hr />
<dl style="margin:0">
@foreach (var (label, waarde) in Velden(Inhoud))
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud, GeopendSoort))
{
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:150px">@label</span>
@@ -358,13 +374,85 @@ else
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
private const int MaxTegelijk = 10;
private static readonly VaultCategory[] Categorieen =
/// <summary>
/// De soorten die deze persoon te zien krijgt: de actieve, plus de niet-actieve waar hij al een
/// item van heeft. De lijst komt van de server en niet meer uit de code, zodat de beheerder er
/// een soort bij kan zetten zonder dat er iets uitgerold hoeft te worden.
/// </summary>
private IReadOnlyList<KluisSoort> Soorten = Array.Empty<KluisSoort>();
/// <summary>Alle namen op nummer, ook van soorten die niet meer actief zijn.</summary>
private IReadOnlyDictionary<int, string> Namen = new Dictionary<int, string>();
/// <summary>De soort van het item dat openstaat, voor de namen van zijn eigen velden.</summary>
private KluisSoort? GeopendSoort;
private KluisSoort? HuidigeSoort => Soorten.FirstOrDefault(s => s.Id == (int)Nieuw.Categorie);
/// <summary>
/// "Bespreken met": bij een eigen soort bepaalt de beheerder dat per soort. Zonder soortenlijst
/// (die is er altijd, maar voor de zekerheid) geldt de oude regel.
/// </summary>
private bool BesprekenMetAan =>
HuidigeSoort?.BesprekenMet ?? Nieuw.Categorie is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil;
private async Task SoortenLaden()
{
VaultCategory.Uitvaartwens, VaultCategory.Donorcodicil, VaultCategory.Polis, VaultCategory.Wachtwoord,
VaultCategory.SocialMedia, VaultCategory.Apparaat, VaultCategory.Pincode,
VaultCategory.Rijbewijs, VaultCategory.Paspoort,
VaultCategory.Document, VaultCategory.Notitie, VaultCategory.Overig
};
Soorten = await SoortService.ZichtbaarVoorAsync(_persoonId);
Namen = await SoortService.NamenAsync();
}
/// <summary>Een leeg nieuw item, in de eerste soort die bruikbaar is (liefst uitvaartwens).</summary>
private NieuwItem LeegItem()
{
var soort = Soorten.FirstOrDefault(s => s.Id == (int)VaultCategory.Uitvaartwens) ?? Soorten.FirstOrDefault();
return soort is null ? new NieuwItem() : new NieuwItem { CategorieNummer = soort.Id };
}
private string Waarde(string sleutel) => Nieuw.Velden.TryGetValue(sleutel, out var w) ? w : "";
private void Zet(string sleutel, string? waarde) => Nieuw.Velden[sleutel] = waarde ?? "";
private bool HeeftWaarde(KluisVeld veld) => !string.IsNullOrWhiteSpace(Waarde(veld.Sleutel));
/// <summary>Eén invoerveld van een eigen soort, getekend naar de beschrijving van de beheerder.</summary>
private RenderFragment VeldInvoer(KluisVeld veld) => @<div class="veld">
@if (veld.Soort == VeldSoort.Uitleg)
{
<div class="uitleg">@veld.Hulptekst</div>
}
else
{
<label>@veld.Label@(veld.Verplicht ? " *" : "")</label>
@if (veld.Soort == VeldSoort.Lang)
{
<textarea rows="4" value="@Waarde(veld.Sleutel)"
@oninput="e => Zet(veld.Sleutel, e.Value?.ToString())"></textarea>
}
else if (veld.Soort == VeldSoort.Datum)
{
<input type="date" value="@Waarde(veld.Sleutel)"
@onchange="e => Zet(veld.Sleutel, e.Value?.ToString())" />
}
else if (veld.Soort == VeldSoort.Keuze)
{
<select @onchange="e => Zet(veld.Sleutel, e.Value?.ToString())">
<option value="" selected="@(Waarde(veld.Sleutel) == "")">Nog niet gekozen</option>
@foreach (var optie in veld.Opties)
{
<option value="@optie" selected="@(Waarde(veld.Sleutel) == optie)">@optie</option>
}
</select>
}
else
{
<input type="text" value="@Waarde(veld.Sleutel)"
@oninput="e => Zet(veld.Sleutel, e.Value?.ToString())" />
}
@if (!string.IsNullOrWhiteSpace(veld.Hulptekst))
{
<div class="uitleg">@veld.Hulptekst</div>
}
}
</div>;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -382,6 +470,8 @@ else
_gebruikerId = user.UserId() ?? 0;
_persoonId = user.PersonId() ?? 0;
Beheerders = await BeheerService.ListManagersAsync(_persoonId);
await SoortenLaden();
Nieuw = LeegItem();
await Verversen();
}
@@ -402,6 +492,8 @@ else
{
Geopend = id;
Inhoud = resultaat.Value;
var gekozen = Items?.FirstOrDefault(i => i.Id == id);
GeopendSoort = gekozen is null ? null : await SoortService.SoortAsync((int)gekozen.Category);
Melding = null;
await BijlagenLaden();
}
@@ -514,6 +606,7 @@ else
Muziek = p.Music ?? "",
Sprekers = p.Speakers ?? "",
Wensen = p.Wishes ?? "",
Velden = p.Velden is null ? new Dictionary<string, string>() : new Dictionary<string, string>(p.Velden),
BesprekenMetPersonId = item.SharedManagerPersonId,
Vrijgave = item.ReleasePolicy,
VrijgaveDatum = item.ReleaseAt is { } r ? DateOnly.FromDateTime(r) : null
@@ -533,7 +626,7 @@ else
private void Beginnen()
{
BewerkId = 0;
Nieuw = new NieuwItem();
Nieuw = LeegItem();
Bijlagen = null;
ToonFormulier = true;
}
@@ -541,7 +634,7 @@ else
private void Annuleren()
{
BewerkId = 0;
Nieuw = new NieuwItem();
Nieuw = LeegItem();
Bijlagen = null;
ToonFormulier = false;
}
@@ -575,7 +668,13 @@ else
Location = Leeg(Nieuw.Locatie),
Music = Leeg(Nieuw.Muziek),
Speakers = Leeg(Nieuw.Sprekers),
Wishes = Leeg(Nieuw.Wensen)
Wishes = Leeg(Nieuw.Wensen),
// Alleen voor een soort die de beheerder zelf maakte. Wat hier ook staat, de server
// houdt alleen de velden over die bij de soort horen en controleert ze nog eens.
Velden = HuidigeSoort is { Ingebouwd: false }
? Nieuw.Velden.Where(v => !string.IsNullOrWhiteSpace(v.Value))
.ToDictionary(v => v.Key, v => v.Value)
: null
};
DateTime? datum = Nieuw.Vrijgave == ReleasePolicy.OpDatum && Nieuw.VrijgaveDatum is { } d
@@ -600,9 +699,10 @@ else
Melding = BewerkId == 0 ? "Opgeslagen in je kluis." : "De wijziging is opgeslagen.";
MeldingSoort = "goed";
BewerkId = 0;
Nieuw = new NieuwItem();
Nieuw = LeegItem();
Bijlagen = null;
ToonFormulier = false;
await SoortenLaden();
await Verversen();
}
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
@@ -615,8 +715,7 @@ else
// Velden() en Naam() staan in Onderdelen/Kluisweergave.cs: de nalatenschapspagina toont
// precies hetzelfde, en twee kopieën van die lijst lopen vroeg of laat uit elkaar.
private static IEnumerable<(string, string)> Velden(VaultPayload p) => Kluisweergave.Velden(p);
private static string Naam(VaultCategory c) => Kluisweergave.Naam(c);
private string Naam(VaultCategory c) => Kluisweergave.Naam(c, Namen);
private static string Voorbeeld(VaultCategory c) => c switch
{
@@ -640,6 +739,19 @@ else
public class NieuwItem
{
public VaultCategory Categorie { get; set; } = VaultCategory.Uitvaartwens;
/// <summary>
/// Dezelfde keuze als nummer. De keuzelijst bindt hieraan en niet aan de enum: een soort
/// die de beheerder maakte heeft geen naam in de enum, alleen een nummer.
/// </summary>
public int CategorieNummer
{
get => (int)Categorie;
set => Categorie = (VaultCategory)value;
}
/// <summary>De ingevulde velden van een soort die de beheerder zelf maakte, op sleutel.</summary>
public Dictionary<string, string> Velden { get; set; } = new();
public string Titel { get; set; } = "";
public string? Notitie { get; set; }
public string? Website { get; set; }
@@ -7,6 +7,7 @@
@inject ILegacyRepository LegacyOpslag
@inject IPersonRepository Personen
@inject VaultService Kluis
@inject VaultCategoryService SoortService
@rendermode InteractiveServer
<PageTitle>Nalatenschap - MijnBesluit</PageTitle>
@@ -126,7 +127,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
<div class="rij">
<div class="groei">
<span>@item.Title</span>
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
<div class="zacht">@Kluisweergave.Naam(item.Category, Namen)</div>
</div>
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
</div>
@@ -155,7 +156,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
<div class="rij">
<div class="groei">
<span>@item.Title</span>
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
<div class="zacht">@Kluisweergave.Naam(item.Category, Namen)</div>
</div>
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
</div>
@@ -173,7 +174,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
<div class="stapel">
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud))
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud, GeopendSoort))
{
<div class="veld">
<label>@label</label>
@@ -388,6 +389,12 @@ else if (Stand.State == DeathProcedureState.Lopend)
private IReadOnlyList<VaultItem>? Nagelaten;
private VaultPayload? Inhoud;
private string? GeopendTitel;
/// <summary>Namen van alle soorten op nummer, ook die de beheerder zelf maakte.</summary>
private IReadOnlyDictionary<int, string> Namen = new Dictionary<int, string>();
/// <summary>De soort van het geopende item, voor de namen van zijn eigen velden.</summary>
private KluisSoort? GeopendSoort;
private IReadOnlyList<VaultFile>? GeopendBijlagen;
protected override async Task OnInitializedAsync()
@@ -397,6 +404,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
_gebruikerId = user.UserId() ?? 0;
_persoonId = user.PersonId() ?? 0;
Namen = await SoortService.NamenAsync();
_familieId = (await FamilieOpslag.ListOwnedByPersonAsync(_persoonId)).FirstOrDefault()?.Id ?? 0;
if (_familieId != 0) Leden = await Families.ListMembersAsync(_familieId, _gebruikerId);
@@ -465,6 +473,9 @@ else if (Stand.State == DeathProcedureState.Lopend)
Inhoud = resultaat.Value;
GeopendTitel = item.Title;
// De velden van het item van een ander: de namen zijn geen geheim, de waarden blijven versleuteld.
GeopendSoort = await SoortService.SoortAsync((int)item.Category);
// Bijlagen zijn optioneel: als ophalen mislukt (bv. geen toegang) tonen we het
// item gewoon zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
var bijlagen = await Kluis.ListFilesAsync(item.Id, _gebruikerId);
@@ -476,6 +487,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
GeopendItem = null;
Inhoud = null;
GeopendTitel = null;
GeopendSoort = null;
GeopendBijlagen = null;
}