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
+80 -1
View File
@@ -246,7 +246,58 @@ public record KluisItem(
int ReleasePolicy,
DateTime? ReleaseAt,
DateTime UpdatedAt,
long? SharedManagerPersonId = null);
long? SharedManagerPersonId = null,
string? CategoryName = null)
{
/// <summary>
/// Hoe de soort heet. De server stuurt de naam mee, zodat een soort die de beheerder zelf
/// maakte ook in een oudere lijst een naam heeft; ontbreekt hij, dan geldt de ingebouwde naam
/// van het nummer.
/// </summary>
public string SoortNaam(Func<int, string> ingebouwd) =>
string.IsNullOrWhiteSpace(CategoryName) ? ingebouwd(Category) : CategoryName;
}
/// <summary>
/// Een soort kluisitem zoals de server hem aanbiedt. Een ingebouwde soort (nummer 1 tot en met 11
/// en 99) heeft zijn invulscherm in de app en geen <see cref="Velden"/>; een soort die de
/// beheerder maakte heeft een nummer vanaf 100 en beschrijft zijn formulier hier.
/// </summary>
public record KluisSoort(int Id, string Naam, string? Uitleg, bool BesprekenMet, bool Ingebouwd,
IReadOnlyList<KluisVeld> Velden);
/// <summary>Eén veld van het formulier van een soort die de beheerder maakte.</summary>
public record KluisVeld(string Sleutel, string Label, string Soort, IReadOnlyList<string> Opties,
string? Hulptekst, bool Verplicht, bool Actief)
{
public const string Tekst = "tekst";
public const string Lang = "lang";
public const string Datum = "datum";
public const string Keuze = "keuze";
public const string Uitleg = "uitleg";
}
/// <summary>
/// De ingebouwde soorten kluisitems, voor als de server er geen lijst van geeft: een oudere
/// server, of geen verbinding op het moment dat het scherm opent. Dezelfde nummers, namen en
/// volgorde als de startrijen in db/21_kluissoorten.sql; een controle in de zelftest bewaakt dat
/// ze gelijk blijven.
/// </summary>
public static class IngebouwdeSoorten
{
private static KluisSoort S(int id, string naam, bool bespreken = false) =>
new(id, naam, null, bespreken, true, Array.Empty<KluisVeld>());
public static readonly IReadOnlyList<KluisSoort> Lijst = new[]
{
S(3, "Wachtwoord"), S(8, "Sociale media"), S(4, "Apparaat"), S(5, "Pincode"), S(2, "Polis"),
S(1, "Uitvaartwens", true), S(9, "Donorcodicil", true), S(10, "Rijbewijs"), S(11, "Paspoort"),
S(6, "Document"), S(7, "Notitie"), S(99, "Overig")
};
/// <summary>De naam bij een nummer; "Overig" voor wat we niet kennen.</summary>
public static string Naam(int id) => Lijst.FirstOrDefault(s => s.Id == id)?.Naam ?? "Overig";
}
/// <summary>De ontsleutelde inhoud van een kluisitem. Per soort is maar een deel gevuld.</summary>
public class KluisInhoud
@@ -273,6 +324,34 @@ public class KluisInhoud
public string? Music { get; set; }
public string? Speakers { get; set; }
public string? Wishes { get; set; }
/// <summary>
/// De velden van een soort die de beheerder zelf maakte, op de vaste sleutel van het veld.
/// Bij de ingebouwde soorten blijft dit leeg.
/// </summary>
public Dictionary<string, string> Velden { get; set; } = new();
/// <summary>
/// De ingevulde velden van een soort die de beheerder maakte, met hun label, in de volgorde van
/// het formulier. Alleen wat is ingevuld; datums staan er leesbaar, zoals "12 oktober 2026".
/// Een veld dat niet meer in de soort staat, wordt niet getoond.
/// </summary>
public IEnumerable<(string Naam, string Waarde)> EigenRegels(KluisSoort? soort)
{
if (soort is null) yield break;
foreach (var veld in soort.Velden.Where(v => v.Soort != KluisVeld.Uitleg))
{
if (!Velden.TryGetValue(veld.Sleutel, out var waarde) || string.IsNullOrWhiteSpace(waarde)) continue;
if (veld.Soort == KluisVeld.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>Een bijlage bij een kluisitem, zonder de inhoud zelf.</summary>