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
@@ -31,6 +31,7 @@ public static class ClientChecks
Accounts = Wereld.Accounts,
Families = Wereld.Families,
Vault = Wereld.Vault,
Soorten = Wereld.Soorten,
Messages = Wereld.Messages,
Legacy = Wereld.Legacy,
Overzicht = Wereld.Overzicht,
@@ -64,6 +65,67 @@ public static class ClientChecks
public static void Register(Runner runner)
{
runner.TestAsync("App: de soortenlijst komt over de lijn en de ingebouwde lijst in de app klopt ermee", async () =>
{
await using var proef = new Proef();
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
var lijst = await app.KluisSoortenAsync();
Assert.True(lijst.Gelukt, $"de lijst komt binnen ({lijst.Melding})");
var server = lijst.Waarde!;
var ingebouwd = IngebouwdeSoorten.Lijst;
Assert.True(server.Select(s => (s.Id, s.Naam, s.BesprekenMet)).SequenceEqual(
ingebouwd.Select(s => (s.Id, s.Naam, s.BesprekenMet))),
"de ingebouwde lijst in de app is gelijk aan wat de server als startlijst heeft");
Assert.True(IngebouwdeSoorten.Naam(11) == "Paspoort" && IngebouwdeSoorten.Naam(4242) == "Overig",
"een onbekend nummer heet Overig");
});
runner.TestAsync("App: een eigen soort met velden, en de naam reist mee met het item", async () =>
{
await using var proef = new Proef();
var w = proef.Wereld;
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Beheer");
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
var soortId = (await w.Soorten.MaakAsync(beheerder.Id, "Niet reanimeren", "Alleen een papieren verklaring is leidend.", false)).Value;
await w.Soorten.VeldToevoegenAsync(beheerder.Id, soortId, "Wil je gereanimeerd worden?", "keuze",
"Ja\nNee\nAlleen na overleg", "Kies wat het beste past.", true);
await w.Soorten.VeldToevoegenAsync(beheerder.Id, soortId, "Datum verklaring", "datum", null, null, false);
var lijst = (await app.KluisSoortenAsync()).Waarde!;
var eigen = lijst.Single(s => s.Id == soortId);
Assert.True(!eigen.Ingebouwd && eigen.Uitleg!.Contains("papieren"), "de soort komt met zijn uitleg binnen");
Assert.True(eigen.Velden.Count == 2, "met twee velden");
Assert.True(eigen.Velden[0].Soort == KluisVeld.Keuze && eigen.Velden[0].Verplicht
&& eigen.Velden[0].Opties.SequenceEqual(new[] { "Ja", "Nee", "Alleen na overleg" }),
"het keuzeveld heeft zijn opties en is verplicht");
Assert.True(eigen.Velden[1].Soort == KluisVeld.Datum && !eigen.Velden[1].Verplicht, "het datumveld is niet verplicht");
var inhoud = new KluisInhoud
{
Velden = new() { ["wil_je_gereanimeerd_worden"] = "Nee", ["datum_verklaring"] = "2026-10-12" }
};
var opgeslagen = await app.KluisOpslaanAsync(soortId, "Mijn verklaring", inhoud, 0);
Assert.True(opgeslagen.Gelukt, $"het item wordt bewaard ({opgeslagen.Melding})");
var items = (await app.KluisAsync()).Waarde!;
var item = items.Single(i => i.Title == "Mijn verklaring");
Assert.True(item.Category == soortId, "het item staat in de nieuwe soort");
Assert.True(item.CategoryName == "Niet reanimeren", "en de naam reist met het item mee");
Assert.True(item.SoortNaam(IngebouwdeSoorten.Naam) == "Niet reanimeren", "ook zonder de lijst van de app");
var open = (await app.KluisOpenenAsync(item.Id)).Waarde!;
Assert.True(open.Velden["wil_je_gereanimeerd_worden"] == "Nee" && open.Velden["datum_verklaring"] == "2026-10-12",
"de velden komen er precies zo uit als ze erin gingen");
// Een ingebouwd item krijgt zijn ingebouwde naam, ook zonder CategoryName van de server.
var polis = new KluisItem(1, "Polis", 2, 0, null, DateTime.UtcNow);
Assert.True(polis.SoortNaam(IngebouwdeSoorten.Naam) == "Polis", "terugval op de ingebouwde naam");
});
runner.TestAsync("App: registreren met een code en de sessie bewaren", async () =>
{
await using var proef = new Proef();