Files
MijnBesluit/src/MijnBesluit.Web/Components/Pages/Beheer.razor
T
jfandClaude Sonnet 5.5 af13d0e533 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
2026-10-20 08:15:40 +00:00

343 lines
15 KiB
Plaintext

@page "/beheer"
@attribute [Authorize]
@inject BeheerService Beheerkant
@rendermode InteractiveServer
<PageTitle>Beheer - MijnBesluit</PageTitle>
@* De beheerpagina. Twee handelingen: een overlijden terugdraaien dat ten onrechte is bevestigd,
en een samenvoeging terugdraaien die niet had gemoeten.
Zie BeheerService voor waarom er niet meer op staat. Kort: een beheerder kan geen kluis
openen en kan niet op naam zoeken. *@
@if (!Magdit)
{
<h1>Niet gevonden</h1>
<p class="zacht">Deze pagina bestaat niet, of hij is niet voor jou.</p>
}
else
{
<h1>Beheer</h1>
<p class="zacht">
Hier draai je twee dingen terug die niemand anders kan: een overlijden dat ten onrechte
is bevestigd, en een samenvoeging van twee profielen die niet had gemoeten. Voor het
eerste is dit nodig als de bezwaarlink uit de waarschuwingsmail verlopen is; de
betrokkene kan dan niet meer inloggen en heeft zelf geen weg terug.
</p>
@* Adverteren heeft een eigen scherm: dat gaat niet over terugdraaien maar over betalingen
afvinken en advertenties beoordelen, en daar hoor je dagelijks te komen in plaats van bij
hoge uitzondering. *@
<p><a href="/beheer/adverteren">Advertenties beheren &rsaquo;</a></p>
@* Kluissoorten: wat mensen in hun kluis kunnen vastleggen, en wat je daar zelf bij zet.
Ook een eigen scherm: dat is inrichten, geen terugdraaien. *@
<p><a href="/beheer/soorten">Kluissoorten beheren &rsaquo;</a></p>
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
@* ------------------------------------------------------------ statistieken *@
<div class="kaart">
<h2 style="margin-top:0">Gebruik van MijnBesluit</h2>
<p class="zacht" style="margin-top:0">
Aantallen, niet inhoud: hier is niets van iemands kluis of berichten te zien. Bedoeld
om de belasting in te schatten, niet om iemand te volgen.
</p>
@if (Stats is null)
{
<p class="zacht" style="margin-bottom:0">Statistieken zijn hier niet beschikbaar.</p>
}
else
{
<div class="statgrid">
<div class="statcel">
<span class="statgetal">@Stats.Profielen</span>
<span class="statlabel">Profielen (@Stats.GeclaimdeProfielen geclaimd)</span>
</div>
<div class="statcel">
<span class="statgetal">@Stats.Families</span>
<span class="statlabel">Families</span>
</div>
<div class="statcel">
<span class="statgetal">@Stats.KluisItems</span>
<span class="statlabel">Kluisitems</span>
</div>
<div class="statcel">
<span class="statgetal">@Stats.KluisBestanden</span>
<span class="statlabel">Bijlagen</span>
</div>
<div class="statcel">
<span class="statgetal">@Stats.Momenten</span>
<span class="statlabel">Momenten (@Stats.BezorgdeMomenten bezorgd)</span>
</div>
<div class="statcel">
<span class="statgetal">@Stats.Nalatenschapbeheerders</span>
<span class="statlabel">Aangewezen beheerders</span>
</div>
</div>
<Groeigrafiek Punten="Stats.GroeiPerDag" />
@if (Stats.ItemsPerCategorie.Count > 0)
{
<h3 style="margin-bottom:.6rem">Kluisitems per categorie</h3>
<div class="stapel">
@foreach (var (categorie, aantal) in Stats.ItemsPerCategorie.OrderByDescending(kv => kv.Value))
{
<div class="rij">
<span class="zacht" style="min-width:140px">@Kluisweergave.Naam(categorie)</span>
<div class="groei">
<div class="voortgang">
<span style="width:@(CategoriePercentage(aantal))%"></span>
</div>
</div>
<span class="zacht" style="min-width:2.5rem;text-align:right">@aantal</span>
</div>
}
</div>
}
}
</div>
<div class="kaart">
<h2 style="margin-top:0">Profiel opzoeken</h2>
<div class="veld">
<label for="adres">E-mailadres</label>
<input id="adres" type="email" @bind="Adres" placeholder="het volledige adres" />
<div class="uitleg">
Alleen het volledige adres, niet een deel ervan en niet op naam. Je hoort al te
weten wie je zoekt.
</div>
</div>
<button class="knop" disabled="@Bezig" @onclick="Zoeken">Zoeken</button>
</div>
@if (Gevonden is { } profiel)
{
<div class="kaart">
<h2 style="margin-top:0">@profiel.DisplayName</h2>
<dl style="margin:0">
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:190px">E-mailadres</span>
<span class="groei">@(profiel.Email ?? "onbekend")</span>
</div>
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:190px">Eigen account</span>
<span class="groei">@(profiel.IsGeclaimd ? "ja" : "nee, het profiel is nog niet geclaimd")</span>
</div>
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:190px">Meldingen</span>
<span class="groei">@profiel.Meldingen van @profiel.MeldingenNodig</span>
</div>
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:190px">Stand</span>
<span class="groei">@Stand(profiel.Stand)</span>
</div>
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:190px">Kan inloggen</span>
<span class="groei">@(profiel.AccountGeblokkeerd ? "nee, het account staat op overleden" : "ja")</span>
</div>
@if (profiel.OpgegaanIn is { } opgegaan)
{
<div class="rij" style="padding:.25rem 0">
<span class="zacht" style="min-width:190px">Opgegaan in</span>
<span class="groei">profiel @opgegaan</span>
</div>
}
</dl>
@if (profiel.OverlijdenBevestigdOp is { } bevestigd)
{
<div class="melding let-op" style="margin-top:1rem">
<p style="margin-top:0">
Het overlijden is bevestigd op @Datums.Datum(bevestigd)@(
profiel.Overlijdensdatum is { } datum ? $", met {Datums.Datum(datum)} als overlijdensdatum" : "").
</p>
<p>
Terugdraaien betekent: alle meldingen vervallen, het account wordt weer
actief, en de toegang die de nalatenschapsbeheerder had gekregen wordt
ingetrokken.
</p>
<div class="veld">
<label for="reden">Waarom draai je dit terug?</label>
<input id="reden" @bind="Reden" placeholder="bijvoorbeeld: telefonisch gesproken, leeft" />
<div class="uitleg">Dit komt in het logboek te staan, bij jouw naam.</div>
</div>
<button class="knop gevaar" disabled="@Bezig" @onclick="Herstellen">
Overlijden terugdraaien
</button>
</div>
}
else
{
<p class="zacht" style="margin-bottom:0">
Voor dit profiel is geen overlijden bevestigd. Er valt daar dus niets terug te draaien.
</p>
}
</div>
@* ------------------------------------------------------------ samenvoegingen *@
<div class="kaart">
<h2 style="margin-top:0">Samenvoegingen</h2>
@if (profiel.Samenvoegingen.Count == 0)
{
<p class="zacht" style="margin-bottom:0">
Dit profiel is nooit samengevoegd. Let op: samenvoegingen van vóór 15 september
2026 zijn niet vastgelegd en kunnen daarom ook niet worden teruggedraaid.
</p>
}
else
{
<p class="zacht" style="margin-top:0">
Terugdraaien zet alles wat toen is verhuisd terug bij het profiel dat is
opgegaan. Wat er ná die tijd bij het andere profiel is bijgekomen, blijft
daar staan &mdash; dat is daar aangemaakt en hoort daar.
</p>
<div class="stapel">
@foreach (var samenvoeging in profiel.Samenvoegingen)
{
<div class="kaart compact">
<div class="rij">
<div class="groei">
<span class="groot">
@samenvoeging.DuplicaatNaam (@samenvoeging.DuplicaatPersonId)
&rarr; @samenvoeging.DoelNaam (@samenvoeging.DoelPersonId)
</span>
<div class="zacht">
@Datums.DatumTijd(samenvoeging.SamengevoegdOp)
&middot; @samenvoeging.Stappen stap@(samenvoeging.Stappen == 1 ? "" : "pen")
</div>
</div>
@if (samenvoeging.IsTeruggedraaid)
{
<span class="label">Teruggedraaid</span>
}
</div>
@if (!samenvoeging.IsTeruggedraaid)
{
<div class="veld" style="margin-top:.8rem">
<label for="reden-@samenvoeging.Id">Waarom draai je dit terug?</label>
<input id="reden-@samenvoeging.Id" @bind="Terugreden"
placeholder="bijvoorbeeld: verkeerde uitnodiging aangenomen" />
<div class="uitleg">Dit komt in het logboek te staan, bij jouw naam.</div>
</div>
<button class="knop gevaar" disabled="@Bezig"
@onclick="() => TerugdraaienAsync(samenvoeging.Id)">
Samenvoeging terugdraaien
</button>
}
</div>
}
</div>
}
</div>
}
}
@code {
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
private long _gebruikerId;
private bool Magdit, Bezig;
private string Adres = "";
private string Reden = "";
private string Terugreden = "";
private BeheerService.Profielbeeld? Gevonden;
private string? Melding, MeldingSoort;
private Statistieken? Stats;
protected override async Task OnInitializedAsync()
{
if (AuthState is null) return;
_gebruikerId = (await AuthState).User.UserId() ?? 0;
Magdit = await Beheerkant.IsBeheerderAsync(_gebruikerId);
if (!Magdit) return;
var statistieken = await Beheerkant.StatistiekenAsync(_gebruikerId);
if (statistieken.Succeeded) Stats = statistieken.Value;
}
/// <summary>Breedte van de staaf voor deze categorie, ten opzichte van de grootste categorie.</summary>
private int CategoriePercentage(int aantal)
{
var grootste = Stats!.ItemsPerCategorie.Values.Max();
return grootste == 0 ? 0 : Math.Max(2, aantal * 100 / grootste);
}
private async Task Zoeken()
{
Bezig = true;
Melding = null;
Gevonden = null;
try
{
var resultaat = await Beheerkant.ZoekOpAdresAsync(Adres, _gebruikerId);
if (resultaat.Succeeded) Gevonden = resultaat.Value;
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
}
finally { Bezig = false; }
}
private async Task Herstellen()
{
if (Gevonden is not { } profiel) return;
Bezig = true;
Melding = null;
try
{
var resultaat = await Beheerkant.HerstelOverlijdenAsync(profiel.PersonId, _gebruikerId, Reden);
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; return; }
Melding = $"Het overlijden van {profiel.DisplayName} is teruggedraaid. " +
"Hij of zij kan weer inloggen, en de nalatenschapsbeheerder is zijn toegang kwijt.";
MeldingSoort = "goed";
Reden = "";
// Opnieuw ophalen, zodat het scherm laat zien wat er nu staat.
var opnieuw = await Beheerkant.ZoekOpAdresAsync(Adres, _gebruikerId);
Gevonden = opnieuw.Succeeded ? opnieuw.Value : null;
}
finally { Bezig = false; }
}
private async Task TerugdraaienAsync(long samenvoegingId)
{
Bezig = true;
Melding = null;
try
{
var resultaat = await Beheerkant.TerugdraaienAsync(samenvoegingId, _gebruikerId, Terugreden);
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; return; }
Melding = "De samenvoeging is teruggedraaid. De twee profielen staan weer los van elkaar.";
MeldingSoort = "goed";
Terugreden = "";
var opnieuw = await Beheerkant.ZoekOpAdresAsync(Adres, _gebruikerId);
Gevonden = opnieuw.Succeeded ? opnieuw.Value : null;
}
finally { Bezig = false; }
}
private static string Stand(DeathProcedureState stand) => stand switch
{
DeathProcedureState.Geen => "geen meldingen",
DeathProcedureState.Lopend => "procedure loopt",
DeathProcedureState.Betwist => "tegengesproken door de betrokkene",
DeathProcedureState.Bevestigd => "overlijden bevestigd",
_ => "onbekend"
};
}