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:
@@ -28,8 +28,8 @@
|
||||
|
||||
<ApplicationTitle>MijnBesluit</ApplicationTitle>
|
||||
<ApplicationId>nl.mijnbesluit.app</ApplicationId>
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>2</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>1.1</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>3</ApplicationVersion>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
||||
|
||||
|
||||
@@ -140,22 +140,11 @@ public static class Tonen
|
||||
public static string Kort(DateOnly? dag) =>
|
||||
dag is null ? "" : $"{dag.Value.Day} {KorteMaanden[dag.Value.Month - 1]} {dag.Value.Year}";
|
||||
|
||||
/// <summary>De nummers komen overeen met VaultCategory in het domein.</summary>
|
||||
public static string Categorie(int categorie) => categorie switch
|
||||
{
|
||||
1 => "Uitvaartwens",
|
||||
2 => "Polis",
|
||||
3 => "Wachtwoord",
|
||||
4 => "Apparaat",
|
||||
5 => "Pincode",
|
||||
6 => "Document",
|
||||
7 => "Notitie",
|
||||
8 => "Sociale media",
|
||||
9 => "Donorcodicil",
|
||||
10 => "Rijbewijs",
|
||||
11 => "Paspoort",
|
||||
_ => "Overig"
|
||||
};
|
||||
/// <summary>
|
||||
/// De ingebouwde naam bij een soortnummer. Een soort die de beheerder zelf maakte staat hier niet
|
||||
/// in; die naam reist met het item mee (<see cref="MijnBesluit.Client.KluisItem.SoortNaam"/>).
|
||||
/// </summary>
|
||||
public static string Categorie(int categorie) => MijnBesluit.Client.IngebouwdeSoorten.Naam(categorie);
|
||||
|
||||
/// <summary>
|
||||
/// Hoe een relatie op het scherm heet. De lijst staat in MijnBesluit.Client, zodat de app en
|
||||
|
||||
@@ -22,18 +22,10 @@
|
||||
<div class="rij kluiskop">
|
||||
<select class="groei" aria-label="Filter op soort" disabled="@Bezig" @bind="Filter" @bind:after="LaadAsync">
|
||||
<option value="">Alles</option>
|
||||
<option value="3">Wachtwoord</option>
|
||||
<option value="8">Sociale media</option>
|
||||
<option value="4">Apparaat</option>
|
||||
<option value="5">Pincode</option>
|
||||
<option value="2">Polis</option>
|
||||
<option value="1">Uitvaartwens</option>
|
||||
<option value="9">Donorcodicil</option>
|
||||
<option value="10">Rijbewijs</option>
|
||||
<option value="11">Paspoort</option>
|
||||
<option value="6">Document</option>
|
||||
<option value="7">Notitie</option>
|
||||
<option value="99">Overig</option>
|
||||
@foreach (var soort in Soorten)
|
||||
{
|
||||
<option value="@soort.Id">@soort.Naam</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => Nieuw()">Toevoegen</button>
|
||||
@@ -52,22 +44,19 @@
|
||||
|
||||
<div class="veld">
|
||||
<label for="soort">Wat leg je vast?</label>
|
||||
@* De nummers komen overeen met VaultCategory in het domein. Overig is 99, niet 9:
|
||||
dat stond hier eerder verkeerd en leverde een categorie op die nergens bij hoort. *@
|
||||
@* De lijst komt van de server: de soorten die de beheerder aanbiedt, plus de soorten
|
||||
waar je al een item van hebt. Kan de server er geen geven, dan staat hier de
|
||||
ingebouwde lijst (IngebouwdeSoorten), met dezelfde nummers. *@
|
||||
<select id="soort" @bind="Categorie">
|
||||
<option value="3">Wachtwoord</option>
|
||||
<option value="8">Sociale media</option>
|
||||
<option value="4">Apparaat</option>
|
||||
<option value="5">Pincode</option>
|
||||
<option value="2">Polis</option>
|
||||
<option value="1">Uitvaartwens</option>
|
||||
<option value="9">Donorcodicil</option>
|
||||
<option value="10">Rijbewijs</option>
|
||||
<option value="11">Paspoort</option>
|
||||
<option value="6">Document</option>
|
||||
<option value="7">Notitie</option>
|
||||
<option value="99">Overig</option>
|
||||
@foreach (var soort in Soorten)
|
||||
{
|
||||
<option value="@soort.Id">@soort.Naam</option>
|
||||
}
|
||||
</select>
|
||||
@if (!string.IsNullOrWhiteSpace(HuidigeSoort?.Uitleg))
|
||||
{
|
||||
<div class="uitleg">@HuidigeSoort!.Uitleg</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
@@ -153,7 +142,17 @@
|
||||
break;
|
||||
}
|
||||
|
||||
@if (Categorie is 1 or 9)
|
||||
@* Een soort die de beheerder zelf maakte beschrijft zijn formulier op de server; hier wordt het
|
||||
getekend. Een ingebouwde soort heeft zijn velden hierboven in de switch. *@
|
||||
@if (HuidigeSoort is { Ingebouwd: false } eigen)
|
||||
{
|
||||
@foreach (var veld in eigen.Velden.Where(v => v.Actief || HeeftWaarde(v)))
|
||||
{
|
||||
@VeldInvoer(veld)
|
||||
}
|
||||
}
|
||||
|
||||
@if (HuidigeSoort?.BesprekenMet == true)
|
||||
{
|
||||
<div class="veld">
|
||||
<label for="bespreken">Bespreken met</label>
|
||||
@@ -278,7 +277,7 @@
|
||||
@onkeydown="e => ToetsAsync(e, () => TegelOpenenAsync(item))">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span class="groot">@Tonen.Categorie(item.Category)</span>
|
||||
<span class="groot">@item.SoortNaam(NaamVan)</span>
|
||||
<div class="zacht" title="@item.Title">@item.Title</div>
|
||||
|
||||
@* stopPropagation, want anders opent een druk op de prullenbak ook meteen
|
||||
@@ -342,7 +341,16 @@
|
||||
/// <summary>Op welk soort de lijst gefilterd staat; leeg is alles. Zelfde opzet als op de website.</summary>
|
||||
private string Filter = "";
|
||||
|
||||
/// <summary>De soorten in de keuzelijst; zie <see cref="LaadAsync"/>.</summary>
|
||||
private IReadOnlyList<KluisSoort> Soorten = IngebouwdeSoorten.Lijst;
|
||||
|
||||
private int Categorie = 3;
|
||||
|
||||
/// <summary>De soort die in het formulier gekozen is. Null als de lijst hem niet kent.</summary>
|
||||
private KluisSoort? HuidigeSoort => Soorten.FirstOrDefault(s => s.Id == Categorie);
|
||||
|
||||
/// <summary>De naam van een soort uit de lijst, of de ingebouwde naam als de lijst hem niet kent.</summary>
|
||||
private string NaamVan(int soort) => Soorten.FirstOrDefault(s => s.Id == soort)?.Naam ?? IngebouwdeSoorten.Naam(soort);
|
||||
private IReadOnlyList<Beheerder> Beheerders = Array.Empty<Beheerder>();
|
||||
private string BesprekenMetTekst = "";
|
||||
private long? BesprekenMetPersonId => long.TryParse(BesprekenMetTekst, out var p) ? p : null;
|
||||
@@ -383,6 +391,11 @@
|
||||
if (lijst.Gelukt) Items = lijst.Waarde!;
|
||||
else Fout = lijst.Melding;
|
||||
|
||||
// De soorten van de server. Geen lijst terug (een oudere server, of even geen verbinding):
|
||||
// dan blijft het de ingebouwde lijst, en werkt de kluis precies zoals voorheen.
|
||||
var soorten = await Toestand.Client.KluisSoortenAsync();
|
||||
Soorten = soorten.Gelukt && soorten.Waarde is { Count: > 0 } ? soorten.Waarde : IngebouwdeSoorten.Lijst;
|
||||
|
||||
var beheerders = await Toestand.Client.BeheerdersAsync();
|
||||
if (beheerders.Gelukt) Beheerders = beheerders.Waarde!;
|
||||
|
||||
@@ -405,7 +418,7 @@
|
||||
Bewerken = new KluisInhoud();
|
||||
ItemId = 0;
|
||||
Titel = "";
|
||||
Categorie = 3;
|
||||
Categorie = Soorten.Any(s => s.Id == 3) ? 3 : Soorten[0].Id;
|
||||
Vrijgave = 1;
|
||||
VrijgaveDatum = DateTime.Today.AddYears(1);
|
||||
BesprekenMetTekst = "";
|
||||
@@ -456,6 +469,15 @@
|
||||
var datum = Vrijgave == 2 ? VrijgaveDatum : null;
|
||||
if (Vrijgave == 2 && datum is null) { Fout = "Kies een datum waarop dit vrijkomt."; return; }
|
||||
|
||||
// Verplichte velden van een soort die de beheerder maakte. De server controleert het ook;
|
||||
// dit is om het meteen bij het veld te kunnen zeggen.
|
||||
if (HuidigeSoort is { Ingebouwd: false } eigen)
|
||||
{
|
||||
var leeg = eigen.Velden.FirstOrDefault(v => v.Actief && v.Verplicht && v.Soort != KluisVeld.Uitleg
|
||||
&& string.IsNullOrWhiteSpace(Waarde(v)));
|
||||
if (leeg is not null) { Fout = $"Vul in: {leeg.Label}."; return; }
|
||||
}
|
||||
|
||||
long nieuwItemId = 0;
|
||||
|
||||
if (await ProbeerAsync(async () =>
|
||||
@@ -592,6 +614,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ eigen velden
|
||||
|
||||
private string Waarde(KluisVeld veld) =>
|
||||
Bewerken is not null && Bewerken.Velden.TryGetValue(veld.Sleutel, out var w) ? w : "";
|
||||
|
||||
private bool HeeftWaarde(KluisVeld veld) => !string.IsNullOrWhiteSpace(Waarde(veld));
|
||||
|
||||
private void Zet(KluisVeld veld, string? waarde)
|
||||
{
|
||||
if (Bewerken is null) return;
|
||||
if (string.IsNullOrWhiteSpace(waarde)) Bewerken.Velden.Remove(veld.Sleutel);
|
||||
else Bewerken.Velden[veld.Sleutel] = waarde.Trim();
|
||||
}
|
||||
|
||||
/// <summary>De opgeslagen datum (jjjj-mm-dd) als datum voor de kiezer; null als er niets staat.</summary>
|
||||
private DateTime? DatumWaarde(KluisVeld veld) =>
|
||||
DateOnly.TryParseExact(Waarde(veld), "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var dag)
|
||||
? dag.ToDateTime(TimeOnly.MinValue)
|
||||
: null;
|
||||
|
||||
private void ZetDatum(KluisVeld veld, DateTime? datum) =>
|
||||
Zet(veld, datum?.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
/// <summary>
|
||||
/// Eén veld van een door de beheerder gemaakte soort. De invoer gaat via Zet in plaats van @bind:
|
||||
/// de waarden staan in een woordenboek op de sleutel van het veld, en een veld dat leeg wordt
|
||||
/// gemaakt hoort er dan uit te verdwijnen in plaats van als lege tekst te blijven staan.
|
||||
/// </summary>
|
||||
private RenderFragment VeldInvoer(KluisVeld veld) => @<div class="veld">
|
||||
@if (veld.Soort == KluisVeld.Uitleg)
|
||||
{
|
||||
<div class="uitleg">@veld.Hulptekst</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label for="@VeldId(veld)">@veld.Label@(veld.Verplicht ? " *" : "")</label>
|
||||
|
||||
@switch (veld.Soort)
|
||||
{
|
||||
case KluisVeld.Lang:
|
||||
<textarea id="@VeldId(veld)" rows="3" value="@Waarde(veld)"
|
||||
@onchange="e => Zet(veld, e.Value?.ToString())"></textarea>
|
||||
break;
|
||||
|
||||
case KluisVeld.Datum:
|
||||
<Datumkiezer Waarde="@DatumWaarde(veld)" WaardeChanged="d => ZetDatum(veld, d)"
|
||||
Optioneel="true" VanJaar="@(DateTime.Today.Year - 80)" TotJaar="@(DateTime.Today.Year + 5)"
|
||||
Aflopend="true" />
|
||||
break;
|
||||
|
||||
case KluisVeld.Keuze:
|
||||
<select id="@VeldId(veld)" value="@Waarde(veld)" @onchange="e => Zet(veld, e.Value?.ToString())">
|
||||
<option value="">Kies…</option>
|
||||
@foreach (var optie in veld.Opties)
|
||||
{
|
||||
<option value="@optie" selected="@(optie == Waarde(veld))">@optie</option>
|
||||
}
|
||||
@* Stond er een waarde die niet meer in de lijst staat, dan blijft die zichtbaar. *@
|
||||
@if (HeeftWaarde(veld) && !veld.Opties.Contains(Waarde(veld)))
|
||||
{
|
||||
<option value="@Waarde(veld)" selected="selected">@Waarde(veld)</option>
|
||||
}
|
||||
</select>
|
||||
break;
|
||||
|
||||
default:
|
||||
<input id="@VeldId(veld)" value="@Waarde(veld)" @onchange="e => Zet(veld, e.Value?.ToString())" />
|
||||
break;
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(veld.Hulptekst))
|
||||
{
|
||||
<div class="uitleg">@veld.Hulptekst</div>
|
||||
}
|
||||
}
|
||||
</div>;
|
||||
|
||||
private static string VeldId(KluisVeld veld) => "veld-" + veld.Sleutel;
|
||||
|
||||
/// <summary>
|
||||
/// Muziek en Wensen mogen meerdere regels worden; dan is een vast aantal regels niet
|
||||
/// genoeg en een schuifbalk niet fijn op een telefoon. Deze twee velden groeien daarom
|
||||
|
||||
@@ -156,7 +156,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Tonen.Categorie(item.Category)</div>
|
||||
<div class="zacht">@item.SoortNaam(IngebouwdeSoorten.Naam)</div>
|
||||
</div>
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick:stopPropagation="true"
|
||||
@onclick="() => ItemOpenenAsync(item)">Openen</button>
|
||||
@@ -187,7 +187,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Tonen.Categorie(item.Category)</div>
|
||||
<div class="zacht">@item.SoortNaam(IngebouwdeSoorten.Naam)</div>
|
||||
</div>
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => ItemOpenenAsync(item)">Openen</button>
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
||||
<div class="stapel">
|
||||
@foreach (var regel in Regels(Inhoud))
|
||||
@foreach (var regel in Regels(Inhoud).Concat(Inhoud.EigenRegels(GeopendSoort)))
|
||||
{
|
||||
<div class="veld">
|
||||
<label>@regel.Naam</label>
|
||||
@@ -482,6 +482,9 @@ else if (Stand is { IsBevestigd: true })
|
||||
private long? Bekeken, GeopendItem;
|
||||
private IReadOnlyList<KluisItem> Nagelaten = Array.Empty<KluisItem>();
|
||||
private KluisInhoud? Inhoud;
|
||||
|
||||
/// <summary>De soort van het geopende item, voor de labels van de velden van een eigen soort.</summary>
|
||||
private KluisSoort? GeopendSoort;
|
||||
private string? GeopendTitel;
|
||||
private IReadOnlyList<Bijlage> GeopendBijlagen = Array.Empty<Bijlage>();
|
||||
|
||||
@@ -604,6 +607,16 @@ else if (Stand is { IsBevestigd: true })
|
||||
Inhoud = r.Waarde;
|
||||
GeopendTitel = item.Title;
|
||||
|
||||
// De labels van de velden van een soort die de beheerder zelf maakte staan niet in de
|
||||
// app. Ze worden apart opgehaald, ook als die soort niet meer wordt aangeboden: de
|
||||
// namen van de velden zijn geen geheim, de ingevulde waarden blijven versleuteld.
|
||||
GeopendSoort = null;
|
||||
if (item.Category >= 100)
|
||||
{
|
||||
var soort = await Toestand.Client.KluisSoortAsync(item.Category);
|
||||
if (soort.Gelukt) GeopendSoort = soort.Waarde;
|
||||
}
|
||||
|
||||
// Bijlagen zijn hier optioneel: als ophalen mislukt tonen we het item gewoon
|
||||
// zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
||||
var bijlagen = await Toestand.Client.BijlagenAsync(item.Id);
|
||||
@@ -616,6 +629,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
{
|
||||
GeopendItem = null;
|
||||
Inhoud = null;
|
||||
GeopendSoort = null;
|
||||
GeopendTitel = null;
|
||||
GeopendBijlagen = Array.Empty<Bijlage>();
|
||||
BijlageDichtdoen();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.Application.Abstractions;
|
||||
@@ -261,6 +262,31 @@ public interface IAuditLog
|
||||
string? details = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>De soorten kluisitems en hun velden, zoals de beheerder ze instelt.</summary>
|
||||
public interface IVaultCategoryRepository
|
||||
{
|
||||
/// <summary>Alle soorten, met hun velden, op volgorde.</summary>
|
||||
Task<IReadOnlyList<VaultCategoryDefinition>> ListAsync(CancellationToken ct = default);
|
||||
|
||||
Task<VaultCategoryDefinition?> FindAsync(int id, CancellationToken ct = default);
|
||||
|
||||
Task<VaultCategoryField?> FindFieldAsync(long fieldId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Per soort hoeveel kluisitems (niet verwijderd) en hoeveel eigenaren. Alleen aantallen.</summary>
|
||||
Task<IReadOnlyList<SoortGebruik>> GebruikAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>De soorten waar deze persoon minstens één kluisitem (niet verwijderd) van heeft.</summary>
|
||||
Task<IReadOnlySet<int>> InGebruikDoorAsync(long personId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Hoeveel kluisitems (niet verwijderd) er van deze soort zijn.</summary>
|
||||
Task<int> AantalItemsAsync(int categoryId, CancellationToken ct = default);
|
||||
|
||||
void Add(VaultCategoryDefinition soort);
|
||||
void Remove(VaultCategoryDefinition soort);
|
||||
void Add(VaultCategoryField veld);
|
||||
void Remove(VaultCategoryField veld);
|
||||
}
|
||||
|
||||
/// <summary>Versturen van uitnodigingen en meldingen.</summary>
|
||||
/// <summary>
|
||||
/// Adverteerders, hun campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace MijnBesluit.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Een soort kluisitem zoals een gebruiker die te zien krijgt: de naam, de uitleg en bij een
|
||||
/// nieuwe soort de velden van het formulier. Dit is ook wat er over de lijn naar de app gaat.
|
||||
/// </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 nieuwe soort.</summary>
|
||||
public record KluisVeld(string Sleutel, string Label, string Soort, IReadOnlyList<string> Opties,
|
||||
string? Hulptekst, bool Verplicht, bool Actief);
|
||||
|
||||
/// <summary>Hoe vaak een soort in gebruik is: aantallen, nooit inhoud of namen.</summary>
|
||||
public record SoortGebruik(int CategoryId, int Items, int Eigenaren);
|
||||
|
||||
/// <summary>Een soort in het beheerscherm, met hoe vaak hij gebruikt wordt.</summary>
|
||||
public record SoortBeheerbeeld(int Id, string Naam, string? Uitleg, bool Actief, int Volgorde,
|
||||
bool BesprekenMet, bool Ingebouwd, int Items, int Eigenaren,
|
||||
IReadOnlyList<VeldBeheerbeeld> Velden)
|
||||
{
|
||||
/// <summary>Verwijderen kan alleen als niemand er een item mee heeft gemaakt en hij niet ingebouwd is.</summary>
|
||||
public bool KanVerwijderen => !Ingebouwd && Items == 0;
|
||||
}
|
||||
|
||||
/// <summary>Een veld in het beheerscherm.</summary>
|
||||
public record VeldBeheerbeeld(long Id, string Sleutel, string Label, string Soort, string? Opties,
|
||||
string? Hulptekst, bool Verplicht, bool Actief, int Volgorde);
|
||||
@@ -35,4 +35,10 @@ public class VaultPayload
|
||||
public string? Music { get; set; }
|
||||
public string? Speakers { get; set; }
|
||||
public string? Wishes { get; set; }
|
||||
|
||||
// Soorten die de beheerder zelf heeft gemaakt (nummer 100 en hoger). Hun velden staan niet als
|
||||
// eigen eigenschap hierboven maar hier, op de vaste sleutel van het veld
|
||||
// (VaultCategoryField.Sleutel). Zo kan er een veld bij zonder dat de code verandert, en blijft
|
||||
// het net zo versleuteld als de rest van de inhoud. Bij de ingebouwde soorten is dit leeg.
|
||||
public Dictionary<string, string>? Velden { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MijnBesluit.Application.Abstractions;
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems. Wat je in de keuzelijst ziet, en bij een door de beheerder gemaakte soort
|
||||
/// ook welke velden het formulier heeft, komt hier vandaan en niet meer uit de code. Zo vraagt een
|
||||
/// nieuwe soort ("Dementie", "Niet reanimeren") geen nieuwe versie van de app.
|
||||
///
|
||||
/// Twee regels bepalen alles wat hier gebeurt:
|
||||
///
|
||||
/// 1. Wat in gebruik is, verdwijnt nooit. Een soort waar al een kluisitem mee is gemaakt kan niet
|
||||
/// worden verwijderd, alleen op niet-actief gezet. Wie er nog nooit een item mee maakte ziet hem
|
||||
/// dan niet meer in zijn keuzelijst; wie dat wel deed, blijft hem zien en kan zijn items
|
||||
/// gewoon bekijken en wijzigen.
|
||||
/// 2. De beheerder ziet aantallen, nooit inhoud. Hoe vaak een soort gebruikt wordt is een tel;
|
||||
/// wie het deed en wat er in staat, blijft versleuteld en onbereikbaar.
|
||||
/// </summary>
|
||||
public class VaultCategoryService
|
||||
{
|
||||
/// <summary>Meer velden dan dit maakt van een formulier een enquête.</summary>
|
||||
public const int MaxVelden = 40;
|
||||
|
||||
/// <summary>Hoe lang een ingevulde waarde in een eigen veld mag zijn.</summary>
|
||||
public const int MaxWaardelengte = 4000;
|
||||
|
||||
private readonly IVaultCategoryRepository _soorten;
|
||||
private readonly IUserRepository _users;
|
||||
private readonly IAuditLog _audit;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public VaultCategoryService(IVaultCategoryRepository soorten, IUserRepository users, IAuditLog audit,
|
||||
IUnitOfWork uow, IClock clock)
|
||||
{
|
||||
_soorten = soorten; _users = users; _audit = audit; _uow = uow; _clock = clock;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ voor gebruikers
|
||||
|
||||
/// <summary>
|
||||
/// De soorten die deze persoon te zien krijgt: alle actieve, plus de niet-actieve waar hij al
|
||||
/// een item van heeft. Op volgorde van de beheerder.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<KluisSoort>> ZichtbaarVoorAsync(long personId, CancellationToken ct = default)
|
||||
{
|
||||
var alle = await _soorten.ListAsync(ct);
|
||||
var gebruikt = await _soorten.InGebruikDoorAsync(personId, ct);
|
||||
|
||||
return alle.Where(s => s.Actief || gebruikt.Contains(s.Id))
|
||||
.Select(NaarSoort)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Alle namen op nummer, voor onder een kluisitem. Ook van soorten die niet meer actief zijn.</summary>
|
||||
public async Task<IReadOnlyDictionary<int, string>> NamenAsync(CancellationToken ct = default) =>
|
||||
(await _soorten.ListAsync(ct)).ToDictionary(s => s.Id, s => s.Naam);
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort op nummer, ook als hij niet actief is of de vrager hem nooit gebruikte. Nodig om
|
||||
/// het item van een ander te kunnen lezen (een nalatenschap, iets dat met je is gedeeld): de
|
||||
/// namen van de velden zijn geen geheim, de ingevulde waarden wel, en die blijven versleuteld.
|
||||
/// </summary>
|
||||
public async Task<KluisSoort?> SoortAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var soort = await _soorten.FindAsync(id, ct);
|
||||
return soort is null ? null : NaarSoort(soort);
|
||||
}
|
||||
|
||||
/// <summary>De soort met dit nummer, met velden. Null als hij niet bestaat.</summary>
|
||||
public Task<VaultCategoryDefinition?> VindAsync(int id, CancellationToken ct = default) =>
|
||||
_soorten.FindAsync(id, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Mag deze persoon een nieuw kluisitem in deze soort maken? Alleen als de soort bestaat en
|
||||
/// actief is, of als hij er al een item van heeft - dan is het voor hem nog gewoon een soort
|
||||
/// die hij kent.
|
||||
/// </summary>
|
||||
public async Task<Result<VaultCategoryDefinition>> MagGebruikenAsync(long personId, int categoryId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var soort = await _soorten.FindAsync(categoryId, ct);
|
||||
if (soort is null) return Result<VaultCategoryDefinition>.Fail("Deze soort bestaat niet.");
|
||||
|
||||
if (!soort.Actief && !(await _soorten.InGebruikDoorAsync(personId, ct)).Contains(soort.Id))
|
||||
return Result<VaultCategoryDefinition>.Fail($"{soort.Naam} wordt niet meer aangeboden.");
|
||||
|
||||
return Result<VaultCategoryDefinition>.Ok(soort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controleert wat er in de eigen velden van een door de beheerder gemaakte soort is ingevuld
|
||||
/// en geeft de schone versie terug: alleen bekende velden, bijgesneden, met de verplichte
|
||||
/// velden ingevuld en datums die kloppen. Op de server en niet alleen in het scherm, want
|
||||
/// een oudere of andere app stuurt wat hij wil.
|
||||
/// </summary>
|
||||
public static Result<Dictionary<string, string>> ControleerVelden(
|
||||
VaultCategoryDefinition soort, IReadOnlyDictionary<string, string?>? ingevuld)
|
||||
{
|
||||
var schoon = new Dictionary<string, string>();
|
||||
|
||||
foreach (var veld in soort.Velden.Where(v => v.Soort != VeldSoort.Uitleg))
|
||||
{
|
||||
ingevuld ??= new Dictionary<string, string?>();
|
||||
var waarde = ingevuld.TryGetValue(veld.Sleutel, out var w) ? w?.Trim() : null;
|
||||
|
||||
if (string.IsNullOrEmpty(waarde))
|
||||
{
|
||||
if (veld.Verplicht && veld.Actief)
|
||||
return Result<Dictionary<string, string>>.Fail($"Vul in: {veld.Label}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waarde.Length > MaxWaardelengte)
|
||||
return Result<Dictionary<string, string>>.Fail($"{veld.Label} is te lang.");
|
||||
|
||||
if (veld.Soort == VeldSoort.Datum &&
|
||||
!DateOnly.TryParseExact(waarde, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out _))
|
||||
return Result<Dictionary<string, string>>.Fail($"{veld.Label}: dat is geen geldige datum.");
|
||||
|
||||
schoon[veld.Sleutel] = waarde;
|
||||
}
|
||||
|
||||
return Result<Dictionary<string, string>>.Ok(schoon);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ voor de beheerder
|
||||
|
||||
/// <summary>Alle soorten met hoe vaak ze gebruikt worden. Alleen voor beheerders.</summary>
|
||||
public async Task<Result<IReadOnlyList<SoortBeheerbeeld>>> OverzichtAsync(long currentUserId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct))
|
||||
return Result<IReadOnlyList<SoortBeheerbeeld>>.Fail("Je bent geen beheerder.");
|
||||
|
||||
var alle = await _soorten.ListAsync(ct);
|
||||
var gebruik = (await _soorten.GebruikAsync(ct)).ToDictionary(g => g.CategoryId);
|
||||
|
||||
var lijst = alle.Select(s =>
|
||||
{
|
||||
gebruik.TryGetValue(s.Id, out var g);
|
||||
return new SoortBeheerbeeld(
|
||||
s.Id, s.Naam, s.Uitleg, s.Actief, s.Volgorde, s.BesprekenMet, s.Ingebouwd,
|
||||
g?.Items ?? 0, g?.Eigenaren ?? 0,
|
||||
s.Velden.OrderBy(v => v.Volgorde).ThenBy(v => v.Id)
|
||||
.Select(v => new VeldBeheerbeeld(v.Id, v.Sleutel, v.Label, v.Soort, v.Opties,
|
||||
v.Hulptekst, v.Verplicht, v.Actief, v.Volgorde))
|
||||
.ToList());
|
||||
}).ToList();
|
||||
|
||||
return Result<IReadOnlyList<SoortBeheerbeeld>>.Ok(lijst);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maakt een nieuwe soort. Hij begint actief, onderaan de lijst. De naam is leesbaar voor de
|
||||
/// server (het item zelf blijft versleuteld), dus een naam die iets over iemands gezondheid
|
||||
/// zegt, zegt dat ook tegen de server.
|
||||
/// </summary>
|
||||
public async Task<Result<int>> MaakAsync(long currentUserId, string naam, string? uitleg, bool besprekenMet,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result<int>.Fail("Je bent geen beheerder.");
|
||||
|
||||
var fout = await ControleerNaamAsync(null, naam, uitleg, ct);
|
||||
if (fout is not null) return Result<int>.Fail(fout);
|
||||
|
||||
var alle = await _soorten.ListAsync(ct);
|
||||
var nu = _clock.UtcNow;
|
||||
var soort = new VaultCategoryDefinition
|
||||
{
|
||||
// Nummers van eigen soorten beginnen bij 100; de database kiest het volgende vrije.
|
||||
Naam = naam.Trim(),
|
||||
Uitleg = Schoon(uitleg),
|
||||
Actief = true,
|
||||
Volgorde = (alle.Count == 0 ? 0 : alle.Max(s => s.Volgorde)) + 10,
|
||||
BesprekenMet = besprekenMet,
|
||||
Ingebouwd = false,
|
||||
AangemaaktOp = nu,
|
||||
GewijzigdOp = nu
|
||||
};
|
||||
|
||||
_soorten.Add(soort);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition),
|
||||
soort.Id, $"gemaakt: {soort.Naam}", ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result<int>.Ok(soort.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wijzigt een soort: de naam, de uitleg, of hij nog wordt aangeboden, waar hij in de lijst
|
||||
/// staat en of er "Bespreken met" bij zit. Dit kan ook bij de ingebouwde soorten.
|
||||
/// </summary>
|
||||
public async Task<Result> WijzigAsync(long currentUserId, int id, string naam, string? uitleg, bool actief,
|
||||
int volgorde, bool besprekenMet, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var soort = await _soorten.FindAsync(id, ct);
|
||||
if (soort is null) return Result.Fail("Deze soort bestaat niet.");
|
||||
|
||||
var fout = await ControleerNaamAsync(id, naam, uitleg, ct);
|
||||
if (fout is not null) return Result.Fail(fout);
|
||||
|
||||
var wijzigingen = new List<string>();
|
||||
if (soort.Naam != naam.Trim()) wijzigingen.Add($"naam {soort.Naam} -> {naam.Trim()}");
|
||||
if (soort.Actief != actief) wijzigingen.Add(actief ? "actief" : "niet-actief");
|
||||
if (soort.BesprekenMet != besprekenMet) wijzigingen.Add(besprekenMet ? "bespreken met aan" : "bespreken met uit");
|
||||
|
||||
soort.Naam = naam.Trim();
|
||||
soort.Uitleg = Schoon(uitleg);
|
||||
soort.Actief = actief;
|
||||
soort.Volgorde = volgorde;
|
||||
soort.BesprekenMet = besprekenMet;
|
||||
soort.GewijzigdOp = _clock.UtcNow;
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition), id,
|
||||
wijzigingen.Count == 0 ? "gewijzigd" : string.Join(", ", wijzigingen), ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verwijdert een soort - maar alleen een door de beheerder gemaakte waar nog nooit een
|
||||
/// kluisitem mee is gemaakt. Is hij in gebruik, dan blijft alleen "niet-actief" over, zodat
|
||||
/// wie hem gebruikt hem houdt.
|
||||
/// </summary>
|
||||
public async Task<Result> VerwijderAsync(long currentUserId, int id, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var soort = await _soorten.FindAsync(id, ct);
|
||||
if (soort is null) return Result.Fail("Deze soort bestaat niet.");
|
||||
if (soort.Ingebouwd) return Result.Fail("Een ingebouwde soort kun je niet verwijderen, alleen niet-actief maken.");
|
||||
|
||||
var aantal = await _soorten.AantalItemsAsync(id, ct);
|
||||
if (aantal > 0)
|
||||
return Result.Fail($"{soort.Naam} is in gebruik ({aantal} " +
|
||||
(aantal == 1 ? "item" : "items") + "). Zet hem op niet-actief; dan zien alleen de mensen die hem gebruiken hem nog.");
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition), id,
|
||||
$"verwijderd: {soort.Naam}", ct);
|
||||
_soorten.Remove(soort);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>Voegt een veld toe aan het formulier van een door de beheerder gemaakte soort.</summary>
|
||||
public async Task<Result> VeldToevoegenAsync(long currentUserId, int categoryId, string label, string soortVeld,
|
||||
string? opties, string? hulptekst, bool verplicht,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var soort = await _soorten.FindAsync(categoryId, ct);
|
||||
if (soort is null) return Result.Fail("Deze soort bestaat niet.");
|
||||
if (soort.Ingebouwd) return Result.Fail("De velden van een ingebouwde soort zitten in de app en zijn hier niet te wijzigen.");
|
||||
if (soort.Velden.Count >= MaxVelden) return Result.Fail($"Meer dan {MaxVelden} velden is te veel voor één formulier.");
|
||||
|
||||
var fout = ControleerVeld(label, soortVeld, opties, hulptekst, verplicht);
|
||||
if (fout is not null) return Result.Fail(fout);
|
||||
|
||||
var veld = new VaultCategoryField
|
||||
{
|
||||
CategoryId = categoryId,
|
||||
Sleutel = NieuweSleutel(label, soort.Velden.Select(v => v.Sleutel)),
|
||||
Label = label.Trim(),
|
||||
Soort = soortVeld,
|
||||
Opties = soortVeld == VeldSoort.Keuze ? SchoneOpties(opties) : null,
|
||||
Hulptekst = Schoon(hulptekst),
|
||||
Verplicht = verplicht && soortVeld != VeldSoort.Uitleg,
|
||||
Actief = true,
|
||||
Volgorde = (soort.Velden.Count == 0 ? 0 : soort.Velden.Max(v => v.Volgorde)) + 10
|
||||
};
|
||||
|
||||
_soorten.Add(veld);
|
||||
soort.GewijzigdOp = _clock.UtcNow;
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition), categoryId,
|
||||
$"veld toegevoegd: {veld.Label}", ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wijzigt een veld. De sleutel en de soort veld staan vast, want daaraan hangt wat mensen al
|
||||
/// hebben ingevuld; het label, de opties, de hulptekst, verplicht, actief en de plek mogen wel.
|
||||
/// </summary>
|
||||
public async Task<Result> VeldWijzigenAsync(long currentUserId, long veldId, string label, string? opties,
|
||||
string? hulptekst, bool verplicht, bool actief, int volgorde,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var veld = await _soorten.FindFieldAsync(veldId, ct);
|
||||
if (veld is null) return Result.Fail("Dit veld bestaat niet.");
|
||||
|
||||
var fout = ControleerVeld(label, veld.Soort, opties, hulptekst, verplicht);
|
||||
if (fout is not null) return Result.Fail(fout);
|
||||
|
||||
veld.Label = label.Trim();
|
||||
veld.Opties = veld.Soort == VeldSoort.Keuze ? SchoneOpties(opties) : null;
|
||||
veld.Hulptekst = Schoon(hulptekst);
|
||||
veld.Verplicht = verplicht && veld.Soort != VeldSoort.Uitleg;
|
||||
veld.Actief = actief;
|
||||
veld.Volgorde = volgorde;
|
||||
|
||||
var soort = await _soorten.FindAsync(veld.CategoryId, ct);
|
||||
if (soort is not null) soort.GewijzigdOp = _clock.UtcNow;
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition),
|
||||
veld.CategoryId, $"veld gewijzigd: {veld.Label}", ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Haalt een veld weg. Alleen zolang er geen kluisitem van deze soort bestaat: anders staan
|
||||
/// er ingevulde gegevens onder en is "niet-actief" de weg - die waarden blijven dan gewoon
|
||||
/// bewaard en zichtbaar bij de items die ze hebben.
|
||||
/// </summary>
|
||||
public async Task<Result> VeldVerwijderenAsync(long currentUserId, long veldId, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var veld = await _soorten.FindFieldAsync(veldId, ct);
|
||||
if (veld is null) return Result.Fail("Dit veld bestaat niet.");
|
||||
|
||||
if (await _soorten.AantalItemsAsync(veld.CategoryId, ct) > 0)
|
||||
return Result.Fail("Er zijn al items van deze soort; zet het veld op niet-actief in plaats van het te verwijderen. Wat er is ingevuld blijft dan bewaard.");
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition),
|
||||
veld.CategoryId, $"veld verwijderd: {veld.Label}", ct);
|
||||
_soorten.Remove(veld);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ hulp
|
||||
|
||||
private async Task<bool> IsBeheerderAsync(long userId, CancellationToken ct) =>
|
||||
(await _users.FindByIdAsync(userId, ct))?.IsBeheerder == true;
|
||||
|
||||
private async Task<string?> ControleerNaamAsync(int? eigenId, string? naam, string? uitleg, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(naam)) return "Geef de soort een naam.";
|
||||
if (naam.Trim().Length > 80) return "Een naam van meer dan 80 tekens is te lang.";
|
||||
if (uitleg is { Length: > 500 }) return "De uitleg mag niet langer zijn dan 500 tekens.";
|
||||
|
||||
var dubbel = (await _soorten.ListAsync(ct))
|
||||
.Any(s => s.Id != eigenId && string.Equals(s.Naam, naam.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
return dubbel ? "Er bestaat al een soort met deze naam." : null;
|
||||
}
|
||||
|
||||
private static string? ControleerVeld(string? label, string? soortVeld, string? opties, string? hulptekst, bool verplicht)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(label)) return "Geef het veld een naam.";
|
||||
if (label.Trim().Length > 100) return "De naam van een veld mag niet langer zijn dan 100 tekens.";
|
||||
if (!VeldSoort.Bekend(soortVeld)) return "Dit soort veld kennen we niet.";
|
||||
if (hulptekst is { Length: > 300 }) return "De hulptekst mag niet langer zijn dan 300 tekens.";
|
||||
|
||||
if (soortVeld == VeldSoort.Keuze)
|
||||
{
|
||||
var lijst = (SchoneOpties(opties) ?? "").Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lijst.Length < 2) return "Een keuzelijst heeft minstens twee opties nodig, één per regel.";
|
||||
if ((SchoneOpties(opties) ?? "").Length > 1000) return "De opties zijn samen te lang.";
|
||||
}
|
||||
|
||||
if (soortVeld == VeldSoort.Uitleg && string.IsNullOrWhiteSpace(hulptekst))
|
||||
return "Een uitlegtekst heeft een tekst nodig; vul die in bij de hulptekst.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Opties één per regel, zonder lege regels en dubbelen.</summary>
|
||||
private static string? SchoneOpties(string? opties) =>
|
||||
opties is null
|
||||
? null
|
||||
: string.Join('\n', opties.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
private static string? Schoon(string? tekst) => string.IsNullOrWhiteSpace(tekst) ? null : tekst.Trim();
|
||||
|
||||
private static readonly Dictionary<char, string> Accenten = BouwAccenten();
|
||||
|
||||
private static Dictionary<char, string> BouwAccenten()
|
||||
{
|
||||
var d = new Dictionary<char, string>();
|
||||
void Voeg(string letters, string door) { foreach (var l in letters) d[l] = door; }
|
||||
Voeg("àáâãäåāăą", "a"); Voeg("çćĉċč", "c"); Voeg("ďđ", "d"); Voeg("èéêëēĕėęě", "e");
|
||||
Voeg("ĝğġģ", "g"); Voeg("ĥħ", "h"); Voeg("ìíîïĩīĭįı", "i"); Voeg("ĵ", "j"); Voeg("ķ", "k");
|
||||
Voeg("ĺļľŀł", "l"); Voeg("ñńņňʼn", "n"); Voeg("òóôõöøōŏő", "o"); Voeg("ŕŗř", "r");
|
||||
Voeg("śŝşš", "s"); Voeg("ţťŧ", "t"); Voeg("ùúûüũūŭůűų", "u"); Voeg("ŵ", "w");
|
||||
Voeg("ýÿŷ", "y"); Voeg("źżž", "z"); d['ß'] = "ss"; d['æ'] = "ae"; d['œ'] = "oe"; d['þ'] = "th";
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Een vaste, korte naam voor een veld, afgeleid van het label: "Wil je gereanimeerd worden?"
|
||||
/// wordt "wil_je_gereanimeerd_worden". Is die al in gebruik in deze soort, dan komt er een
|
||||
/// nummer achter.
|
||||
/// </summary>
|
||||
public static string NieuweSleutel(string label, IEnumerable<string> bestaand)
|
||||
{
|
||||
var bouw = new StringBuilder();
|
||||
foreach (var teken in label.ToLowerInvariant())
|
||||
{
|
||||
// Accenten zelf wegwerken in plaats van via string.Normalize: dat werkt niet overal
|
||||
// hetzelfde zodra InvariantGlobalization aan staat (zoals hier), en de sleutel mag niet
|
||||
// per machine verschillen.
|
||||
var ascii = Accenten.TryGetValue(teken, out var zonder) ? zonder : teken.ToString();
|
||||
foreach (var t in ascii)
|
||||
{
|
||||
if (char.IsAsciiLetterOrDigit(t)) bouw.Append(t);
|
||||
else if (bouw.Length > 0 && bouw[^1] != '_') bouw.Append('_');
|
||||
}
|
||||
}
|
||||
|
||||
var basis = bouw.ToString().Trim('_');
|
||||
if (basis.Length == 0) basis = "veld";
|
||||
if (basis.Length > 30) basis = basis[..30].TrimEnd('_');
|
||||
|
||||
var gebruikt = new HashSet<string>(bestaand, StringComparer.OrdinalIgnoreCase);
|
||||
var sleutel = basis;
|
||||
for (var n = 2; gebruikt.Contains(sleutel); n++) sleutel = $"{basis}_{n}";
|
||||
return sleutel;
|
||||
}
|
||||
|
||||
private static KluisSoort NaarSoort(VaultCategoryDefinition s) =>
|
||||
new(s.Id, s.Naam, s.Uitleg, s.BesprekenMet, s.Ingebouwd,
|
||||
s.Velden.OrderBy(v => v.Volgorde).ThenBy(v => v.Id)
|
||||
.Select(v => new KluisVeld(v.Sleutel, v.Label, v.Soort, v.OptieLijst, v.Hulptekst, v.Verplicht, v.Actief))
|
||||
.ToList());
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class VaultService
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IClock _clock;
|
||||
private readonly IAfbeeldingVerkleiner _verkleiner;
|
||||
private readonly VaultCategoryService? _soorten;
|
||||
|
||||
/// <summary>De langste zijde van de kleine versie die bij een foto wordt bewaard.</summary>
|
||||
public const int ThumbnailZijde = 160;
|
||||
@@ -46,12 +47,14 @@ public class VaultService
|
||||
public VaultService(IVaultRepository vault, IPersonRepository people, IMessageRepository messages,
|
||||
IUserRepository users, IMasterKeyProtector keys, IFileStore files, IAuditLog audit,
|
||||
IUnitOfWork uow, IClock clock, IAfbeeldingVerkleiner? verkleiner = null,
|
||||
IFamilyRepository? families = null, ILegacyRepository? legacy = null)
|
||||
IFamilyRepository? families = null, ILegacyRepository? legacy = null,
|
||||
VaultCategoryService? soorten = null)
|
||||
{
|
||||
_vault = vault; _people = people; _messages = messages; _users = users; _keys = keys;
|
||||
_files = files; _audit = audit; _uow = uow; _clock = clock; _families = families;
|
||||
_verkleiner = verkleiner ?? new GeenAfbeeldingVerkleiner();
|
||||
_legacy = legacy;
|
||||
_soorten = soorten;
|
||||
}
|
||||
|
||||
/// <summary>Titels en categorieën van de kluis. De inhoud blijft versleuteld tot hij echt geopend wordt.</summary>
|
||||
@@ -71,6 +74,10 @@ public class VaultService
|
||||
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
||||
return Result<VaultItem>.Fail("Kies een datum waarop dit item vrijkomt.");
|
||||
|
||||
var soortControle = await ControleerSoortAsync(user, category, payload, nieuw: true, aanbodControleren: true, ct);
|
||||
if (!soortControle.Succeeded) return Result<VaultItem>.Fail(soortControle.Error!);
|
||||
var kanBespreken = await KanBesprekenMetAsync(category, ct);
|
||||
|
||||
var now = _clock.UtcNow;
|
||||
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
||||
var contentKey = EnvelopeCrypto.NewKey();
|
||||
@@ -89,7 +96,7 @@ public class VaultService
|
||||
WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey),
|
||||
ReleasePolicy = release,
|
||||
ReleaseAt = releaseAt,
|
||||
SharedManagerPersonId = KanBesprekenMet(category) ? sharedManagerPersonId : null,
|
||||
SharedManagerPersonId = kanBespreken ? sharedManagerPersonId : null,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
@@ -132,6 +139,12 @@ public class VaultService
|
||||
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
||||
return Result.Fail("Kies een datum waarop dit item vrijkomt.");
|
||||
|
||||
// Blijft het item in dezelfde soort, dan is niet-actief geen bezwaar: het is zijn eigen item.
|
||||
var soortControle = await ControleerSoortAsync(user, category, payload, nieuw: false,
|
||||
aanbodControleren: item.Category != category, ct);
|
||||
if (!soortControle.Succeeded) return Result.Fail(soortControle.Error!);
|
||||
var kanBespreken = await KanBesprekenMetAsync(category, ct);
|
||||
|
||||
var now = _clock.UtcNow;
|
||||
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
||||
var contentKey = EnvelopeCrypto.NewKey();
|
||||
@@ -147,7 +160,7 @@ public class VaultService
|
||||
item.WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey);
|
||||
item.ReleasePolicy = release;
|
||||
item.ReleaseAt = releaseAt;
|
||||
item.SharedManagerPersonId = KanBesprekenMet(category) ? sharedManagerPersonId : null;
|
||||
item.SharedManagerPersonId = kanBespreken ? sharedManagerPersonId : null;
|
||||
item.UpdatedAt = now;
|
||||
|
||||
// Eerder verleende toegang was op de oude sleutel; die is nu waardeloos. Wie het item
|
||||
@@ -320,6 +333,66 @@ public class VaultService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bij welke soorten kluisitems een beheerder te kiezen is om vooraf mee te bespreken. Dat is
|
||||
/// een instelling per soort die de beheerder op de website zet; voorheen stond hij vast op
|
||||
/// uitvaartwens en donorcodicil, en dat zijn ook de startwaarden. Zonder soortenlijst (een
|
||||
/// oude opzet, of een test) geldt nog die oude regel.
|
||||
/// </summary>
|
||||
private async Task<bool> KanBesprekenMetAsync(VaultCategory category, CancellationToken ct)
|
||||
{
|
||||
if (_soorten is null)
|
||||
return category is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil;
|
||||
|
||||
return (await _soorten.VindAsync((int)category, ct))?.BesprekenMet == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kijkt of dit soort item gemaakt of gewijzigd mag worden en maakt de eigen velden schoon.
|
||||
///
|
||||
/// <paramref name="aanbodControleren"/>: wordt de soort nog aangeboden aan deze persoon? Dat
|
||||
/// geldt bij een nieuw item, en bij een item dat van soort wisselt. Een bestaand item dat in
|
||||
/// zijn eigen soort blijft, hoeft dat niet: wie een soort al gebruikt, houdt hem.
|
||||
///
|
||||
/// Een door de beheerder gemaakte soort bewaart zijn velden in <see cref="VaultPayload.Velden"/>.
|
||||
/// Komt er bij het wijzigen niets mee, dan is dat een app die de soort niet kent - die zou de
|
||||
/// ingevulde gegevens overschrijven met niets, dus dat weigeren we.
|
||||
/// </summary>
|
||||
private async Task<Result> ControleerSoortAsync(User user, VaultCategory category, VaultPayload payload,
|
||||
bool nieuw, bool aanbodControleren, CancellationToken ct)
|
||||
{
|
||||
if (_soorten is null) return Result.Ok();
|
||||
|
||||
VaultCategoryDefinition? soort;
|
||||
if (aanbodControleren)
|
||||
{
|
||||
var mag = await _soorten.MagGebruikenAsync(user.PersonId, (int)category, ct);
|
||||
if (!mag.Succeeded) return Result.Fail(mag.Error!);
|
||||
soort = mag.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
soort = await _soorten.VindAsync((int)category, ct);
|
||||
if (soort is null) return Result.Ok(); // een nummer dat nergens meer bij hoort: niet in de weg zitten
|
||||
}
|
||||
|
||||
if (soort!.Ingebouwd)
|
||||
{
|
||||
payload.Velden = null;
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
if (!nieuw && payload.Velden is null)
|
||||
return Result.Fail("Dit item kan alleen met een bijgewerkte versie van de app worden gewijzigd. Werk de app bij en probeer het opnieuw.");
|
||||
|
||||
var schoon = VaultCategoryService.ControleerVelden(
|
||||
soort, payload.Velden?.ToDictionary(v => v.Key, v => (string?)v.Value));
|
||||
if (!schoon.Succeeded) return Result.Fail(schoon.Error!);
|
||||
|
||||
payload.Velden = schoon.Value;
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Geeft de gekozen nalatenschapsbeheerder van een Uitvaartwens meteen leestoegang tot dit
|
||||
/// item, zodat het al vóór een overlijden te bespreken is. Alleen als de gekozen persoon
|
||||
@@ -327,14 +400,6 @@ public class VaultService
|
||||
/// is er geen datasleutel om de contentsleutel opnieuw mee in te pakken). Ontbreekt een van
|
||||
/// beide, dan wordt de keuze stil overgeslagen; het item zelf blijft wel bewaard staan.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Bij welke soorten kluisitems een beheerder te kiezen is om vooraf mee te bespreken. Op dit
|
||||
/// moment een uitvaartwens en een donorcodicil - allebei dingen waar je iemand graag vooraf
|
||||
/// bij betrekt, in plaats van dat diegene het pas na een overlijden voor het eerst ziet.
|
||||
/// </summary>
|
||||
private static bool KanBesprekenMet(VaultCategory category) =>
|
||||
category is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil;
|
||||
|
||||
private async Task DeelMetBeheerderAsync(VaultItem item, User owner, long beheerderPersonId, CancellationToken ct)
|
||||
{
|
||||
if (_legacy is not null)
|
||||
|
||||
@@ -333,6 +333,21 @@ public class MijnBesluitClient : IAsyncDisposable
|
||||
public Task<Uitkomst<IReadOnlyList<KluisItem>>> KluisAsync(int? categorie = null, CancellationToken ct = default) =>
|
||||
StuurAsync<IReadOnlyList<KluisItem>>(SocketActions.VaultList, new { category = categorie }, ct);
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems die ik te zien krijg. Faalt dit of komt er niets terug (een oudere
|
||||
/// server), dan hoort het scherm op zijn ingebouwde lijst terug te vallen - zie
|
||||
/// <see cref="IngebouwdeSoorten"/>.
|
||||
/// </summary>
|
||||
public Task<Uitkomst<IReadOnlyList<KluisSoort>>> KluisSoortenAsync(CancellationToken ct = default) =>
|
||||
StuurAsync<IReadOnlyList<KluisSoort>>(SocketActions.VaultCategories, null, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort op nummer, met zijn velden - ook als die soort niet meer wordt aangeboden. Voor het
|
||||
/// lezen van het item van een ander, waar je eigen lijst de soort niet hoeft te kennen.
|
||||
/// </summary>
|
||||
public Task<Uitkomst<KluisSoort>> KluisSoortAsync(int soortId, CancellationToken ct = default) =>
|
||||
StuurAsync<KluisSoort>(SocketActions.VaultCategory, new { id = soortId }, ct);
|
||||
|
||||
public Task<Uitkomst<KluisInhoud>> KluisOpenenAsync(long itemId, CancellationToken ct = default) =>
|
||||
StuurAsync<KluisInhoud>(SocketActions.VaultOpen, new { id = itemId }, ct);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
namespace MijnBesluit.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort kluisitem zoals de beheerder die instelt: "Wachtwoord", "Polis", of een nieuwe als
|
||||
/// "Niet reanimeren". Het nummer is hetzelfde nummer als <c>VaultItem.Category</c>; de elf
|
||||
/// ingebouwde soorten en Overig (99) houden hun oude nummer, nieuwe soorten beginnen bij 100.
|
||||
///
|
||||
/// Een ingebouwde soort heeft zijn invulscherm in de app zelf en dus geen <see cref="Velden"/>.
|
||||
/// Een nieuwe soort beschrijft zijn velden hier, en de app tekent het formulier daarnaar - zodat
|
||||
/// een nieuwe soort geen nieuwe versie van de app vraagt.
|
||||
/// </summary>
|
||||
public class VaultCategoryDefinition
|
||||
{
|
||||
/// <summary>Vanaf dit nummer zijn soorten door de beheerder gemaakt.</summary>
|
||||
public const int EersteEigenNummer = 100;
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Naam { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Korte uitleg die in de app onder de keuzelijst staat. Mag leeg.</summary>
|
||||
public string? Uitleg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wordt de soort nog aangeboden? Niet-actief verdwijnt uit de keuzelijst van iedereen die er
|
||||
/// nog nooit een item van maakte; wie dat wel deed, blijft de soort zien.
|
||||
/// </summary>
|
||||
public bool Actief { get; set; } = true;
|
||||
|
||||
public int Volgorde { get; set; } = 1000;
|
||||
|
||||
/// <summary>Mag de eigenaar bij dit soort een beheerder kiezen om het vooraf mee te bespreken?</summary>
|
||||
public bool BesprekenMet { get; set; }
|
||||
|
||||
/// <summary>Invulscherm zit in de app; velden zijn hier niet te wijzigen en de soort is niet te verwijderen.</summary>
|
||||
public bool Ingebouwd { get; set; }
|
||||
|
||||
public DateTime AangemaaktOp { get; set; } = DateTime.UtcNow;
|
||||
public DateTime GewijzigdOp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public List<VaultCategoryField> Velden { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Een veld in het formulier van een door de beheerder gemaakte soort.</summary>
|
||||
public class VaultCategoryField
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// De vaste naam waaronder de waarde in de versleutelde inhoud staat. Wordt bij het maken
|
||||
/// van het veld uit het label afgeleid en wijzigt daarna nooit meer: een label hernoemen mag,
|
||||
/// maar mag geen ingevulde gegevens losmaken van hun veld.
|
||||
/// </summary>
|
||||
public string Sleutel { get; set; } = string.Empty;
|
||||
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Een van <see cref="VeldSoort"/>. Verandert na het maken niet meer.</summary>
|
||||
public string Soort { get; set; } = VeldSoort.Tekst;
|
||||
|
||||
/// <summary>Bij <see cref="VeldSoort.Keuze"/>: de opties, één per regel.</summary>
|
||||
public string? Opties { get; set; }
|
||||
|
||||
public string? Hulptekst { get; set; }
|
||||
public bool Verplicht { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Een niet-actief veld staat niet meer in nieuwe formulieren, maar wat er al is ingevuld
|
||||
/// blijft staan en wordt bij het wijzigen van zo'n item nog getoond.
|
||||
/// </summary>
|
||||
public bool Actief { get; set; } = true;
|
||||
|
||||
public int Volgorde { get; set; } = 1000;
|
||||
|
||||
public VaultCategoryDefinition? Category { get; set; }
|
||||
|
||||
/// <summary>De opties als lijst, zonder lege regels.</summary>
|
||||
public IReadOnlyList<string> OptieLijst =>
|
||||
string.IsNullOrWhiteSpace(Opties)
|
||||
? Array.Empty<string>()
|
||||
: Opties.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
/// <summary>De soorten invoerveld die een formulier kent.</summary>
|
||||
public static class VeldSoort
|
||||
{
|
||||
/// <summary>Eén regel tekst.</summary>
|
||||
public const string Tekst = "tekst";
|
||||
|
||||
/// <summary>Meerdere regels tekst.</summary>
|
||||
public const string Lang = "lang";
|
||||
|
||||
/// <summary>Een datum; wordt als jjjj-mm-dd bewaard.</summary>
|
||||
public const string Datum = "datum";
|
||||
|
||||
/// <summary>Kiezen uit een lijst, bijvoorbeeld Ja / Nee / Alleen na overleg.</summary>
|
||||
public const string Keuze = "keuze";
|
||||
|
||||
/// <summary>Geen invoer maar een stukje uitleg tussen de velden.</summary>
|
||||
public const string Uitleg = "uitleg";
|
||||
|
||||
public static readonly string[] Alle = { Tekst, Lang, Datum, Keuze, Uitleg };
|
||||
|
||||
public static bool Bekend(string? soort) => soort is not null && Alle.Contains(soort);
|
||||
|
||||
/// <summary>Hoe de soort op het scherm heet, voor in het beheerscherm.</summary>
|
||||
public static string Naam(string soort) => soort switch
|
||||
{
|
||||
Tekst => "Korte tekst",
|
||||
Lang => "Lange tekst",
|
||||
Datum => "Datum",
|
||||
Keuze => "Keuzelijst",
|
||||
Uitleg => "Uitlegtekst (geen invoer)",
|
||||
_ => soort
|
||||
};
|
||||
}
|
||||
@@ -73,7 +73,8 @@ public enum AuditAction
|
||||
OverlijdenGemeld = 50, OverlijdenBetwist = 51, OverlijdenBevestigd = 52,
|
||||
NalatenschapToegangVerleend = 60,
|
||||
AdvertentieIngediend = 70, AdvertentieBetaald = 71, AdvertentieGoedgekeurd = 72,
|
||||
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74
|
||||
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74,
|
||||
KluisSoortBeheerd = 80
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -93,6 +93,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFamilyRepository, FamilyRepository>();
|
||||
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
||||
services.AddScoped<IVaultRepository, VaultRepository>();
|
||||
services.AddScoped<IVaultCategoryRepository, VaultCategoryRepository>();
|
||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
services.AddScoped<ILegacyRepository, LegacyRepository>();
|
||||
services.AddScoped<IToestelRepository, ToestelRepository>();
|
||||
@@ -103,6 +104,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IAuditLog, EfAuditLog>();
|
||||
|
||||
services.AddScoped<AccountService>();
|
||||
services.AddScoped<VaultCategoryService>();
|
||||
services.AddScoped<VaultService>();
|
||||
// De link naar de landingspagina voor iemand zonder account moet naar de echte site wijzen.
|
||||
services.AddScoped(sp =>
|
||||
|
||||
@@ -18,6 +18,8 @@ public class MijnBesluitDbContext : DbContext
|
||||
public DbSet<Invitation> Invitations => Set<Invitation>();
|
||||
public DbSet<VaultItem> VaultItems => Set<VaultItem>();
|
||||
public DbSet<VaultFile> VaultFiles => Set<VaultFile>();
|
||||
public DbSet<VaultCategoryDefinition> VaultCategories => Set<VaultCategoryDefinition>();
|
||||
public DbSet<VaultCategoryField> VaultCategoryFields => Set<VaultCategoryField>();
|
||||
public DbSet<Toestel> Toestellen => Set<Toestel>();
|
||||
public DbSet<Sessie> Sessies => Set<Sessie>();
|
||||
public DbSet<ContentGrant> ContentGrants => Set<ContentGrant>();
|
||||
@@ -218,6 +220,37 @@ public class MijnBesluitDbContext : DbContext
|
||||
e.HasOne(x => x.VaultFile).WithMany().HasForeignKey(x => x.VaultFileId);
|
||||
});
|
||||
|
||||
b.Entity<VaultCategoryDefinition>(e =>
|
||||
{
|
||||
e.ToTable("vault_categories");
|
||||
e.Property(x => x.Id).HasColumnName("id");
|
||||
e.Property(x => x.Naam).HasColumnName("naam").HasMaxLength(80).IsRequired();
|
||||
e.Property(x => x.Uitleg).HasColumnName("uitleg").HasMaxLength(500);
|
||||
e.Property(x => x.Actief).HasColumnName("actief");
|
||||
e.Property(x => x.Volgorde).HasColumnName("volgorde");
|
||||
e.Property(x => x.BesprekenMet).HasColumnName("bespreken_met");
|
||||
e.Property(x => x.Ingebouwd).HasColumnName("ingebouwd");
|
||||
e.Property(x => x.AangemaaktOp).HasColumnName("aangemaakt_op");
|
||||
e.Property(x => x.GewijzigdOp).HasColumnName("gewijzigd_op");
|
||||
e.HasMany(x => x.Velden).WithOne(v => v.Category).HasForeignKey(v => v.CategoryId);
|
||||
});
|
||||
|
||||
b.Entity<VaultCategoryField>(e =>
|
||||
{
|
||||
e.ToTable("vault_category_fields");
|
||||
e.Property(x => x.Id).HasColumnName("id");
|
||||
e.Property(x => x.CategoryId).HasColumnName("category_id");
|
||||
e.Property(x => x.Sleutel).HasColumnName("sleutel").HasMaxLength(40).IsRequired();
|
||||
e.Property(x => x.Label).HasColumnName("label").HasMaxLength(100).IsRequired();
|
||||
e.Property(x => x.Soort).HasColumnName("soort").HasMaxLength(10).IsRequired();
|
||||
e.Property(x => x.Opties).HasColumnName("opties").HasMaxLength(1000);
|
||||
e.Property(x => x.Hulptekst).HasColumnName("hulptekst").HasMaxLength(300);
|
||||
e.Property(x => x.Verplicht).HasColumnName("verplicht");
|
||||
e.Property(x => x.Actief).HasColumnName("actief");
|
||||
e.Property(x => x.Volgorde).HasColumnName("volgorde");
|
||||
e.Ignore(x => x.OptieLijst);
|
||||
});
|
||||
|
||||
b.Entity<ContentGrant>(e =>
|
||||
{
|
||||
e.ToTable("content_grants");
|
||||
|
||||
@@ -229,6 +229,44 @@ public class InvitationRepository : IInvitationRepository
|
||||
public void Remove(Invitation invitation) => _db.Invitations.Remove(invitation);
|
||||
}
|
||||
|
||||
/// <summary>De soorten kluisitems; zie <see cref="IVaultCategoryRepository"/>.</summary>
|
||||
public class VaultCategoryRepository : IVaultCategoryRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public VaultCategoryRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<VaultCategoryDefinition>> ListAsync(CancellationToken ct = default) =>
|
||||
await _db.VaultCategories.Include(c => c.Velden)
|
||||
.OrderBy(c => c.Volgorde).ThenBy(c => c.Naam).ToListAsync(ct);
|
||||
|
||||
public Task<VaultCategoryDefinition?> FindAsync(int id, CancellationToken ct = default) =>
|
||||
_db.VaultCategories.Include(c => c.Velden).FirstOrDefaultAsync(c => c.Id == id, ct);
|
||||
|
||||
public Task<VaultCategoryField?> FindFieldAsync(long fieldId, CancellationToken ct = default) =>
|
||||
_db.VaultCategoryFields.FirstOrDefaultAsync(f => f.Id == fieldId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<SoortGebruik>> GebruikAsync(CancellationToken ct = default) =>
|
||||
await _db.VaultItems.Where(v => v.DeletedAt == null)
|
||||
.GroupBy(v => v.Category)
|
||||
.Select(g => new SoortGebruik((int)g.Key, g.Count(), g.Select(v => v.OwnerPersonId).Distinct().Count()))
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlySet<int>> InGebruikDoorAsync(long personId, CancellationToken ct = default)
|
||||
{
|
||||
var lijst = await _db.VaultItems.Where(v => v.OwnerPersonId == personId && v.DeletedAt == null)
|
||||
.Select(v => v.Category).Distinct().ToListAsync(ct);
|
||||
return lijst.Select(c => (int)c).ToHashSet();
|
||||
}
|
||||
|
||||
public Task<int> AantalItemsAsync(int categoryId, CancellationToken ct = default) =>
|
||||
_db.VaultItems.CountAsync(v => v.Category == (VaultCategory)categoryId && v.DeletedAt == null, ct);
|
||||
|
||||
public void Add(VaultCategoryDefinition soort) => _db.VaultCategories.Add(soort);
|
||||
public void Remove(VaultCategoryDefinition soort) => _db.VaultCategories.Remove(soort);
|
||||
public void Add(VaultCategoryField veld) => _db.VaultCategoryFields.Add(veld);
|
||||
public void Remove(VaultCategoryField veld) => _db.VaultCategoryFields.Remove(veld);
|
||||
}
|
||||
|
||||
public class VaultRepository : IVaultRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
|
||||
@@ -98,6 +98,19 @@ public static class SocketActions
|
||||
/// <summary>Kluisitems die iemand rechtstreeks met jou heeft gedeeld, vooraf te bespreken.</summary>
|
||||
public const string VaultSharedWithMe = "vault.shared-with-me";
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems die deze gebruiker te zien krijgt, met bij een door de beheerder gemaakte
|
||||
/// soort de velden van het formulier. Een oudere server kent dit niet; de app valt dan terug op
|
||||
/// de ingebouwde lijst.
|
||||
/// </summary>
|
||||
public const string VaultCategories = "vault.categories";
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort op nummer, met zijn velden, ook als die soort niet meer wordt aangeboden. Voor het
|
||||
/// lezen van het item van een ander, waar de lijst van jezelf de soort niet hoeft te bevatten.
|
||||
/// </summary>
|
||||
public const string VaultCategory = "vault.category";
|
||||
|
||||
/// <summary>Nalatenschappen die aan jou zijn toevertrouwd, en wat daarvan is vrijgegeven.</summary>
|
||||
public const string LegacyEntrusted = "legacy.entrusted";
|
||||
public const string LegacyEntrustedItems = "legacy.entrusted-items";
|
||||
|
||||
@@ -15,6 +15,12 @@ public class RequestScope
|
||||
public required LegacyService Legacy { get; init; }
|
||||
public required OverzichtService Overzicht { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems. Optioneel: zonder deze dienst werkt de kluis met de ingebouwde soorten
|
||||
/// en krijgen items geen soortnaam mee, en de app valt terug op haar eigen lijst.
|
||||
/// </summary>
|
||||
public VaultCategoryService? Soorten { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Voor het samenvoegen van dubbele profielen. Optioneel: zonder deze dienst werkt alles,
|
||||
/// alleen wordt een tweede uitnodiging dan geweigerd in plaats van samengevoegd.
|
||||
@@ -76,6 +82,17 @@ public class SocketDispatcher
|
||||
f.Id, f.FileName, f.ContentType, f.SizeBytes, f.CreatedAt, f.IsProfilePhoto
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// De namen van alle soorten op nummer, om onder elk kluisitem te kunnen zetten hoe het heet.
|
||||
/// Een soort die de beheerder zelf maakte staat niet in de lijst van de app zelf, dus de naam
|
||||
/// reist met het item mee. Zonder soortendienst is de lijst leeg en valt de app terug.
|
||||
/// </summary>
|
||||
private static async Task<IReadOnlyDictionary<int, string>> SoortNamenAsync(RequestScope scope, CancellationToken ct) =>
|
||||
scope.Soorten is null ? new Dictionary<int, string>() : await scope.Soorten.NamenAsync(ct);
|
||||
|
||||
private static string? SoortNaam(IReadOnlyDictionary<int, string> namen, int soort) =>
|
||||
namen.TryGetValue(soort, out var naam) ? naam : null;
|
||||
|
||||
/// <summary>Voor als er geen klok wordt meegegeven; de overdracht heeft er wel een nodig.</summary>
|
||||
private sealed class SysteemKlok : MijnBesluit.Application.Abstractions.IClock
|
||||
{
|
||||
@@ -451,12 +468,31 @@ public class SocketDispatcher
|
||||
{
|
||||
var p = request.PayloadAs<VaultListPayload>();
|
||||
var items = await scope.Vault.ListAsync(session!.PersonId, p?.Category, ct);
|
||||
var namen = await SoortNamenAsync(scope, ct);
|
||||
return SocketResponse.Ok(id, items.Select(i => new
|
||||
{
|
||||
i.Id, i.Title, i.Category, i.ReleasePolicy, i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId
|
||||
i.Id, i.Title, i.Category, i.ReleasePolicy, i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId,
|
||||
CategoryName = SoortNaam(namen, (int)i.Category)
|
||||
}));
|
||||
}
|
||||
|
||||
case SocketActions.VaultCategory:
|
||||
{
|
||||
var p = request.PayloadAs<IdPayload>();
|
||||
if (p is null) return SocketResponse.Error(id, "Geen soort opgegeven.");
|
||||
var soort = scope.Soorten is null ? null : await scope.Soorten.SoortAsync((int)p.Id, ct);
|
||||
return soort is null ? SocketResponse.Error(id, "Deze soort bestaat niet.") : SocketResponse.Ok(id, soort);
|
||||
}
|
||||
|
||||
case SocketActions.VaultCategories:
|
||||
{
|
||||
// Zonder soortendienst een lege lijst: de app valt dan terug op haar ingebouwde soorten.
|
||||
var lijst = scope.Soorten is null
|
||||
? Array.Empty<KluisSoort>()
|
||||
: (await scope.Soorten.ZichtbaarVoorAsync(session!.PersonId, ct)).ToArray();
|
||||
return SocketResponse.Ok(id, lijst);
|
||||
}
|
||||
|
||||
case SocketActions.VaultOpen:
|
||||
{
|
||||
var p = request.PayloadAs<IdPayload>();
|
||||
@@ -807,10 +843,12 @@ public class SocketDispatcher
|
||||
var r = await scope.Legacy.NalatenschapItemsAsync(p.PersonId, session!.UserId, ct);
|
||||
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
||||
|
||||
var nalatenschapNamen = await SoortNamenAsync(scope, ct);
|
||||
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
||||
{
|
||||
i.Id, i.Title, Category = (int)i.Category, ReleasePolicy = (int)i.ReleasePolicy,
|
||||
i.ReleaseAt, i.CreatedAt
|
||||
i.ReleaseAt, i.CreatedAt,
|
||||
CategoryName = SoortNaam(nalatenschapNamen, (int)i.Category)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -828,10 +866,12 @@ public class SocketDispatcher
|
||||
var r = await scope.Vault.ListSharedWithMeAsync(session!.UserId, ct);
|
||||
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
||||
|
||||
var gedeeldNamen = await SoortNamenAsync(scope, ct);
|
||||
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
||||
{
|
||||
i.Id, i.Title, Category = (int)i.Category, ReleasePolicy = (int)i.ReleasePolicy,
|
||||
i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId
|
||||
i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId,
|
||||
CategoryName = SoortNaam(gedeeldNamen, (int)i.Category)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ public class SocketServerService : BackgroundService
|
||||
Accounts = scope.ServiceProvider.GetRequiredService<AccountService>(),
|
||||
Families = scope.ServiceProvider.GetRequiredService<FamilyService>(),
|
||||
Vault = scope.ServiceProvider.GetRequiredService<VaultService>(),
|
||||
Soorten = scope.ServiceProvider.GetRequiredService<VaultCategoryService>(),
|
||||
Messages = scope.ServiceProvider.GetRequiredService<MessageService>(),
|
||||
Legacy = scope.ServiceProvider.GetRequiredService<LegacyService>(),
|
||||
Meldingen = scope.ServiceProvider.GetRequiredService<MeldingService>(),
|
||||
|
||||
@@ -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 ›</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 ›</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">‹ 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…</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> · verplicht</text> }
|
||||
@if (!v.Actief) { <text> · 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 Nee 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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user