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:
@@ -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